diff --git a/docs/advanced_template.md b/docs/advanced_template.md index 5a8892d..491d8a7 100644 --- a/docs/advanced_template.md +++ b/docs/advanced_template.md @@ -4,6 +4,17 @@ {tm.description} +{tm.props:if: + +## TM Properties + +Key|Value| +|:----:|:----:| +{tm.props:call:getPair:|{{item.key}}|{{item.value}}| +} + +} + ## Dataflow Diagram - Level 0 DFD ![](sample.png) @@ -100,6 +111,10 @@ Description|{{item.description}}| In Scope|{{item.inScope}}| Type|{{item:call:getElementType}}| Finding Count|{{item:call:getFindingCount}}| +{{item.props:if:{{item.props:call:getPair:|{{{{item.key}}}}|{{{{item.value}}}}| +}} +}} + {{item.findings:if: diff --git a/docs/basic_template.md b/docs/basic_template.md index 451711b..4024385 100644 --- a/docs/basic_template.md +++ b/docs/basic_template.md @@ -7,19 +7,13 @@   -{tm.assumptions:if: - -|Assumptions| +{tm.assumptions:if:|Assumptions| |-----------| {tm.assumptions:repeat:|{{item}}| } - -  -    } - ## Dataflow Diagram - Level 0 DFD ![](sample.png) @@ -67,3 +61,4 @@ Name|Description|Classification   }| + diff --git a/docs/pytm/index.html b/docs/pytm/index.html index 25d09c8..bc818ec 100644 --- a/docs/pytm/index.html +++ b/docs/pytm/index.html @@ -1635,6 +1635,7 @@

Instance variables

required=False, doc="Location of the source code that describes this element relative to the directory of the model script.", ) + props = varDict(dict([]), doc="Custom name/value pairs containing data about this element.") def __init__(self, name, **kwargs): for key, value in kwargs.items(): @@ -1758,6 +1759,22 @@

Instance variables

return True return False + + def getProperty(self, prop): + """getter method to extract data from props dict and avoid KeyError exceptions""" + + if (self.props): + try: + value = self.props[prop] + print("getProperty:value = " + value) + except KeyError: + value = None + else: + value = None + + return value + + def _attr_values(self): klass = self.__class__ result = {} @@ -1929,6 +1946,22 @@

Instance variables

return self.data.get(instance, self.default) +
var props
+
+

Custom name/value pairs containing data about this element.

+
+ +Expand source code + +
def __get__(self, instance, owner):
+    # when x.d is called we get here
+    # instance = x
+    # owner = type(x)
+    if instance is None:
+        return self
+    return self.data.get(instance, self.default)
+
+
var sourceFiles

Location of the source code that describes this element relative to the directory of the model script.

@@ -2036,6 +2069,30 @@

Methods

return self.source.inside(*boundaries) and self.sink.inBoundary is None
+
+def getProperty(self, prop) +
+
+

getter method to extract data from props dict and avoid KeyError exceptions

+
+ +Expand source code + +
def getProperty(self, prop):
+    """getter method to extract data from props dict and avoid KeyError exceptions"""
+
+    if (self.props):
+       try:
+          value = self.props[prop]
+          print("getProperty:value = " + value)
+       except KeyError:
+          value = None
+    else:
+       value = None
+
+    return value
+
+
def inside(self, *boundaries)
@@ -3548,6 +3605,7 @@

Class variables

