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

@@ -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

@@ -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 @@
enters
exits
findings
+getProperty
inBoundary
inScope
inside
@@ -4679,6 +4774,7 @@
name
oneOf
overrides
+props
sourceFiles
@@ -4807,6 +4903,7 @@
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

@@ -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"