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 + +![](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}}}} +---- + +}} +}} +} + 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