doc="""How to handle duplicate Dataflow with same properties, except name and notes""", ) + props = varDict(dict([]), doc="Custom name/value pairs containing data about the model.") assumptions = varStrings( [], required=False, @@ -3572,6 +3630,7 @@

Class variables

cls._threats = [] cls._boundaries = [] cls._data = [] + cls._threatsExcluded = [] def _init_threats(self): TM._threats = [] @@ -3606,6 +3665,9 @@

Class variables

if not t.apply(e) and t.id not in override_ids: continue + if t.id in TM._threatsExcluded: + continue + finding_count += 1 f = Finding(e, id=str(finding_count), threat=t) logger.debug(f"new finding: {f}") @@ -3793,15 +3855,21 @@

Class variables

threats = encode_threat_data(TM._threats) findings = encode_threat_data(self.findings) + elements = encode_element_threat_data(TM._elements) + assets = encode_element_threat_data(TM._assets) + actors = encode_element_threat_data(TM._actors) + boundaries = encode_element_threat_data(TM._boundaries) + flows = encode_element_threat_data(TM._flows) + data = { "tm": self, - "dataflows": TM._flows, + "dataflows": flows, "threats": threats, "findings": findings, - "elements": TM._elements, - "assets": TM._assets, - "actors": TM._actors, - "boundaries": TM._boundaries, + "elements": elements, + "assets": assets, + "actors": actors, + "boundaries": boundaries, "data": TM._data, } @@ -3815,6 +3883,9 @@

Class variables

if result.debug: logger.setLevel(logging.DEBUG) + if result.exclude is not None: + TM._threatsExcluded = result.exclude.split(",") + if result.seq is True: print(self.seq()) @@ -3839,9 +3910,6 @@

Class variables

if result.report is not None: print(self.report(result.report)) - if result.exclude is not None: - TM._threatsExcluded = result.exclude.split(",") - if result.describe is not None: _describe_classes(result.describe.split()) @@ -3964,7 +4032,8 @@

Static methods

cls._assets = [] cls._threats = [] cls._boundaries = [] - cls._data = [] + cls._data = [] + cls._threatsExcluded = [] @@ -4099,6 +4168,22 @@

Instance variables

return self.data.get(instance, self.default) +
var props
+
+

Custom name/value pairs containing data about the model.

+
+ +Expand source code + +
def __get__(self, instance, owner):
+    # when x.d is called we get here
+    # instance = x
+    # owner = type(x)
+    if instance is None:
+        return self
+    return self.data.get(instance, self.default)
+
+
var threatsFile

JSON file with custom threats

@@ -4155,6 +4240,9 @@

Methods

if result.debug: logger.setLevel(logging.DEBUG) + if result.exclude is not None: + TM._threatsExcluded = result.exclude.split(",") + if result.seq is True: print(self.seq()) @@ -4179,9 +4267,6 @@

Methods

if result.report is not None: print(self.report(result.report)) - if result.exclude is not None: - TM._threatsExcluded = result.exclude.split(",") - if result.describe is not None: _describe_classes(result.describe.split()) @@ -4211,15 +4296,21 @@

Methods

threats = encode_threat_data(TM._threats) findings = encode_threat_data(self.findings) + elements = encode_element_threat_data(TM._elements) + assets = encode_element_threat_data(TM._assets) + actors = encode_element_threat_data(TM._actors) + boundaries = encode_element_threat_data(TM._boundaries) + flows = encode_element_threat_data(TM._flows) + data = { "tm": self, - "dataflows": TM._flows, + "dataflows": flows, "threats": threats, "findings": findings, - "elements": TM._elements, - "assets": TM._assets, - "actors": TM._actors, - "boundaries": TM._boundaries, + "elements": elements, + "assets": assets, + "actors": actors, + "boundaries": boundaries, "data": TM._data, } @@ -4257,6 +4348,9 @@

Methods

if not t.apply(e) and t.id not in override_ids: continue + if t.id in TM._threatsExcluded: + continue + finding_count += 1 f = Finding(e, id=str(finding_count), threat=t) logger.debug(f"new finding: {f}") @@ -4670,6 +4764,7 @@

Element

  • enters
  • exits
  • findings
  • +
  • getProperty
  • inBoundary
  • inScope
  • inside
  • @@ -4679,6 +4774,7 @@

    Element

  • name
  • oneOf
  • overrides
  • +
  • props
  • sourceFiles
  • @@ -4807,6 +4903,7 @@

    TM

  • name
  • onDuplicates
  • process
  • +
  • props
  • report
  • reset
  • resolve
  • diff --git a/pytm/pytm.py b/pytm/pytm.py index b18a197..a474440 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -217,6 +217,13 @@ def __set__(self, instance, value): super().__set__(instance, DataSet(value)) +class varDict(var): + def __set__(self, instance, value): + if not isinstance(value, dict): + raise ValueError("expecting a dict, got a {}".format(type(value))) + super().__set__(instance, value) + + class DataSet(set): def __contains__(self, item): if isinstance(item, str): @@ -746,11 +753,13 @@ class TM: doc="""How to handle duplicate Dataflow with same properties, except name and notes""", ) + props = varDict(dict([]), doc="Custom name/value pairs containing data about the model.") assumptions = varStrings( [], required=False, doc="A list of assumptions about the design/model.", ) + custom_properties = dict([]) def __init__(self, name, **kwargs): for key, value in kwargs.items(): @@ -1294,6 +1303,7 @@ class Element: required=False, doc="Location of the source code that describes this element relative to the directory of the model script.", ) + props = varDict(dict([]), doc="Custom name/value pairs containing data about this element.") controls = varControls(None) def __init__(self, name, **kwargs): @@ -1419,6 +1429,22 @@ def inside(self, *boundaries): return True return False + + def getProperty(self, prop): + """getter method to extract data from props dict and avoid KeyError exceptions""" + + if (self.props): + try: + value = self.props[prop] + print("getProperty:value = " + value) + except KeyError: + value = None + else: + value = None + + return value + + def _attr_values(self): klass = self.__class__ result = {} @@ -1868,6 +1894,7 @@ def serialize(obj, nested=False): elif ( not nested and not isinstance(value, str) + and not isinstance(value, dict) and isinstance(value, Iterable) ): value = [v.id if isinstance(v, Finding) else v.name for v in value] diff --git a/pytm/report_util.py b/pytm/report_util.py index 90df7de..c8a17c0 100644 --- a/pytm/report_util.py +++ b/pytm/report_util.py @@ -1,5 +1,23 @@ +class Pair: + key = ""; + value = ""; class ReportUtils: + + @staticmethod + def getPair(obj): + returnValue = [] + if (isinstance(obj, dict)): + for k, v in obj.items(): + p = Pair() + p.key = str(k) + p.value = str(v) + returnValue.append(p) + else: + return "ERROR: getPair method is not valid for " + element.__class__.__name__ + + return returnValue + @staticmethod def getParentName(element): from pytm import Boundary diff --git a/tests/output.json b/tests/output.json index bfd2627..304c3c9 100644 --- a/tests/output.json +++ b/tests/output.json @@ -67,6 +67,7 @@ ], "overrides": [], "port": -1, + "props": {}, "protocol": "", "sourceFiles": [] } @@ -144,6 +145,7 @@ ], "overrides": [], "port": -1, + "props": {}, "protocol": "", "sourceFiles": [], "usesCache": false, @@ -221,6 +223,7 @@ "outputs": [], "overrides": [], "port": -1, + "props": {}, "protocol": "", "sourceFiles": [], "usesEnvironmentVariables": false @@ -297,6 +300,7 @@ ], "overrides": [], "port": -1, + "props": {}, "protocol": "", "sourceFiles": [], "tracksExecutionFlow": false, @@ -376,6 +380,7 @@ ], "overrides": [], "port": -1, + "props": {}, "protocol": "", "sourceFiles": [], "storesLogData": false, @@ -444,6 +449,7 @@ "minTLSVersion": "TLSVersion.NONE", "name": "Internet", "overrides": [], + "props": {}, "sourceFiles": [] }, { @@ -503,9 +509,11 @@ "minTLSVersion": "TLSVersion.NONE", "name": "Server/DB", "overrides": [], + "props": {}, "sourceFiles": [] } ], + "custom_properties": {}, "data": [ { "carriedBy": [ @@ -596,6 +604,7 @@ ], "overrides": [], "port": -1, + "props": {}, "protocol": "", "sourceFiles": [] }, @@ -671,6 +680,7 @@ ], "overrides": [], "port": -1, + "props": {}, "protocol": "", "sourceFiles": [], "usesCache": false, @@ -748,6 +758,7 @@ "outputs": [], "overrides": [], "port": -1, + "props": {}, "protocol": "", "sourceFiles": [], "usesEnvironmentVariables": false @@ -824,6 +835,7 @@ ], "overrides": [], "port": -1, + "props": {}, "protocol": "", "sourceFiles": [], "tracksExecutionFlow": false, @@ -903,6 +915,7 @@ ], "overrides": [], "port": -1, + "props": {}, "protocol": "", "sourceFiles": [], "storesLogData": false, @@ -979,6 +992,7 @@ "note": "bbb", "order": 1, "overrides": [], + "props": {}, "protocol": "", "response": null, "responseTo": null, @@ -1053,6 +1067,7 @@ "note": "ccc", "order": 2, "overrides": [], + "props": {}, "protocol": "", "response": null, "responseTo": null, @@ -1127,6 +1142,7 @@ "note": "", "order": 3, "overrides": [], + "props": {}, "protocol": "", "response": null, "responseTo": null, @@ -1201,6 +1217,7 @@ "note": "", "order": 4, "overrides": [], + "props": {}, "protocol": "", "response": null, "responseTo": null, @@ -1275,6 +1292,7 @@ "note": "", "order": 5, "overrides": [], + "props": {}, "protocol": "", "response": null, "responseTo": null, @@ -1349,6 +1367,7 @@ "note": "", "order": 6, "overrides": [], + "props": {}, "protocol": "", "response": null, "responseTo": null, @@ -1366,6 +1385,7 @@ "mergeResponses": false, "name": "my test tm", "onDuplicates": "Action.NO_ACTION", + "props": {}, "threatsExcluded": [], "threatsFile": "pytm/threatlib/threats.json" } \ No newline at end of file diff --git a/tests/output.md b/tests/output.md index e019932..ac1dfce 100644 --- a/tests/output.md +++ b/tests/output.md @@ -9,7 +9,6 @@ aaa - ## Dataflow Diagram - Level 0 DFD ![](sample.png) @@ -45,3 +44,4 @@ Name|Description|Classification   || + diff --git a/tm.py b/tm.py index 68a4197..ca0b23a 100755 --- a/tm.py +++ b/tm.py @@ -17,6 +17,7 @@ tm.description = "This is a sample threat model of a very simple system - a web-based comment system. The user enters comments and these are added to a database and displayed back to the user. The thought is that it is, though simple, a complete enough example to express meaningful threats." tm.isOrdered = True tm.mergeResponses = True +tm.props = { "repo_url" : "https://github.com/izar/pytm" } tm.assumptions = [ "Here you can document a list of assumptions about the system", ] @@ -39,6 +40,7 @@ web.controls.encodesOutput = True web.controls.authorizesSource = False web.sourceFiles = ["pytm/json.py", "docs/template.md"] +web.props = { "Security Champion" : "John Smith" } db = Datastore("SQL Database") db.OS = "CentOS"