diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index 53ebb87..8511dd9 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -37,7 +37,7 @@ jobs:
persist-credentials: false
- name: "Run analysis"
- uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # v2.0.6
+ uses: ossf/scorecard-action@v2.3.1
with:
results_file: results.sarif
results_format: sarif
diff --git a/README.md b/README.md
index a810f0c..0b7251a 100644
--- a/README.md
+++ b/README.md
@@ -365,6 +365,16 @@ the `target.input` and `target.output` attributes. For example, to match a threa
servers with incoming traffic, use `any(target.inputs)`. A more advanced example,
matching elements connecting to SQL datastores, would be `any(f.sink.oneOf(Datastore) and f.sink.isSQL for f in target.outputs)`.
+## Making slides!
+
+Once a threat model is done and ready, the dreaded presentation stage comes in - and now pytm can help you there as well, with a template that expresses your threat model in slides, using the power of (RevealMD)[https://github.com/webpro/reveal-md]! Just use the template docs/revealjs.md and you will get some pretty slides, fully configurable, that you can present and share from your browser.
+
+
+
+https://github.com/izar/pytm/assets/368769/30218241-c7cc-4085-91e9-bbec2843f838
+
+
+
## Currently supported threats
```text
diff --git a/docs/reveal.md b/docs/reveal.md
new file mode 100644
index 0000000..4fb4c16
--- /dev/null
+++ b/docs/reveal.md
@@ -0,0 +1,185 @@
+# {tm.name}
+
+---
+
+## System Description
+
+{tm.description}
+
+---
+
+## Dataflow Diagram
+
+
+
+---
+
+## Dataflows
+
+----
+
+{dataflows:repeat:
+
+- **name** : {{item.display_name:call:}}
+- **from** : {{item.source.name}}
+- **to** : {{item.sink.name}}:{{item.dstPort}}
+- **data** : {{item.data}}
+- **protocol** : {{item.protocol}}
+
+----
+}
+
+---
+
+## Data Dictionary
+
+----
+
+{data:repeat:
+
+- **name** : {{item.name}}
+- **description** : {{item.description}}
+- **classification** : {{item.classification.name}}
+- **carried by** : {{item.carriedBy:repeat:{{{{item.name}}}}
}}
+- **processed by** : {{item.processedBy:repeat:{{{{item.name}}}}
}}
+
+----
+}
+
+
+---
+
+## Actors
+
+----
+
+{actors:repeat:
+- **name** : {{item.name}}
+- **description** : {{item.description}}
+- **is Admin** : {{item.isAdmin}}
+- **# of findings** : {{item:call:getFindingCount}}
+
+{{item.findings:not:
+---
+}}
+
+{{item.findings:if:
+----
+**Findings**
+
+----
+
+{{item.findings:repeat:
+ {{{{item.id}}}} -- {{{{item.description}}}}
+
+ - **Targeted Element** : {{{{item.target}}}}
+ - **Severity** : {{{{item.severity}}}}
+ - **References** : {{{{item.references}}}}
+
+----
+
+}}
+}}
+}
+
+## Trust Boundaries
+
+----
+
+{boundaries:repeat:
+- **name** : {{item.name}}
+- **description** : {{item.description}}
+- **in scope** : {{item.inScope}}
+- **immediate parent** : {{item.parents:if:{{item:call:getParentName}}}}{{item.parents:not:N/A, primary boundary}}
+- **all parents** : {{item.parents:call:{{{{item.display_name:call:}}}}, }}
+- **classification** : {{item.maxClassification}}
+- **finding count** : {{item:call:getFindingCount}}
+
+{{item.findings:not:
+---
+}}
+
+{{item.findings:if:
+----
+**Findings**
+
+----
+
+{{item.findings:repeat:
+ {{{{item.id}}}} - {{{{item.description}}}}
+
+ - **Targeted Element** : {{{{item.target}}}}
+ - **Severity** : {{{{item.severity}}}}
+ - **References** : {{{{item.references}}}}
+----
+
+}}
+}}
+}
+
+## Assets
+
+{assets:repeat:
+
+- **name** : {{item.name}}
+- **description** : {{item.description}}
+- **in scope** : {{item.inScope}}
+- **type** : {{item:call:getElementType}}
+- **# of findings** : {{item:call:getFindingCount}}
+
+{{item.findings:not:
+---
+}}
+
+{{item.findings:if:
+----
+**Findings**
+
+----
+
+{{item.findings:repeat:
+ {{{{item.id}}}} - {{{{item.description}}}}
+
+ - **Targeted Element** : {{{{item.target}}}}
+ - **Severity** : {{{{item.severity}}}}
+ - **References** : {{{{item.references}}}}
+----
+
+}}
+}}
+}
+
+## Data Flows
+
+{dataflows:repeat:
+Name|{{item.name}}
+|:----|:----|
+Description|{{item.description}}|
+Sink|{{item.sink}}|
+Source|{{item.source}}|
+Is Response|{{item.isResponse}}|
+In Scope|{{item.inScope}}|
+Finding Count|{{item:call:getFindingCount}}|
+
+{{item.findings:not:
+---
+}}
+
+{{item.findings:if:
+----
+**Findings**
+
+----
+
+{{item.findings:repeat:
+ {{{{item.id}}}} - {{{{item.description}}}}
+
+ - **Targeted Element** : {{{{item.target}}}}
+ - **Severity** : {{{{item.severity}}}}
+ - **References** : {{{{item.references}}}}
+----
+
+}}
+}}
+}
+
diff --git a/pytm/pytm.py b/pytm/pytm.py
index 9906648..3bd7333 100644
--- a/pytm/pytm.py
+++ b/pytm/pytm.py
@@ -21,8 +21,6 @@
from weakref import WeakKeyDictionary
from datetime import datetime
-from pydal import DAL, Field
-
from .template_engine import SuperFormatter
""" Helper functions """
@@ -607,8 +605,10 @@ class Threat:
to a boolean True or False""",
)
details = varString("")
+ likelihood = varString("")
severity = varString("")
mitigations = varString("")
+ prerequisites = varString("")
example = varString("")
references = varString("")
target = ()
@@ -616,6 +616,7 @@ class Threat:
def __init__(self, **kwargs):
self.id = kwargs["SID"]
self.description = kwargs.get("description", "")
+ self.likelihood = kwargs.get("Likelihood Of Attack", "")
self.condition = kwargs.get("condition", "True")
target = kwargs.get("target", "Element")
if not isinstance(target, str) and isinstance(target, Iterable):
@@ -626,6 +627,7 @@ def __init__(self, **kwargs):
self.details = kwargs.get("details", "")
self.severity = kwargs.get("severity", "")
self.mitigations = kwargs.get("mitigations", "")
+ self.prerequisites = kwargs.get("prerequisites", "")
self.example = kwargs.get("example", "")
self.references = kwargs.get("references", "")
@@ -1173,6 +1175,25 @@ def _stale(self, days):
return ""
def sqlDump(self, filename):
+ try:
+ from pydal import DAL, Field
+ except ImportError as e:
+ raise UIError(
+ e, """This feature requires the pyDAL package,
+ Please install the package via pip or your packagemanger of choice.
+ """
+ )
+
+ @lru_cache(maxsize=None)
+ def get_table(db, klass):
+ name = klass.__name__
+ fields = [
+ Field("SID" if i == "id" else i)
+ for i in dir(klass)
+ if not i.startswith("_") and not callable(getattr(klass, i))
+ ]
+ return db.define_table(name, fields)
+
try:
rmtree("./sqldump")
os.mkdir("./sqldump")
@@ -1199,10 +1220,10 @@ def sqlDump(self, filename):
Data,
Finding,
):
- self.get_table(db, klass)
+ get_table(db, klass)
for e in TM._threats + TM._data + TM._elements + self.findings + [self]:
- table = self.get_table(db, e.__class__)
+ table = get_table(db, e.__class__)
row = {}
for k, v in serialize(e).items():
if k == "id":
@@ -1212,15 +1233,6 @@ def sqlDump(self, filename):
db.close()
- @lru_cache(maxsize=None)
- def get_table(self, db, klass):
- name = klass.__name__
- fields = [
- Field("SID" if i == "id" else i)
- for i in dir(klass)
- if not i.startswith("_") and not callable(getattr(klass, i))
- ]
- return db.define_table(name, fields)
class Controls:
@@ -1964,7 +1976,7 @@ def serialize(obj, nested=False):
value = value.name
elif isinstance(obj, Threat) and i == "target":
value = [v.__name__ for v in value]
- elif i == "levels" or i == "sourceFiles":
+ elif i in ("levels", "sourceFiles", "assumptions"):
value = list(value)
elif (
not nested