From a53b3fd240bc16884ef0da58b6b38871d571cd10 Mon Sep 17 00:00:00 2001 From: Raphael Ahrens Date: Wed, 13 Mar 2024 17:18:45 +0100 Subject: [PATCH 1/7] Added a first draft for #234 In #234 @colesmj suggested to move the import of pydal into the sqlDumb function. This commit does this and if the import fails raises an UIError with an explanation on how to proceed. The text is just a first draft. To move the import the function get_table was also moved inside the sqlDump function. --- pytm/pytm.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/pytm/pytm.py b/pytm/pytm.py index 9906648..632c144 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 """ @@ -1173,6 +1171,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 +1216,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 +1229,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: From dafc08feeb0239bd45b6abebfa4511fa1d288830 Mon Sep 17 00:00:00 2001 From: Izar Tarandach Date: Thu, 21 Mar 2024 08:04:55 -0400 Subject: [PATCH 2/7] updating versions --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 424f5d76a97bba37ea7c1cbabe0dd60a493f9a85 Mon Sep 17 00:00:00 2001 From: Izar Tarandach Date: Thu, 21 Mar 2024 08:06:38 -0400 Subject: [PATCH 3/7] Update scorecard.yml updating breaking version of OSS scorecard-action --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From f868ffd3f5be3c224e7a16e0b4e6885d00b98456 Mon Sep 17 00:00:00 2001 From: Izar Tarandach Date: Thu, 11 Apr 2024 12:07:42 -0400 Subject: [PATCH 4/7] first draft --- docs/reveal.md | 185 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 docs/reveal.md 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 + +![](sample.png) + +--- + +## 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}}}} +---- + +}} +}} +} + From bd363e9c47ab3325f6f9e685b5caf27fdc2ed2e3 Mon Sep 17 00:00:00 2001 From: Raphael Ahrens Date: Fri, 12 Apr 2024 11:29:42 +0200 Subject: [PATCH 5/7] Added `prerequisites` and `likelihood` to Threat In threats.json the two properties ("prerequisites", "Likelihood Of Attack") are defined, but are not used in the rest of pytm. This commit adds the two properties to the Threat class, so they can be used by other parts of pytm. For me this was relevant, since I started to experiment with a different format for threats mentioned in #237 . And after exporting threat.json to a markdown format and back into threat.json these two fields where missing. --- pytm/pytm.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pytm/pytm.py b/pytm/pytm.py index 632c144..fdd1f31 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -605,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 = () @@ -614,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): @@ -624,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", "") From 129591510c2a3a879324f0f97d5e6dc23e7762ea Mon Sep 17 00:00:00 2001 From: Izar Tarandach Date: Fri, 12 Apr 2024 09:19:10 -0400 Subject: [PATCH 6/7] Update README.md with movie Demo-ing the reveal template. --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 From 9c90a2511cb7d91b78ef681ab4bcceeee99408a4 Mon Sep 17 00:00:00 2001 From: Raphael Ahrens Date: Thu, 18 Apr 2024 06:13:15 +0200 Subject: [PATCH 7/7] Fixed #221 Got an error "AttributeError: 'str' ... When pytm was run with the `--sqldump` flag with the example `tm.py` from the repository the execution failed with ``` AttributeError: 'str' object has no attribute 'name'" ``` This was caused by the `assumptions` attribute https://github.com/izar/pytm/blob/6ca9f75ddaa5bda3503a6b8cbce5e6700e03e644/tm.py#L20-L22 When dumping the model into the database all attributes of the TM class are turned into strings, by first turning the obj into a dictionary, where specific attributes are removed and some are converted, and then each value in the dictionary are turned into strings. This filtering and conversion is done by the `serilaize(obj, nested=False)` function. `sqlDump` transforms the values into strings. The problem in #221 was that when `nested` is false the default behavior of `serialize()` is to assume that any list of values holds objects which have either a `.name` or are an instance of `Finding`. Since `assumptions` is a list of strings this fails. The fix was to add `assumptions` to an already existing check for similar attributes. Also the check was changed from `i == x or ...` to an `in` check. But to be honest this code is very complex and holds many assumptions, which are not true for all classes and is constantly checking the type of the class. Maybe it would be best to write specific serialize functions for some classes, and only have a genral serialize function which takes in an object and a blacklist of attributes. The `to_serializable` singledispatch function already crates special functions for each class for the JSON conversion, maybe this can be extended. --- pytm/pytm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytm/pytm.py b/pytm/pytm.py index fdd1f31..3bd7333 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -1976,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