From b794ddf277b8a1d0525f6c326d5f2c95ab3c9283 Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Wed, 7 Apr 2021 10:12:59 -0400 Subject: [PATCH 01/86] Add sections for Actors and Assets, add finding_id to Findings --- docs/template.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/template.md b/docs/template.md index 55a7a03f..9ff7fae0 100644 --- a/docs/template.md +++ b/docs/template.md @@ -31,14 +31,35 @@ Name|Description|Classification   +## Actors +  + +Name|Description|Classification +|:----:|:--------:|:----:| +{actors:repeat:|{{item.name}}|{{item.description}}|{{item.isAdmin}}| +} + +  + +## Assets +  + +Name|Description|Classification +|:----:|:--------:|:----:| +{assets:repeat:|{{item.name}}|{{item.description}}|{{item.__class__.__name__}}| +} + +  + + ## Potential Threats     -|{findings:repeat: +{findings:repeat:
- {{item.id}} -- {{item.description}} + {{item.id}} -- {{item.threat_id}} -- {{item.description}}
Targeted Element

{{item.target}}

Severity
From a9c060a6f28b26ee3daba9243fb5b6d4d04e92fc Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Wed, 7 Apr 2021 17:57:55 -0400 Subject: [PATCH 02/86] Template Reporting Improvements --- docs/template.md | 106 +++++++++++++++++++++++++++++++--------- pytm/report_util.py | 33 +++++++++++++ pytm/template_engine.py | 29 ++++++++++- tm.py | 10 ++++ 4 files changed, 154 insertions(+), 24 deletions(-) create mode 100644 pytm/report_util.py diff --git a/docs/template.md b/docs/template.md index 9ff7fae0..86e9a128 100644 --- a/docs/template.md +++ b/docs/template.md @@ -21,12 +21,15 @@ Name|From|To |Data|Protocol|Port {dataflows:repeat:|{{item.name}}|{{item.source.name}}|{{item.sink.name}}|{{item.data}}|{{item.protocol}}|{{item.dstPort}}| } +
+
+ ## Data Dictionary   -Name|Description|Classification -|:----:|:--------:|:----:| -{data:repeat:|{{item.name}}|{{item.description}}|{{item.classification.name}}| +Name|Description|Classification|Carried|Processed +|:----:|:--------:|:----:|:----|:----| +{data:repeat:|{{item.name}}|{{item.description}}|{{item.classification.name}}|{{item.carriedBy:repeat:{{{{item.name}}}}
}}|{{item.processedBy:repeat:{{{{item.name}}}}
}}| }   @@ -34,44 +37,103 @@ Name|Description|Classification ## Actors   -Name|Description|Classification +Name|Description|isAdmin |:----:|:--------:|:----:| {actors:repeat:|{{item.name}}|{{item.description}}|{{item.isAdmin}}| } -  +
+
-## Assets -  +## Boundaries + +{boundaries:repeat: +Element|{{item.name}} +|:----|:----| +Description|{{item.description}}| +InScope|{{item.inScope}}| +Parent|{{item:utils:getParentName}}| +Parents|{{item.parents:call:{{{{item.name}}}}, }}| +Classification|{{item.maxClassification}}| + +
+
-Name|Description|Classification -|:----:|:--------:|:----:| -{assets:repeat:|{{item.name}}|{{item.description}}|{{item.__class__.__name__}}| } -  +## Assets +{assets:repeat: -## Potential Threats +
+
-  -  +Element|{{item.name}} +|:----|:----| +Description|{{item.description}}| +InScope|{{item.inScope}}| +Type|{{item.__class__.__name__}}| +Finding Count|{{item:utils:countFindings}}| + +###### Threats -{findings:repeat: +{{item.findings:repeat:
- {{item.id}} -- {{item.threat_id}} -- {{item.description}} + {{{{item.id}}}} -- {{{{item.threat_id}}}} -- {{{{item.description}}}}
Targeted Element
-

{{item.target}}

+

{{{{item.target}}}}

Severity
-

{{item.severity}}

+

{{{{item.severity}}}}

Example Instances
-

{{item.example}}

+

{{{{item.example}}}}

Mitigations
-

{{item.mitigations}}

+

{{{{item.mitigations}}}}

References
-

{{item.references}}

+

{{{{item.references}}}}

     
-}| +}} +} + +
+
+ +## Data Flows + +{dataflows:repeat(e): + +
+
+ +Dataflow|{{item.name}} +|:----|:----| +Description|{{item.description}}| +InScope|{{item.inScope}}| +Finding Count|{{item:utils:countFindings}}| + +###### Threats + +{{item.findings:repeat: +
+ {{{{item.id}}}} -- {{{{item.threat_id}}}} -- {{{{item.description}}}} +
Targeted Element
+

{{{{item.target}}}}

+
Severity
+

{{{{item.severity}}}}

+
Example Instances
+

{{{{item.example}}}}

+
Mitigations
+

{{{{item.mitigations}}}}

+
References
+

{{{{item.references}}}}

+   +   +   +
+}} + +} + +  diff --git a/pytm/report_util.py b/pytm/report_util.py new file mode 100644 index 00000000..8de6f7ed --- /dev/null +++ b/pytm/report_util.py @@ -0,0 +1,33 @@ + +class ReportUtils: + @staticmethod + def getParentName(element): + from pytm import Boundary + if (isinstance(element, Boundary)): + parent = element.inBoundary + if (parent is not None): + return parent.name + else: + return str("") + else: + return "ERROR: getParentName method is not valid for " + element.__class__.__name__ + + + @staticmethod + def printParents(element): + from pytm import Boundary + if (isinstance(element, Boundary)): + parents = map(lambda b: b.name, element.parents()) + return list(parents) + else: + return "ERROR: printParents method is not valid for " + element.__class__.__name__ + + @staticmethod + def countFindings(element): + from pytm import Element + if (isinstance(element, Element)): + return str(len(list(element.findings))) + else: + return "ERROR: countFindings method is not valid for " + element.___class___.___name___ + + \ No newline at end of file diff --git a/pytm/template_engine.py b/pytm/template_engine.py index 916939fe..1f2a7126 100644 --- a/pytm/template_engine.py +++ b/pytm/template_engine.py @@ -12,8 +12,33 @@ def format_field(self, value, spec): if type(value) is dict: value = value.items() return "".join([self.format(template, item=item) for item in value]) - elif spec == "call": - return value() + elif spec.startswith("call"): + result = value() + if type(result) is list: + template = spec.partition(":")[-1] + return "".join([self.format(template, item=item) for item in result]) + + return result + elif spec.startswith("utils"): + + spec_parts = spec.split(":") + + method_name = spec_parts[1] + template = spec_parts[-1] + + module_name = "pytm.report_util" + klass_name = "ReportUtils" + module = __import__(module_name, fromlist=['ReportUtils']) + klass = getattr(module, klass_name) + + method = getattr(klass, method_name) + result = method(value) + + if type(result) is list: + return "".join([self.format(template, item=item) for item in result]) + + return result + elif spec.startswith("if"): return (value and spec.partition(":")[-1]) or "" else: diff --git a/tm.py b/tm.py index 7dc32709..b756d7e4 100755 --- a/tm.py +++ b/tm.py @@ -17,10 +17,20 @@ tm.isOrdered = True tm.mergeResponses = True +all = Boundary("TM Boundary") internet = Boundary("Internet") +internet.inBoundary = all + +company = Boundary("Company") +company.inBoundary = all + server_db = Boundary("Server/DB") server_db.levels = [2] +server_db.inBoundary = company + vpc = Boundary("AWS VPC") +vpc.inBoundary = all + user = Actor("User") user.inBoundary = internet From 8cfa40a420703f5d9abb7a544e853f3b84f41d53 Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Wed, 7 Apr 2021 18:21:28 -0400 Subject: [PATCH 03/86] updated tests --- .gitignore | 1 + tests/output.md | 212 +++++++++++++++++++++++++++++++++++++++-- tests/test_pytmfunc.py | 3 + 3 files changed, 209 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 24bf1143..f67268c8 100644 --- a/.gitignore +++ b/.gitignore @@ -126,6 +126,7 @@ plantuml.jar tm/ /sqldump /tests/output_current.json +/tests/output_current.md /tests/1.txt /tests/0.txt /tests/.config.pytm diff --git a/tests/output.md b/tests/output.md index 23805a06..55297876 100644 --- a/tests/output.md +++ b/tests/output.md @@ -26,19 +26,217 @@ Name|From|To |Data|Protocol|Port |Query for tasks|Task queue worker|SQL Database|[]||-1| +
+
+ ## Data Dictionary   -Name|Description|Classification -|:----:|:--------:|:----:| -|auth cookie||PUBLIC| +Name|Description|Classification|Carried|Processed +|:----:|:--------:|:----:|:----|:----| +|auth cookie||PUBLIC|User enters comments (*)
|User
Web Server
|   -## Potential Threats - -  +## Actors   -|| +Name|Description|isAdmin +|:----:|:--------:|:----:| +|User||False| + + +
+
+ +## Boundaries + + +Element|Internet +|:----|:----| +Description|| +InScope|True| +Parent|| +Parents|| +Classification|Classification.UNKNOWN| + +
+
+ + +Element|Server/DB +|:----|:----| +Description|| +InScope|True| +Parent|| +Parents|| +Classification|Classification.UNKNOWN| + +
+
+ + + +## Assets + + + +
+
+ +Element|Web Server +|:----|:----| +Description|| +InScope|True| +Type|Server| +Finding Count|0| + +###### Threats + + + + +
+
+ +Element|Lambda func +|:----|:----| +Description|| +InScope|True| +Type|Lambda| +Finding Count|0| + +###### Threats + + + + +
+
+ +Element|Task queue worker +|:----|:----| +Description|| +InScope|True| +Type|Process| +Finding Count|0| + +###### Threats + + + + +
+
+ +Element|SQL Database +|:----|:----| +Description|| +InScope|True| +Type|Datastore| +Finding Count|0| + +###### Threats + + + + +
+
+ +## Data Flows + + + +
+
+ +Dataflow|User enters comments (*) +|:----|:----| +Description|| +InScope|True| +Finding Count|0| + +###### Threats + + + + + +
+
+ +Dataflow|Insert query with comments +|:----|:----| +Description|| +InScope|True| +Finding Count|0| + +###### Threats + + + + + +
+
+ +Dataflow|Call func +|:----|:----| +Description|| +InScope|True| +Finding Count|0| + +###### Threats + + + + + +
+
+ +Dataflow|Retrieve comments +|:----|:----| +Description|| +InScope|True| +Finding Count|0| + +###### Threats + + + + + +
+
+ +Dataflow|Show comments (*) +|:----|:----| +Description|| +InScope|True| +Finding Count|0| + +###### Threats + + + + + +
+
+ +Dataflow|Query for tasks +|:----|:----| +Description|| +InScope|True| +Finding Count|0| + +###### Threats + + + + + +  diff --git a/tests/test_pytmfunc.py b/tests/test_pytmfunc.py index d6f7fa50..e43b058e 100644 --- a/tests/test_pytmfunc.py +++ b/tests/test_pytmfunc.py @@ -354,6 +354,9 @@ def test_report(self): self.assertTrue(tm.check()) output = tm.report("docs/template.md") + with open(os.path.join(dir_path, "output_current.md"), "w") as x: + x.write(output) + self.maxDiff = None self.assertEqual(output.strip(), expected.strip()) From ae607f37aab224af5f6d466b2bdf0394484f5dc2 Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Thu, 8 Apr 2021 10:08:54 -0400 Subject: [PATCH 04/86] Changed util method names, added getElementType method, and bug fix on error message --- docs/template.md | 6 +++--- pytm/report_util.py | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/template.md b/docs/template.md index 86e9a128..f2847805 100644 --- a/docs/template.md +++ b/docs/template.md @@ -72,8 +72,8 @@ Element|{{item.name}} |:----|:----| Description|{{item.description}}| InScope|{{item.inScope}}| -Type|{{item.__class__.__name__}}| -Finding Count|{{item:utils:countFindings}}| +Type|{{item:utils:getElementType}}| +Finding Count|{{item:utils:getFindingCount}}| ###### Threats @@ -111,7 +111,7 @@ Dataflow|{{item.name}} |:----|:----| Description|{{item.description}}| InScope|{{item.inScope}}| -Finding Count|{{item:utils:countFindings}}| +Finding Count|{{item:utils:getFindingCount}}| ###### Threats diff --git a/pytm/report_util.py b/pytm/report_util.py index 8de6f7ed..0a3d8040 100644 --- a/pytm/report_util.py +++ b/pytm/report_util.py @@ -14,20 +14,29 @@ def getParentName(element): @staticmethod - def printParents(element): + def getNamesOfParents(element): from pytm import Boundary if (isinstance(element, Boundary)): parents = map(lambda b: b.name, element.parents()) return list(parents) else: - return "ERROR: printParents method is not valid for " + element.__class__.__name__ + return "ERROR: getNamesOfParents method is not valid for " + element.__class__.__name__ @staticmethod - def countFindings(element): + def getFindingCount(element): from pytm import Element if (isinstance(element, Element)): return str(len(list(element.findings))) else: - return "ERROR: countFindings method is not valid for " + element.___class___.___name___ + return "ERROR: getFindingCount method is not valid for " + element.__class__.__name__ + + @staticmethod + def getElementType(element): + from pytm import Element + if (isinstance(element, Element)): + return str(element.__class__.__name__) + else: + return "ERROR: getElementType method is not valid for " + element.__class__.__name__ + \ No newline at end of file From 4b6e2b8b384327c81bcdd46fa5f3225598e5b832 Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Sat, 10 Apr 2021 01:10:26 -0400 Subject: [PATCH 05/86] Code, template cleanup, refactored template engine operations, addressed review comments and deep code suggestion --- docs/Stylesheet.css | 10 ++- docs/template.md | 123 ++++++++++++++++------------ pytm/report_util.py | 7 +- pytm/template_engine.py | 88 +++++++++++++++----- tests/output.md | 174 +++++++++++++--------------------------- 5 files changed, 203 insertions(+), 199 deletions(-) diff --git a/docs/Stylesheet.css b/docs/Stylesheet.css index 1872c7da..425604fe 100644 --- a/docs/Stylesheet.css +++ b/docs/Stylesheet.css @@ -43,8 +43,8 @@ hr { opacity: .5; } table { - margin: .75rem 0; - padding: 0; + margin: .75rem 0 0 1rem; + padding: 0; width: 50%; text-align: left; white-space: nowrap; @@ -76,4 +76,8 @@ table tr td { text-align: left; border: 1px solid #ccc; } -/* @end */ \ No newline at end of file + +details { + margin-left: 2rem +} +/* @end */ diff --git a/docs/template.md b/docs/template.md index f2847805..9054e8c6 100644 --- a/docs/template.md +++ b/docs/template.md @@ -1,12 +1,9 @@ ## System Description -  {tm.description} -  - ## Dataflow Diagram - Level 0 DFD ![](sample.png) @@ -14,68 +11,99 @@   ## Dataflows -  Name|From|To |Data|Protocol|Port |:----:|:----:|:---:|:----:|:--------:|:----:| {dataflows:repeat:|{{item.name}}|{{item.source.name}}|{{item.sink.name}}|{{item.data}}|{{item.protocol}}|{{item.dstPort}}| } -
-
- ## Data Dictionary -  Name|Description|Classification|Carried|Processed |:----:|:--------:|:----:|:----|:----| {data:repeat:|{{item.name}}|{{item.description}}|{{item.classification.name}}|{{item.carriedBy:repeat:{{{{item.name}}}}
}}|{{item.processedBy:repeat:{{{{item.name}}}}
}}| } -  - ## Actors -  -Name|Description|isAdmin -|:----:|:--------:|:----:| -{actors:repeat:|{{item.name}}|{{item.description}}|{{item.isAdmin}}| -} +{actors:repeat: +Name|{{item.name}} +|:----|:----| +Description|{{item.description}}| +Is Admin|{{item.isAdmin}} +Finding Count|{{item:call:getFindingCount}}| + +{{item.findings:if: -
-
+**Threats** + +{{item.findings:repeat: +
+ {{{{item.id}}}} -- {{{{item.threat_id}}}} -- {{{{item.description}}}} +
Targeted Element
+

{{{{item.target}}}}

+
Severity
+

{{{{item.severity}}}}

+
Example Instances
+

{{{{item.example}}}}

+
Mitigations
+

{{{{item.mitigations}}}}

+
References
+

{{{{item.references}}}}

+   +
+}} +}} +} ## Boundaries {boundaries:repeat: -Element|{{item.name}} +Name|{{item.name}} |:----|:----| Description|{{item.description}}| -InScope|{{item.inScope}}| -Parent|{{item:utils:getParentName}}| -Parents|{{item.parents:call:{{{{item.name}}}}, }}| +In Scope|{{item.inScope}}| +Immediate Parent|{{item:call:getParentName}}{{item.parents:not:Primary Boundary}}| +All Parents|{{item.parents:call:{{{{item.name}}}}, }}| Classification|{{item.maxClassification}}| +Finding Count|{{item:call:getFindingCount}}| + +{{item.findings:if: -
-
+**Threats** +{{item.findings:repeat: +
+ {{{{item.id}}}} -- {{{{item.threat_id}}}} -- {{{{item.description}}}} +
Targeted Element
+

{{{{item.target}}}}

+
Severity
+

{{{{item.severity}}}}

+
Example Instances
+

{{{{item.example}}}}

+
Mitigations
+

{{{{item.mitigations}}}}

+
References
+

{{{{item.references}}}}

+   +
+}} +}} } ## Assets {assets:repeat: - -
-
- -Element|{{item.name}} +|Name|{{item.name}}| |:----|:----| Description|{{item.description}}| -InScope|{{item.inScope}}| -Type|{{item:utils:getElementType}}| -Finding Count|{{item:utils:getFindingCount}}| +In Scope|{{item.inScope}}| +Type|{{item:call:getElementType}}| +Finding Count|{{item:call:getFindingCount}}| -###### Threats +{{item.findings:if: + +**Threats** {{item.findings:repeat:
@@ -91,29 +119,26 @@ Finding Count|{{item:utils:getFindingCount}}|
References

{{{{item.references}}}}

  -   -  
}} +}} } -
-
- ## Data Flows -{dataflows:repeat(e): - -
-
- -Dataflow|{{item.name}} +{dataflows:repeat: +Name|{{item.name}} |:----|:----| Description|{{item.description}}| -InScope|{{item.inScope}}| -Finding Count|{{item:utils:getFindingCount}}| +Sink|{{item.sink}}| +Source|{{item.source}}| +|Is Response|{{item.isResponse}} +In Scope|{{item.inScope}}| +Finding Count|{{item:call:getFindingCount}}| + +{{item.findings:if: -###### Threats +**Threats** {{item.findings:repeat:
@@ -128,12 +153,8 @@ Finding Count|{{item:utils:getFindingCount}}|

{{{{item.mitigations}}}}

References

{{{{item.references}}}}

-   -    
}} - +}} } - -  diff --git a/pytm/report_util.py b/pytm/report_util.py index 0a3d8040..90df7de6 100644 --- a/pytm/report_util.py +++ b/pytm/report_util.py @@ -17,8 +17,8 @@ def getParentName(element): def getNamesOfParents(element): from pytm import Boundary if (isinstance(element, Boundary)): - parents = map(lambda b: b.name, element.parents()) - return list(parents) + parents = [p.name for p in element.parents()] + return parents else: return "ERROR: getNamesOfParents method is not valid for " + element.__class__.__name__ @@ -37,6 +37,3 @@ def getElementType(element): return str(element.__class__.__name__) else: return "ERROR: getElementType method is not valid for " + element.__class__.__name__ - - - \ No newline at end of file diff --git a/pytm/template_engine.py b/pytm/template_engine.py index 1f2a7126..6e5d7cef 100644 --- a/pytm/template_engine.py +++ b/pytm/template_engine.py @@ -1,4 +1,6 @@ # shamelessly lifted from https://makina-corpus.com/blog/metier/2016/the-worlds-simplest-python-template-engine +# but modified to include support to call methods which return lists, to call external utility methods, use +# if operator with methods and added a not operator. import string @@ -7,39 +9,83 @@ class SuperFormatter(string.Formatter): """World's simplest Template engine.""" def format_field(self, value, spec): + spec_parts = spec.split(":") if spec.startswith("repeat"): + # Example usage, format, count of spec_parts, exampple format + # object:repeat:template 2 {item.findings:repeat:{{item.id}}, } + template = spec.partition(":")[-1] if type(value) is dict: value = value.items() return "".join([self.format(template, item=item) for item in value]) + elif spec.startswith("call"): - result = value() - if type(result) is list: - template = spec.partition(":")[-1] - return "".join([self.format(template, item=item) for item in result]) + # Example usage, format, count of spec_parts, exampple format + # methood:call 1 {item:call:getParentName} + # methood:call:template 2 {item.parents:call:{{item.name}}, } + # object:call:method_name 2 {item:call:getFindingCount} + # object:call:method_name:template 3 {item:call:getNamesOfParents: + # **{{item}}** + # } + + if (hasattr(value, "__call__")): + + result = value() + if type(result) is list: + template = spec.partition(":")[-1] + return "".join([self.format(template, item=item) for item in result]) + + return result - return result - elif spec.startswith("utils"): + else: - spec_parts = spec.split(":") + method_name = spec_parts[1] + template = spec_parts[-1] - method_name = spec_parts[1] - template = spec_parts[-1] + result = self.call_util_method(method_name, value) - module_name = "pytm.report_util" - klass_name = "ReportUtils" - module = __import__(module_name, fromlist=['ReportUtils']) - klass = getattr(module, klass_name) + if type(result) is list: + return "".join([self.format(template, item=item) for item in result]) - method = getattr(klass, method_name) - result = method(value) + return result - if type(result) is list: - return "".join([self.format(template, item=item) for item in result]) - - return result + return "ERROR using call operator" + + elif (spec.startswith("if") or spec.startswith("not")): + # Example usage, format, count of spec_parts, exampple format + # object.boolean:if:template 2 {item.isResponse:if:True} + # method:if:method_name:template 3 {item.parents:if:Has a parent} + # object:if:method_name:template 3 {item:if:getNamesOfParents: + # **{{item}}** + # } + # object.boolean:not:template 2 {item.isResponse:not:False} + # method:not:method_name:template 3 {item.parents:not:Does not have a parents} + # object:not:method_name:template 3 {item:not:getNamesOfParents: + # **{{item}}** + # } + + if (hasattr(value, "__call__")): + result = value() + elif(len(spec_parts) == 3): + method_name = spec_parts[1] + result = self.call_util_method(method_name, value) + else: + result = value + + if (spec.startswith("if")): + return (result and spec_parts[-1]) or "" + else: + return (not result and spec_parts[-1]) or "" - elif spec.startswith("if"): - return (value and spec.partition(":")[-1]) or "" else: return super(SuperFormatter, self).format_field(value, spec) + + def call_util_method(self, method_name, object): + module_name = "pytm.report_util" + klass_name = "ReportUtils" + module = __import__(module_name, fromlist=['ReportUtils']) + klass = getattr(module, klass_name) + method = getattr(klass, method_name) + + result = method(object) + return result diff --git a/tests/output.md b/tests/output.md index 55297876..4bf8c319 100644 --- a/tests/output.md +++ b/tests/output.md @@ -1,12 +1,9 @@ ## System Description -  aaa -  - ## Dataflow Diagram - Level 0 DFD ![](sample.png) @@ -14,7 +11,6 @@ aaa   ## Dataflows -  Name|From|To |Data|Protocol|Port |:----:|:----:|:---:|:----:|:--------:|:----:| @@ -26,217 +22,157 @@ Name|From|To |Data|Protocol|Port |Query for tasks|Task queue worker|SQL Database|[]||-1| -
-
- ## Data Dictionary -  Name|Description|Classification|Carried|Processed |:----:|:--------:|:----:|:----|:----| |auth cookie||PUBLIC|User enters comments (*)
|User
Web Server
| -  - ## Actors -  -Name|Description|isAdmin -|:----:|:--------:|:----:| -|User||False| + +Name|User +|:----|:----| +Description|| +Is Admin|False +Finding Count|0| + -
-
## Boundaries -Element|Internet +Name|Internet |:----|:----| Description|| -InScope|True| -Parent|| -Parents|| +In Scope|True| +Immediate Parent|Primary Boundary| +All Parents|| Classification|Classification.UNKNOWN| +Finding Count|0| -
-
-Element|Server/DB +Name|Server/DB |:----|:----| Description|| -InScope|True| -Parent|| -Parents|| +In Scope|True| +Immediate Parent|Primary Boundary| +All Parents|| Classification|Classification.UNKNOWN| +Finding Count|0| -
-
## Assets - -
-
- -Element|Web Server +|Name|Web Server| |:----|:----| Description|| -InScope|True| +In Scope|True| Type|Server| Finding Count|0| -###### Threats - - - -
-
-Element|Lambda func +|Name|Lambda func| |:----|:----| Description|| -InScope|True| +In Scope|True| Type|Lambda| Finding Count|0| -###### Threats - - -
-
- -Element|Task queue worker +|Name|Task queue worker| |:----|:----| Description|| -InScope|True| +In Scope|True| Type|Process| Finding Count|0| -###### Threats - - -
-
- -Element|SQL Database +|Name|SQL Database| |:----|:----| Description|| -InScope|True| +In Scope|True| Type|Datastore| Finding Count|0| -###### Threats - -
-
- ## Data Flows - -
-
- -Dataflow|User enters comments (*) +Name|User enters comments (*) |:----|:----| Description|| -InScope|True| +Sink|Server(Web Server)| +Source|Actor(User)| +|Is Response|False +In Scope|True| Finding Count|0| -###### Threats - - - - -
-
-Dataflow|Insert query with comments +Name|Insert query with comments |:----|:----| Description|| -InScope|True| +Sink|Datastore(SQL Database)| +Source|Server(Web Server)| +|Is Response|False +In Scope|True| Finding Count|0| -###### Threats - - - -
-
- -Dataflow|Call func +Name|Call func |:----|:----| Description|| -InScope|True| +Sink|Lambda(Lambda func)| +Source|Server(Web Server)| +|Is Response|False +In Scope|True| Finding Count|0| -###### Threats - - - -
-
- -Dataflow|Retrieve comments +Name|Retrieve comments |:----|:----| Description|| -InScope|True| +Sink|Server(Web Server)| +Source|Datastore(SQL Database)| +|Is Response|False +In Scope|True| Finding Count|0| -###### Threats - - - -
-
- -Dataflow|Show comments (*) +Name|Show comments (*) |:----|:----| Description|| -InScope|True| +Sink|Actor(User)| +Source|Server(Web Server)| +|Is Response|False +In Scope|True| Finding Count|0| -###### Threats - - - -
-
- -Dataflow|Query for tasks +Name|Query for tasks |:----|:----| Description|| -InScope|True| +Sink|Datastore(SQL Database)| +Source|Process(Task queue worker)| +|Is Response|False +In Scope|True| Finding Count|0| -###### Threats - - - -  From 26f8bd5c9b0eb69de974cc69dfb9e71a7b3acb86 Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Thu, 15 Apr 2021 03:51:10 -0400 Subject: [PATCH 06/86] Bugfix+review comments. Created 2 templates, updated tests, README, sample.png. Reverted changes to tm.py. --- README.md | 6 +- docs/{template.md => advanced_template.md} | 3 +- docs/basic_template.md | 56 ++++++++ pytm/template_engine.py | 75 +++++----- sample.png | Bin 24347 -> 60312 bytes tests/output.md | 160 ++------------------- tests/test_pytmfunc.py | 2 +- tm.py | 9 +- 8 files changed, 109 insertions(+), 202 deletions(-) rename docs/{template.md => advanced_template.md} (96%) create mode 100644 docs/basic_template.md diff --git a/README.md b/README.md index e4a0efc5..487ef6ec 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ The `tm.py` is an example model. You can run it to generate the report and diagr ``` mkdir -p tm -./tm.py --report docs/template.md | pandoc -f markdown -t html > tm/report.html +./tm.py --report docs/basic_template.md | pandoc -f markdown -t html > tm/report.html ./tm.py --dfd | dot -Tpng -o tm/dfd.png ./tm.py --seq | java -Djava.awt.headless=true -jar $PLANTUML_PATH -tpng -pipe > tm/seq.png ``` @@ -64,7 +64,7 @@ optional arguments: -h, --help show this help message and exit --debug print debug messages --dfd output DFD (default) - --report REPORT output report using the named template file (sample template file is under docs/template.md) + --report REPORT output report using the named template file (sample template file is under docs/basic_template.md) --exclude EXCLUDE specify threat IDs to be ignored --seq output sequential diagram --list list all available threats @@ -196,7 +196,7 @@ The diagrams and findings can be included in the template to create a final repo ```bash -tm.py --report docs/template.md | pandoc -f markdown -t html > report.html +tm.py --report docs/basic_template.md | pandoc -f markdown -t html > report.html ``` The templating format used in the report template is very simple: diff --git a/docs/template.md b/docs/advanced_template.md similarity index 96% rename from docs/template.md rename to docs/advanced_template.md index 9054e8c6..9d1297df 100644 --- a/docs/template.md +++ b/docs/advanced_template.md @@ -63,8 +63,7 @@ Name|{{item.name}} |:----|:----| Description|{{item.description}}| In Scope|{{item.inScope}}| -Immediate Parent|{{item:call:getParentName}}{{item.parents:not:Primary Boundary}}| -All Parents|{{item.parents:call:{{{{item.name}}}}, }}| +Parent|{{item:call:getParentName}}{{item.parents:not:N/A, primary boundary}}| Classification|{{item.maxClassification}}| Finding Count|{{item:call:getFindingCount}}| diff --git a/docs/basic_template.md b/docs/basic_template.md new file mode 100644 index 00000000..b89635d8 --- /dev/null +++ b/docs/basic_template.md @@ -0,0 +1,56 @@ + + +## System Description +  + +{tm.description} + +  + +## Dataflow Diagram - Level 0 DFD + +![](sample.png) + +  + +## Dataflows +  + +Name|From|To |Data|Protocol|Port +|:----:|:----:|:---:|:----:|:--------:|:----:| +{dataflows:repeat:|{{item.name}}|{{item.source.name}}|{{item.sink.name}}|{{item.data}}|{{item.protocol}}|{{item.dstPort}}| +} + +## Data Dictionary +  + +Name|Description|Classification +|:----:|:--------:|:----:| +{data:repeat:|{{item.name}}|{{item.description}}|{{item.classification.name}}| +} + +  + +## Potential Threats + +  +  + +|{findings:repeat: +
+ {{item.threat_id}} -- {{item.description}} +
Targeted Element
+

{{item.target}}

+
Severity
+

{{item.severity}}

+
Example Instances
+

{{item.example}}

+
Mitigations
+

{{item.mitigations}}

+
References
+

{{item.references}}

+   +   +   +
+}| diff --git a/pytm/template_engine.py b/pytm/template_engine.py index 6e5d7cef..aea40cdb 100644 --- a/pytm/template_engine.py +++ b/pytm/template_engine.py @@ -9,6 +9,7 @@ class SuperFormatter(string.Formatter): """World's simplest Template engine.""" def format_field(self, value, spec): + spec_parts = spec.split(":") if spec.startswith("repeat"): # Example usage, format, count of spec_parts, exampple format @@ -19,63 +20,55 @@ def format_field(self, value, spec): value = value.items() return "".join([self.format(template, item=item) for item in value]) - elif spec.startswith("call"): - # Example usage, format, count of spec_parts, exampple format - # methood:call 1 {item:call:getParentName} - # methood:call:template 2 {item.parents:call:{{item.name}}, } - # object:call:method_name 2 {item:call:getFindingCount} - # object:call:method_name:template 3 {item:call:getNamesOfParents: - # **{{item}}** - # } - - if (hasattr(value, "__call__")): + elif spec.startswith("call:") and hasattr(value, "__call__"): + # Example usage, format, exampple format + # methood:call {item:call:getParentName} + # methood:call:template {item.parents:call:{{item.name}}, } + result = value() - result = value() - if type(result) is list: - template = spec.partition(":")[-1] - return "".join([self.format(template, item=item) for item in result]) + if type(result) is list: + template = spec.partition(":")[-1] + return "".join([self.format(template, item=item) for item in result]) - return result + return result - else: - - method_name = spec_parts[1] - template = spec_parts[-1] + elif spec.startswith("call:"): + # Example usage, format, exampple format + # object:call:method_name {item:call:getFindingCount} + # object:call:method_name:template {item:call:getNamesOfParents: + # **{{item}}** + # } - result = self.call_util_method(method_name, value) + method_name = spec_parts[1] + template = spec.partition(":")[-1] - if type(result) is list: - return "".join([self.format(template, item=item) for item in result]) + result = self.call_util_method(method_name, value) - return result + if type(result) is list: + return "".join([self.format(template, item=item) for item in result]) - return "ERROR using call operator" + return result elif (spec.startswith("if") or spec.startswith("not")): - # Example usage, format, count of spec_parts, exampple format - # object.boolean:if:template 2 {item.isResponse:if:True} - # method:if:method_name:template 3 {item.parents:if:Has a parent} - # object:if:method_name:template 3 {item:if:getNamesOfParents: - # **{{item}}** - # } - # object.boolean:not:template 2 {item.isResponse:not:False} - # method:not:method_name:template 3 {item.parents:not:Does not have a parents} - # object:not:method_name:template 3 {item:not:getNamesOfParents: - # **{{item}}** - # } - + # Example usage, format, exampple format + # object.bool:if:template {item.inScope:if:

Is in scope.

} + # object:if:template {item.findings:if:

Has Findings

} + # object.method:if:template {item.parents:if:

Has Parents

} + # + # object.bool:not:template {item.inScope:not:

Is not in scope.

} + # object:not:template {item.findings:not:

Has No Findings

} + # object.method:not:template {item.parents:not:

Has No Parents

} + + template = spec.partition(":")[-1] if (hasattr(value, "__call__")): result = value() - elif(len(spec_parts) == 3): - method_name = spec_parts[1] - result = self.call_util_method(method_name, value) else: result = value if (spec.startswith("if")): - return (result and spec_parts[-1]) or "" + return (result and template or "") else: - return (not result and spec_parts[-1]) or "" + return (not result and template or "") else: return super(SuperFormatter, self).format_field(value, spec) diff --git a/sample.png b/sample.png index de5fe502b23349ea1ca25b0543fcd92ea411fc5e..dc7d815f824b1c4d3312547179b791e8cd4bf38a 100644 GIT binary patch literal 60312 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 diff --git a/tests/output.md b/tests/output.md index 4bf8c319..23805a06 100644 --- a/tests/output.md +++ b/tests/output.md @@ -1,9 +1,12 @@ ## System Description +  aaa +  + ## Dataflow Diagram - Level 0 DFD ![](sample.png) @@ -11,6 +14,7 @@ aaa   ## Dataflows +  Name|From|To |Data|Protocol|Port |:----:|:----:|:---:|:----:|:--------:|:----:| @@ -23,156 +27,18 @@ Name|From|To |Data|Protocol|Port ## Data Dictionary +  -Name|Description|Classification|Carried|Processed -|:----:|:--------:|:----:|:----|:----| -|auth cookie||PUBLIC|User enters comments (*)
|User
Web Server
| - - -## Actors - - -Name|User -|:----|:----| -Description|| -Is Admin|False -Finding Count|0| - - - - -## Boundaries - - -Name|Internet -|:----|:----| -Description|| -In Scope|True| -Immediate Parent|Primary Boundary| -All Parents|| -Classification|Classification.UNKNOWN| -Finding Count|0| - - - -Name|Server/DB -|:----|:----| -Description|| -In Scope|True| -Immediate Parent|Primary Boundary| -All Parents|| -Classification|Classification.UNKNOWN| -Finding Count|0| - - - - -## Assets - - -|Name|Web Server| -|:----|:----| -Description|| -In Scope|True| -Type|Server| -Finding Count|0| - - - -|Name|Lambda func| -|:----|:----| -Description|| -In Scope|True| -Type|Lambda| -Finding Count|0| - - - -|Name|Task queue worker| -|:----|:----| -Description|| -In Scope|True| -Type|Process| -Finding Count|0| - - - -|Name|SQL Database| -|:----|:----| -Description|| -In Scope|True| -Type|Datastore| -Finding Count|0| - - - - -## Data Flows - - -Name|User enters comments (*) -|:----|:----| -Description|| -Sink|Server(Web Server)| -Source|Actor(User)| -|Is Response|False -In Scope|True| -Finding Count|0| - - - -Name|Insert query with comments -|:----|:----| -Description|| -Sink|Datastore(SQL Database)| -Source|Server(Web Server)| -|Is Response|False -In Scope|True| -Finding Count|0| - - - -Name|Call func -|:----|:----| -Description|| -Sink|Lambda(Lambda func)| -Source|Server(Web Server)| -|Is Response|False -In Scope|True| -Finding Count|0| - - - -Name|Retrieve comments -|:----|:----| -Description|| -Sink|Server(Web Server)| -Source|Datastore(SQL Database)| -|Is Response|False -In Scope|True| -Finding Count|0| - - - -Name|Show comments (*) -|:----|:----| -Description|| -Sink|Actor(User)| -Source|Server(Web Server)| -|Is Response|False -In Scope|True| -Finding Count|0| - +Name|Description|Classification +|:----:|:--------:|:----:| +|auth cookie||PUBLIC| -Name|Query for tasks -|:----|:----| -Description|| -Sink|Datastore(SQL Database)| -Source|Process(Task queue worker)| -|Is Response|False -In Scope|True| -Finding Count|0| +  +## Potential Threats +  +  +|| diff --git a/tests/test_pytmfunc.py b/tests/test_pytmfunc.py index e43b058e..7cfc89e8 100644 --- a/tests/test_pytmfunc.py +++ b/tests/test_pytmfunc.py @@ -352,7 +352,7 @@ def test_report(self): Dataflow(worker, db, "Query for tasks") self.assertTrue(tm.check()) - output = tm.report("docs/template.md") + output = tm.report("docs/basic_template.md") with open(os.path.join(dir_path, "output_current.md"), "w") as x: x.write(output) diff --git a/tm.py b/tm.py index b756d7e4..1734ef14 100755 --- a/tm.py +++ b/tm.py @@ -17,20 +17,12 @@ tm.isOrdered = True tm.mergeResponses = True -all = Boundary("TM Boundary") internet = Boundary("Internet") -internet.inBoundary = all - -company = Boundary("Company") -company.inBoundary = all server_db = Boundary("Server/DB") server_db.levels = [2] -server_db.inBoundary = company vpc = Boundary("AWS VPC") -vpc.inBoundary = all - user = Actor("User") user.inBoundary = internet @@ -42,6 +34,7 @@ web.sanitizesInput = False web.encodesOutput = True web.authorizesSource = False +web.inBoundary = server_db db = Datastore("SQL Database") db.OS = "CentOS" From c2088e339ac2abd537623406187f442ff806dab6 Mon Sep 17 00:00:00 2001 From: Jan Was Date: Fri, 30 Apr 2021 19:32:28 +0000 Subject: [PATCH 07/86] Update docs --- docs/pytm/index.html | 282 +++++++++++++++++++++++++++++++++++-------- docs/threats.md | 12 +- 2 files changed, 235 insertions(+), 59 deletions(-) diff --git a/docs/pytm/index.html b/docs/pytm/index.html index bde34e65..f54328a8 100644 --- a/docs/pytm/index.html +++ b/docs/pytm/index.html @@ -191,7 +191,7 @@

Class variables

port = varInt(-1, doc="Default TCP port for outgoing data flows") protocol = varString("", doc="Default network protocol for outgoing data flows") - data = varData([], doc="Default type of data in outgoing data flows") + data = varData([], doc="pytm.Data object(s) in outgoing data flows") inputs = varElements([], doc="incoming Dataflows") outputs = varElements([], doc="outgoing Dataflows") authenticatesDestination = varBool( @@ -209,7 +209,8 @@

Class variables

providesIntegrity = False def __init__(self, name, **kwargs): - super().__init__(name, **kwargs) + super().__init__(name, **kwargs) + TM._actors.append(self)

Ancestors

    @@ -260,7 +261,7 @@

    Instance variables

    var data
    -

    Default type of data in outgoing data flows

    +

    pytm.Data object(s) in outgoing data flows

    Expand source code @@ -512,8 +513,9 @@

    Class variables

    name = varString("", required=True) description = varString("") + format = varString("") classification = varClassification( - Classification.PUBLIC, + Classification.UNKNOWN, required=True, doc="Level of classification for this piece of data", ) @@ -564,7 +566,13 @@

    Class variables

    ) def __str__(self): - return "{0}({1})".format(type(self).__name__, self.name) + return "{0}({1})".format(type(self).__name__, self.name) + + def _safeset(self, attr, value): + try: + setattr(self, attr, value) + except ValueError: + pass

    Instance variables

    @@ -641,6 +649,22 @@

    Instance variables

    return self.data.get(instance, self.default)
    +
    var format
    +
    +
    +
    + +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 isCredentials

    Does the data contain authentication information, @@ -787,7 +811,7 @@

    Instance variables

    doc="TLS version used.", ) protocol = varString("", doc="Protocol used in this data flow") - data = varData([], doc="Default type of data in incoming data flows") + data = varData([], doc="pytm.Data object(s) in incoming data flows") authenticatesDestination = varBool( False, doc="""Verifies the identity of the destination, @@ -823,11 +847,7 @@

    Instance variables

    color = {color}; fontcolor = {color}; dir = {direction}; - label = < - <table border="0" cellborder="0" cellpadding="2"> - <tr><td><font color="{color}"><b>{label}</b></font></td></tr> - </table> - >; + label = "{label}"; ] """ @@ -846,7 +866,7 @@

    Instance variables

    label = self._label() if mergeResponses and self.response is not None: direction = "both" - label += "<br/>" + self.response._label() + label += "\n" + self.response._label() return self._dfd_template().format( source=self.source._uniq_name(), @@ -938,7 +958,7 @@

    Instance variables

    var data
    -

    Default type of data in incoming data flows

    +

    pytm.Data object(s) in incoming data flows

    Expand source code @@ -1293,18 +1313,33 @@

    Methods

    def _dfd_template(self): return """{uniq_name} [ shape = {shape}; + fixedsize = shape; + image = "{image}"; + imagescale = true; color = {color}; fontcolor = {color}; - label = < - <table sides="TB" cellborder="0" cellpadding="2"> - <tr><td><b>{label}</b></td></tr> - </table> - >; + xlabel = "{label}"; + label = ""; ] """ def _shape(self): - return "none" + return "none" + + def dfd(self, **kwargs): + self._is_drawn = True + + levels = kwargs.get("levels", None) + if levels and not levels & self.levels: + return "" + + return self._dfd_template().format( + uniq_name=self._uniq_name(), + label=self._label(), + color=self._color(), + shape=self._shape(), + image=os.path.join(os.path.dirname(__file__), "images", "datastore.png"), + )

    Ancestors

      @@ -1586,8 +1621,7 @@

      Instance variables

      minTLSVersion = varTLSVersion( TLSVersion.NONE, required=False, - doc="""Minimum required TLS version required. -Note that currently only TLS 1.2 and 1.3 are considered secure.""", + doc="""Minimum TLS version required.""", ) findings = varFindings([], doc="Threats that apply to this element") overrides = varFindings( @@ -1596,6 +1630,11 @@

      Instance variables

      a custom response, CVSS score or override other attributes.""", ) levels = varInts({0}, doc="List of levels (0, 1, 2, ...) to be drawn in the model.") + sourceFiles = varStrings( + [], + required=False, + doc="Location of the source code that describes this element relative to the directory of the model script.", + ) def __init__(self, name, **kwargs): for key, value in kwargs.items(): @@ -1627,11 +1666,8 @@

      Instance variables

      shape = {shape}; color = {color}; fontcolor = {color}; - label = < - <table border="0" cellborder="0" cellpadding="2"> - <tr><td><b>{label}</b></td></tr> - </table> - >; + label = "{label}"; + margin = 0.02; ] """ @@ -1659,7 +1695,7 @@

      Instance variables

      return self.name def _label(self): - return "<br/>".join(wrap(self.display_name(), 18)) + return "\\n".join(wrap(self.display_name(), 18)) def _shape(self): return "square" @@ -1846,8 +1882,7 @@

      Instance variables

    var minTLSVersion
    -

    Minimum required TLS version required. -Note that currently only TLS 1.2 and 1.3 are considered secure.

    +

    Minimum TLS version required.

    Expand source code @@ -1894,6 +1929,22 @@

    Instance variables

    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.

    +
    + +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)
    +
    +

    Methods

    @@ -2090,8 +2141,10 @@

    Instance variables

    severity = varString("", required=True, doc="Threat severity") mitigations = varString("", required=True, doc="Threat mitigations") example = varString("", required=True, doc="Threat example") - id = varString("", required=True, doc="Threat ID") + id = varInt("", required=True, doc="Finding ID") + threat_id = varString("", required=True, doc="Threat ID") references = varString("", required=True, doc="Threat references") + condition = varString("", required=True, doc="Threat condition") response = varString( "", required=False, @@ -2123,18 +2176,19 @@

    Instance variables

    "severity", "mitigations", "example", - "id", "references", + "condition", ] threat = kwargs.pop("threat", None) if threat: + kwargs["threat_id"] = getattr(threat, "id") for a in attrs: # copy threat attrs into kwargs to allow to override them in next step kwargs[a] = getattr(threat, a) - threat_id = kwargs.get("id", None) + threat_id = kwargs.get("threat_id", None) for f in element.overrides: - if f.id != threat_id: + if f.threat_id != threat_id: continue for i in dir(f.__class__): attr = getattr(f.__class__, i) @@ -2152,6 +2206,12 @@

    Instance variables

    for k, v in kwargs.items(): setattr(self, k, v) + def _safeset(self, attr, value): + try: + setattr(self, attr, value) + except ValueError: + pass + def __repr__(self): return "<{0}.{1}({2}) at {3}>".format( self.__module__, type(self).__name__, self.id, hex(id(self)) @@ -2162,6 +2222,22 @@

    Instance variables

    Instance variables

    +
    var condition
    +
    +

    Threat condition

    +
    + +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 cvss

    The CVSS score and/or vector

    @@ -2244,7 +2320,7 @@

    Instance variables

    var id
    -

    Threat ID

    +

    Finding ID

    Expand source code @@ -2343,6 +2419,22 @@

    Instance variables

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

    Threat ID

    +
    + +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)
    +
    +
    @@ -2368,9 +2460,7 @@

    Instance variables

    def _dfd_template(self): return """{uniq_name} [ shape = {shape}; - fixedsize = shape; - image = "{image}"; - imagescale = true; + color = {color}; fontcolor = {color}; label = < @@ -2393,11 +2483,10 @@

    Instance variables

    label=self._label(), color=self._color(), shape=self._shape(), - image=os.path.join(os.path.dirname(__file__), "images", "lambda.png"), ) def _shape(self): - return "none"
    + return "rectangle; style=rounded"

    Ancestors

      @@ -3435,6 +3524,8 @@

      Class variables

      _flows = [] _elements = [] + _actors = [] + _assets = [] _threats = [] _boundaries = [] _data = [] @@ -3471,6 +3562,8 @@

      Class variables

      def reset(cls): cls._flows = [] cls._elements = [] + cls._actors = [] + cls._assets = [] cls._threats = [] cls._boundaries = [] cls._data = [] @@ -3487,24 +3580,29 @@

      Class variables

      TM._threats.append(Threat(**i)) def resolve(self): + finding_count = 0 findings = [] elements = defaultdict(list) for e in TM._elements: if not e.inScope: continue - override_ids = set(f.id for f in e.overrides) + override_ids = set(f.threat_id for f in e.overrides) # if element is a dataflow filter out overrides from source and sink # because they will be always applied there anyway try: - override_ids -= set(f.id for f in e.source.overrides + e.sink.overrides) + override_ids -= set( + f.threat_id for f in e.source.overrides + e.sink.overrides + ) except AttributeError: pass for t in TM._threats: if not t.apply(e) and t.id not in override_ids: continue - f = Finding(e, threat=t) + + finding_count += 1 + f = Finding(e, id=finding_count, threat=t) findings.append(f) elements[e].append(f) self.findings = findings @@ -3525,7 +3623,7 @@

      Class variables

      _apply_defaults(TM._flows, TM._data) for e in TM._elements: - top = Counter(f.id for f in e.overrides).most_common(1) + top = Counter(f.threat_id for f in e.overrides).most_common(1) if not top: continue threat_id, count = top[0] @@ -3686,15 +3784,21 @@

      Class variables

      with open(template_path) as file: template = file.read() + threats = encode_threat_data(TM._threats) + findings = encode_threat_data(self.findings) + data = { "tm": self, "dataflows": TM._flows, - "threats": TM._threats, - "findings": self.findings, + "threats": threats, + "findings": findings, "elements": TM._elements, + "assets": TM._assets, + "actors": TM._actors, "boundaries": TM._boundaries, "data": TM._data, } + return self._sf.format(template, **data) def process(self): @@ -3715,6 +3819,7 @@

      Class variables

      result.report is not None or result.json is not None or result.sqldump is not None + or result.stale_days is not None ): self.resolve() @@ -3737,6 +3842,50 @@

      Class variables

      if result.list is True: [print("{} - {}".format(t.id, t.description)) for t in TM._threats] + if result.stale_days is not None: + print(self._stale(result.stale_days)) + + def _stale(self, days): + try: + base_path = os.path.dirname(sys.argv[0]) + tm_mtime = datetime.fromtimestamp( + os.stat(base_path + f"/{sys.argv[0]}").st_mtime + ) + except os.error as err: + sys.stderr.write(f"{sys.argv[0]} - {err}\n") + sys.stderr.flush() + return "[ERROR]" + + print(f"Checking for code {days} days older than this model.") + + for e in TM._elements: + + for src in e.sourceFiles: + try: + src_mtime = datetime.fromtimestamp( + os.stat(base_path + f"/{src}").st_mtime + ) + except os.error as err: + sys.stderr.write(f"{sys.argv[0]} - {err}\n") + sys.stderr.flush() + continue + + age = (src_mtime - tm_mtime).days + + # source code is older than model by more than the speficied delta + if (age) >= days: + print(f"This model is {age} days older than {base_path}/{src}.") + elif age <= -days: + print( + f"Model script {sys.argv[0]}" + + " is only " + + str(-1 * age) + + " days newer than source code file " + + f"{base_path}/{src}" + ) + + return "" + def sqlDump(self, filename): try: rmtree("./sqldump") @@ -3772,7 +3921,7 @@

      Class variables

      for k, v in serialize(e).items(): if k == "id": k = "SID" - row[k] = ", ".join(v) if isinstance(v, list) else v + row[k] = ", ".join(str(i) for i in v) if isinstance(v, list) else v db[table].bulk_insert([row]) db.close() @@ -3802,6 +3951,8 @@

      Static methods

      def reset(cls): cls._flows = [] cls._elements = [] + cls._actors = [] + cls._assets = [] cls._threats = [] cls._boundaries = [] cls._data = [] @@ -3989,6 +4140,7 @@

      Methods

      result.report is not None or result.json is not None or result.sqldump is not None + or result.stale_days is not None ): self.resolve() @@ -4009,7 +4161,10 @@

      Methods

      _describe_classes(result.describe.split()) if result.list is True: - [print("{} - {}".format(t.id, t.description)) for t in TM._threats] + [print("{} - {}".format(t.id, t.description)) for t in TM._threats] + + if result.stale_days is not None: + print(self._stale(result.stale_days))
      @@ -4025,15 +4180,21 @@

      Methods

      with open(template_path) as file: template = file.read() + threats = encode_threat_data(TM._threats) + findings = encode_threat_data(self.findings) + data = { "tm": self, "dataflows": TM._flows, - "threats": TM._threats, - "findings": self.findings, + "threats": threats, + "findings": findings, "elements": TM._elements, + "assets": TM._assets, + "actors": TM._actors, "boundaries": TM._boundaries, "data": TM._data, } + return self._sf.format(template, **data)
      @@ -4047,24 +4208,29 @@

      Methods

      Expand source code
      def resolve(self):
      +    finding_count = 0
           findings = []
           elements = defaultdict(list)
           for e in TM._elements:
               if not e.inScope:
                   continue
       
      -        override_ids = set(f.id for f in e.overrides)
      +        override_ids = set(f.threat_id for f in e.overrides)
               # if element is a dataflow filter out overrides from source and sink
               # because they will be always applied there anyway
               try:
      -            override_ids -= set(f.id for f in e.source.overrides + e.sink.overrides)
      +            override_ids -= set(
      +                f.threat_id for f in e.source.overrides + e.sink.overrides
      +            )
               except AttributeError:
                   pass
       
               for t in TM._threats:
                   if not t.apply(e) and t.id not in override_ids:
                       continue
      -            f = Finding(e, threat=t)
      +
      +            finding_count += 1
      +            f = Finding(e, id=finding_count, threat=t)
                   findings.append(f)
                   elements[e].append(f)
           self.findings = findings
      @@ -4116,7 +4282,7 @@ 

      Methods

      for k, v in serialize(e).items(): if k == "id": k = "SID" - row[k] = ", ".join(v) if isinstance(v, list) else v + row[k] = ", ".join(str(i) for i in v) if isinstance(v, list) else v db[table].bulk_insert([row]) db.close()
      @@ -4167,6 +4333,12 @@

      Methods

      self.example = kwargs.get("example", "") self.references = kwargs.get("references", "") + def _safeset(self, attr, value): + try: + setattr(self, attr, value) + except ValueError: + pass + def __repr__(self): return "<{0}.{1}({2}) at {3}>".format( self.__module__, type(self).__name__, self.id, hex(id(self)) @@ -4401,6 +4573,7 @@

      Data

    • classification
    • credentialsLife
    • description
    • +
    • format
    • isCredentials
    • isDestEncryptedAtRest
    • isPII
    • @@ -4477,6 +4650,7 @@

      Element

    • name
    • oneOf
    • overrides
    • +
    • sourceFiles
  • @@ -4488,6 +4662,7 @@

    ExternalEnt
  • Finding

  • diff --git a/docs/threats.md b/docs/threats.md index cbf91cf7..33853d0c 100644 --- a/docs/threats.md +++ b/docs/threats.md @@ -228,7 +228,7 @@ An attacker targets a system that uses JavaScript Object Notation (JSON) as a tr
    https://capec.mitre.org/data/definitions/111.html, http://cwe.mitre.org/data/definitions/345.html, http://cwe.mitre.org/data/definitions/346.html, http://cwe.mitre.org/data/definitions/352.html
    Condition
    -
    target.implementsNonce is False and target.data =='JSON'
    +
    target.implementsNonce is False and any(d.format == 'JSON' for d in target.data)
  • @@ -748,7 +748,7 @@ An adversary corrupts or modifies the content of XML schema information passed b
    https://capec.mitre.org/data/definitions/146.html, http://cwe.mitre.org/data/definitions/15.html, http://cwe.mitre.org/data/definitions/472.html
    Condition
    -
    target.data == 'XML' and target.authorizesSource is False
    +
    any(d.format == 'XML' for d in target.data) and target.authorizesSource is False
    @@ -774,7 +774,7 @@ An attacker initiates a resource depletion attack where a large number of small
    https://capec.mitre.org/data/definitions/147.html, http://cwe.mitre.org/data/definitions/400.html, http://cwe.mitre.org/data/definitions/770.html
    Condition
    -
    target.data == 'XML'
    +
    any(d.format == 'XML' for d in target.data)
    @@ -1268,7 +1268,7 @@ An attacker submits an XML document to a target application where the XML docume
    https://capec.mitre.org/data/definitions/197.html, http://cwe.mitre.org/data/definitions/400.html, http://cwe.mitre.org/data/definitions/770.html
    Condition
    -
    target.data == 'XML' and target.handlesResources is False
    +
    any(d.format == 'XML' for d in target.data) and target.handlesResources is False
    @@ -1606,7 +1606,7 @@ An attacker subverts an intermediate system used to process XML content and forc
    https://capec.mitre.org/data/definitions/219.html
    Condition
    -
    target.protocol == 'HTTP' and target.data =='XML'
    +
    target.protocol == 'HTTP' and any(d.format == 'XML' for d in target.data)
    @@ -2620,7 +2620,7 @@ An attacker can access data in transit or at rest that is not sufficiently prote
    https://cwe.mitre.org/data/definitions/311.html, https://cwe.mitre.org/data/definitions/312.html, https://cwe.mitre.org/data/definitions/916.html, https://cwe.mitre.org/data/definitions/653.html
    Condition
    -
    target.hasDataLeaks
    +
    target.hasDataLeaks()
    From cf1e31955371ae6803e01ee4c9679bff96cf4980 Mon Sep 17 00:00:00 2001 From: Jan Was Date: Tue, 13 Apr 2021 19:57:35 +0000 Subject: [PATCH 08/86] Revert "bugfix, added Finding.target to report output." This reverts commit 7d60b614525eaa90c94d345a11bc0366b2e16a39. --- pytm/pytm.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pytm/pytm.py b/pytm/pytm.py index 9158b03f..35c961f1 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -38,7 +38,7 @@ class var(object): - """ A descriptor that allows setting a value only once """ + """A descriptor that allows setting a value only once""" def __init__(self, default, required=False, doc="", onSet=None): self.default = default @@ -1128,7 +1128,7 @@ def __str__(self): return "{0}({1})".format(type(self).__name__, self.name) def _uniq_name(self): - """ transform name and uuid into a unique string """ + """transform name and uuid into a unique string""" h = sha224(str(self.uuid).encode("utf-8")).hexdigest() name = "".join(x for x in self.name if x.isalpha()) return "{0}_{1}_{2}".format(type(self).__name__.lower(), name, h[:10]) @@ -1182,7 +1182,7 @@ def _safeset(self, attr, value): pass def oneOf(self, *elements): - """ Is self one of a list of Elements """ + """Is self one of a list of Elements""" for element in elements: if inspect.isclass(element): if isinstance(self, element): @@ -1192,7 +1192,7 @@ def oneOf(self, *elements): return False def crosses(self, *boundaries): - """ Does self (dataflow) cross any of the list of boundaries """ + """Does self (dataflow) cross any of the list of boundaries""" if self.source.inBoundary is self.sink.inBoundary: return False for boundary in boundaries: @@ -1216,15 +1216,15 @@ def crosses(self, *boundaries): return False def enters(self, *boundaries): - """ does self (dataflow) enter into one of the list of boundaries """ + """does self (dataflow) enter into one of the list of boundaries""" return self.source.inBoundary is None and self.sink.inside(*boundaries) def exits(self, *boundaries): - """ does self (dataflow) exit one of the list of boundaries """ + """does self (dataflow) exit one of the list of boundaries""" return self.source.inside(*boundaries) and self.sink.inBoundary is None def inside(self, *boundaries): - """ is self inside of one of the list of boundaries """ + """is self inside of one of the list of boundaries""" for boundary in boundaries: if inspect.isclass(boundary): if isinstance(self.inBoundary, boundary): From 5262a4a031a6d816b1df6297d553bff14f01bf9c Mon Sep 17 00:00:00 2001 From: Jan Was Date: Tue, 13 Apr 2021 19:59:29 +0000 Subject: [PATCH 09/86] Finding.id should be a str --- pytm/pytm.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/pytm/pytm.py b/pytm/pytm.py index 35c961f1..922dcba6 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -582,7 +582,7 @@ class Finding: severity = varString("", required=True, doc="Threat severity") mitigations = varString("", required=True, doc="Threat mitigations") example = varString("", required=True, doc="Threat example") - id = varInt("", required=True, doc="Finding ID") + id = varString("", required=True, doc="Finding ID") threat_id = varString("", required=True, doc="Threat ID") references = varString("", required=True, doc="Threat references") condition = varString("", required=True, doc="Threat condition") @@ -746,7 +746,7 @@ def resolve(self): continue finding_count += 1 - f = Finding(e, id=finding_count, threat=t) + f = Finding(e, id=str(finding_count), threat=t) findings.append(f) elements[e].append(f) self.findings = findings @@ -1819,7 +1819,7 @@ def encode_threat_data(obj): "mitigations", "example", "id", - "target", + "threat_id", "references", "condition", ] @@ -1827,18 +1827,14 @@ def encode_threat_data(obj): for e in obj: t = copy.deepcopy(e) - if isinstance(t, Finding): - attrs.append("threat_id") - for a in attrs: - v = getattr(e, a) - - if isinstance(v, int): - t._safeset(a, v) - elif isinstance(v, tuple): - t._safeset(a, v) - else: - t._safeset(a, html.escape(v)) + try: + v = getattr(e, a) + except AttributeError: + # ignore missing attributes, since this can be called + # on both a Finding and a Threat + continue + setattr(t, a, html.escape(v)) encoded_threat_data.append(t) From deb9c82e2a82bff8f521a4d4509b9f0470c688dd Mon Sep 17 00:00:00 2001 From: Raphael Ahrens Date: Thu, 6 May 2021 16:37:41 +0200 Subject: [PATCH 10/86] Added datastore.png to setup.py datstore.png was not part of the `package_data` field so it was not installed. --- setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c28599a3..5296daa0 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,11 @@ python_requires=">=3", install_requires=["pydal>=20200714.1"], package_data={ - "pytm": ["images/lambda.png", "threatlib/threats.json"], + "pytm": [ + "images/datastore.png", + "images/lambda.png", + "threatlib/threats.json", + ], }, exclude_package_data={"": ["report.html"]}, include_package_data=True, From 7ecf1eaecfe6a263c47a53698aadec6709681aac Mon Sep 17 00:00:00 2001 From: Jan Was Date: Thu, 3 Jun 2021 18:34:08 +0000 Subject: [PATCH 11/86] Add missing dependencies in Dockerfile --- Dockerfile | 12 +++++++----- docs/pytm/index.html | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index b4976ac6..3129bf3e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,14 @@ -FROM python:3.8.6-alpine3.12 +FROM python:3.9.5-alpine3.13 WORKDIR /usr/src/app ENTRYPOINT ["sh"] -ENV PLANTUML_VER 1.2020.18 +ENV PLANTUML_VER 1.2021.7 ENV PLANTUML_PATH /usr/local/lib/plantuml.jar -ENV PANDOC_VER 2.10.1 +ENV PANDOC_VER 2.14.0.1 RUN apk add --no-cache graphviz openjdk11-jre fontconfig make curl ttf-liberation ttf-linux-libertine ttf-dejavu \ + && apk add --no-cache --virtual .build-deps gcc musl-dev \ && rm -rf /var/cache/apk/* \ && curl -LO https://netix.dl.sourceforge.net/project/plantuml/$PLANTUML_VER/plantuml.$PLANTUML_VER.jar \ && mv plantuml.$PLANTUML_VER.jar $PLANTUML_PATH \ @@ -17,8 +18,9 @@ RUN apk add --no-cache graphviz openjdk11-jre fontconfig make curl ttf-liberatio ENV _JAVA_OPTIONS -Duser.home=/tmp -Dawt.useSystemAAFontSettings=gasp RUN printf '@startuml\n@enduml' | java -Djava.awt.headless=true -jar $PLANTUML_PATH -tpng -pipe >/dev/null -COPY requirements.txt ./ -RUN pip install --no-cache-dir -r requirements.txt +COPY requirements.txt requirements-dev.txt ./ +RUN pip install --no-cache-dir -r requirements-dev.txt \ + && apk del .build-deps COPY pytm ./pytm COPY docs ./docs diff --git a/docs/pytm/index.html b/docs/pytm/index.html index f54328a8..30cb0a55 100644 --- a/docs/pytm/index.html +++ b/docs/pytm/index.html @@ -1653,7 +1653,7 @@

    Instance variables

    return "{0}({1})".format(type(self).__name__, self.name) def _uniq_name(self): - """ transform name and uuid into a unique string """ + """transform name and uuid into a unique string""" h = sha224(str(self.uuid).encode("utf-8")).hexdigest() name = "".join(x for x in self.name if x.isalpha()) return "{0}_{1}_{2}".format(type(self).__name__.lower(), name, h[:10]) @@ -1707,7 +1707,7 @@

    Instance variables

    pass def oneOf(self, *elements): - """ Is self one of a list of Elements """ + """Is self one of a list of Elements""" for element in elements: if inspect.isclass(element): if isinstance(self, element): @@ -1717,7 +1717,7 @@

    Instance variables

    return False def crosses(self, *boundaries): - """ Does self (dataflow) cross any of the list of boundaries """ + """Does self (dataflow) cross any of the list of boundaries""" if self.source.inBoundary is self.sink.inBoundary: return False for boundary in boundaries: @@ -1741,15 +1741,15 @@

    Instance variables

    return False def enters(self, *boundaries): - """ does self (dataflow) enter into one of the list of boundaries """ + """does self (dataflow) enter into one of the list of boundaries""" return self.source.inBoundary is None and self.sink.inside(*boundaries) def exits(self, *boundaries): - """ does self (dataflow) exit one of the list of boundaries """ + """does self (dataflow) exit one of the list of boundaries""" return self.source.inside(*boundaries) and self.sink.inBoundary is None def inside(self, *boundaries): - """ is self inside of one of the list of boundaries """ + """is self inside of one of the list of boundaries""" for boundary in boundaries: if inspect.isclass(boundary): if isinstance(self.inBoundary, boundary): @@ -1971,7 +1971,7 @@

    Methods

    Expand source code
    def crosses(self, *boundaries):
    -    """ Does self (dataflow) cross any of the list of boundaries """
    +    """Does self (dataflow) cross any of the list of boundaries"""
         if self.source.inBoundary is self.sink.inBoundary:
             return False
         for boundary in boundaries:
    @@ -2018,7 +2018,7 @@ 

    Methods

    Expand source code
    def enters(self, *boundaries):
    -    """ does self (dataflow) enter into one of the list of boundaries """
    +    """does self (dataflow) enter into one of the list of boundaries"""
         return self.source.inBoundary is None and self.sink.inside(*boundaries)
    @@ -2032,7 +2032,7 @@

    Methods

    Expand source code
    def exits(self, *boundaries):
    -    """ does self (dataflow) exit one of the list of boundaries """
    +    """does self (dataflow) exit one of the list of boundaries"""
         return self.source.inside(*boundaries) and self.sink.inBoundary is None
    @@ -2046,7 +2046,7 @@

    Methods

    Expand source code
    def inside(self, *boundaries):
    -    """ is self inside of one of the list of boundaries """
    +    """is self inside of one of the list of boundaries"""
         for boundary in boundaries:
             if inspect.isclass(boundary):
                 if isinstance(self.inBoundary, boundary):
    @@ -2066,7 +2066,7 @@ 

    Methods

    Expand source code
    def oneOf(self, *elements):
    -    """ Is self one of a list of Elements """
    +    """Is self one of a list of Elements"""
         for element in elements:
             if inspect.isclass(element):
                 if isinstance(self, element):
    @@ -2141,7 +2141,7 @@ 

    Instance variables

    severity = varString("", required=True, doc="Threat severity") mitigations = varString("", required=True, doc="Threat mitigations") example = varString("", required=True, doc="Threat example") - id = varInt("", required=True, doc="Finding ID") + id = varString("", required=True, doc="Finding ID") threat_id = varString("", required=True, doc="Threat ID") references = varString("", required=True, doc="Threat references") condition = varString("", required=True, doc="Threat condition") @@ -3602,7 +3602,7 @@

    Class variables

    continue finding_count += 1 - f = Finding(e, id=finding_count, threat=t) + f = Finding(e, id=str(finding_count), threat=t) findings.append(f) elements[e].append(f) self.findings = findings @@ -4230,7 +4230,7 @@

    Methods

    continue finding_count += 1 - f = Finding(e, id=finding_count, threat=t) + f = Finding(e, id=str(finding_count), threat=t) findings.append(f) elements[e].append(f) self.findings = findings From 808c83d4f182d9b0bb1bb547e6692b0ca290a65a Mon Sep 17 00:00:00 2001 From: Izar Tarandach Date: Wed, 4 Aug 2021 13:12:18 -0400 Subject: [PATCH 12/86] Fixed a serialization issue with sourceFiles and removed bad example from tm.py --- pytm/pytm.py | 2 ++ tm.py | 21 ++++++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/pytm/pytm.py b/pytm/pytm.py index 922dcba6..f57ffa02 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -1798,6 +1798,8 @@ def serialize(obj, nested=False): value = [v.__name__ for v in value] elif i == "levels": value = list(value) + elif i == "sourceFiles": + value = ",".join(value) elif ( not nested and not isinstance(value, str) diff --git a/tm.py b/tm.py index 5546e677..8c598640 100755 --- a/tm.py +++ b/tm.py @@ -58,7 +58,9 @@ my_lambda.inBoundary = vpc my_lambda.levels = [1, 2] -token_user_identity = Data("Token verifying user identity", classification=Classification.SECRET) +token_user_identity = Data( + "Token verifying user identity", classification=Classification.SECRET +) db_to_secretDb = Dataflow(db, secretDb, "Database verify real user identity") db_to_secretDb.protocol = "RDA-TCP" db_to_secretDb.dstPort = 40234 @@ -66,31 +68,36 @@ db_to_secretDb.note = "Verifying that the user is who they say they are." db_to_secretDb.maxClassification = Classification.SECRET -comments_in_text = Data("Comments in HTML or Markdown", classification=Classification.PUBLIC) +comments_in_text = Data( + "Comments in HTML or Markdown", classification=Classification.PUBLIC +) user_to_web = Dataflow(user, web, "User enters comments (*)") user_to_web.protocol = "HTTP" user_to_web.dstPort = 80 user_to_web.data = comments_in_text user_to_web.note = "This is a simple web app\nthat stores and retrieves user comments." +query_insert = Data("Insert query with comments", classification=Classification.PUBLIC) web_to_db = Dataflow(web, db, "Insert query with comments") web_to_db.protocol = "MySQL" web_to_db.dstPort = 3306 -# this is a BAD way of defining a data object, here for a demo on how it -# will appear on the sample report. Use Data objects. -web_to_db.data = "MySQL insert statement, all literals" +web_to_db.data = query_insert web_to_db.note = ( "Web server inserts user comments\ninto it's SQL query and stores them in the DB." ) -comment_retrieved = Data("Web server retrieves comments from DB", classification=Classification.PUBLIC) +comment_retrieved = Data( + "Web server retrieves comments from DB", classification=Classification.PUBLIC +) db_to_web = Dataflow(db, web, "Retrieve comments") db_to_web.protocol = "MySQL" db_to_web.dstPort = 80 db_to_web.data = comment_retrieved db_to_web.responseTo = web_to_db -comment_to_show = Data("Web server shows comments to the end user", classifcation=Classification.PUBLIC) +comment_to_show = Data( + "Web server shows comments to the end user", classifcation=Classification.PUBLIC +) web_to_user = Dataflow(web, user, "Show comments (*)") web_to_user.protocol = "HTTP" web_to_user.data = comment_to_show From 6331fa1aacf267e7247f14efe11378037ef30e55 Mon Sep 17 00:00:00 2001 From: Izar Tarandach Date: Wed, 4 Aug 2021 13:19:36 -0400 Subject: [PATCH 13/86] Fixed a serialization issue with sourceFiles and removed bad example from tm.py --- pytm/pytm.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pytm/pytm.py b/pytm/pytm.py index f57ffa02..7e351bea 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -195,7 +195,7 @@ def __set__(self, instance, value): ) ] sys.stderr.write( - f"FIXME: a dataflow is using a string as the Data attribute. This has been deprecated and Data objects should be created instead.\n" + "FIXME: a dataflow is using a string as the Data attribute. This has been deprecated and Data objects should be created instead.\n" ) if not isinstance(value, Iterable): @@ -1796,10 +1796,8 @@ 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": + elif i == "levels" or i == "sourceFiles": value = list(value) - elif i == "sourceFiles": - value = ",".join(value) elif ( not nested and not isinstance(value, str) From 14e89f8f07eca89108d6a13b60da61fb5a5de9b2 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Thu, 5 Aug 2021 01:31:30 +0000 Subject: [PATCH 14/86] fix: Dockerfile to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-ALPINE313-APKTOOLS-1533754 - https://snyk.io/vuln/SNYK-ALPINE313-KRB5-1533445 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 3129bf3e..54d51bd9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.5-alpine3.13 +FROM python:3.10.0rc1-alpine3.13 WORKDIR /usr/src/app ENTRYPOINT ["sh"] From 77dcab7ed066987fba1f9d6058d83796a56ff903 Mon Sep 17 00:00:00 2001 From: Raphael Ahrens Date: Thu, 5 Aug 2021 16:06:02 +0200 Subject: [PATCH 15/86] Added the list-elements command model.py --list-elements shows a list of all elements which can be used in a threat model with pytm. Why? I often find my self looking up the exact names of the elements and there doc string. Currently the output looks like this. Elements: Actor -- An entity usually initiating actions Asset -- An asset with outgoing or incoming dataflows Boundary -- Trust boundary groups elements and data with the same trust level. Dataflow -- A data flow from a source to a sink Datastore -- An entity storing data ExternalEntity -- Lambda -- A lambda function running in a Function-as-a-Service (FaaS) environment Process -- An entity processing data Server -- An entity processing data SetOfProcesses -- Atributes: Action -- Action taken when validating a threat model. Classification -- An enumeration. Data -- Represents a single piece of data that traverses the system Lifetime -- An enumeration. TLSVersion -- An enumeration. --- pytm/pytm.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pytm/pytm.py b/pytm/pytm.py index 7e351bea..6899234c 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -492,6 +492,32 @@ def _describe_classes(classes): print() +def _list_elements(): + """List all elements which can be used in a threat model with the corisponding description""" + def all_subclasses(cls): + """Get all sub classes of a class""" + subclasses = set(cls.__subclasses__()) + return subclasses.union( + (s for c in subclasses for s in all_subclasses(c))) + + def print_components(cls_list): + elements = sorted(cls_list, key=lambda c: c.__name__) + max_len = max((len(e.__name__) for e in elements)) + for sc in elements: + doc = sc.__doc__ if sc.__doc__ is not None else '' + print(f'{sc.__name__:<{max_len}} -- {doc}') + #print all elements + print('Elements:') + print_components(all_subclasses(Element)) + + # Print Attributes + print('\nAtributes:') + print_components( + all_subclasses(OrderedEnum) | {Data, Action, Lifetime} + ) + + + def _get_elements_and_boundaries(flows): """filter out elements and boundaries not used in this TM""" elements = set() @@ -983,6 +1009,9 @@ def process(self): if result.describe is not None: _describe_classes(result.describe.split()) + if result.list_elements: + _list_elements() + if result.list is True: [print("{} - {}".format(t.id, t.description)) for t in TM._threats] @@ -1864,6 +1893,9 @@ def get_args(): _parser.add_argument( "--describe", help="describe the properties available for a given element" ) + _parser.add_argument( + "--list-elements", action="store_true", help="list all elements which can be part of a threat model" + ) _parser.add_argument("--json", help="output a JSON file") _parser.add_argument( "--levels", From 419205f6b443e807cc10a967c97388baf0776c1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannik=20Sch=C3=A4fer?= Date: Fri, 6 Aug 2021 11:01:20 +0200 Subject: [PATCH 16/86] Fixed sample threat model in README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0e126836..46b01672 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ that periodically cleans the Database. #!/usr/bin/env python3 -from pytm.pytm import TM, Server, Datastore, Dataflow, Boundary, Actor, Lambda +from pytm.pytm import TM, Server, Datastore, Dataflow, Boundary, Actor, Lambda, Data, Classification tm = TM("my test tm") tm.description = "another test tm" @@ -163,11 +163,12 @@ web_to_user.protocol = "HTTP" web_to_db = Dataflow(web, db, "Insert query with comments") web_to_db.protocol = "MySQL" web_to_db.dstPort = 3306 + +db_to_web = Dataflow(db, web, "Comments contents") +db_to_web.protocol = "MySQL" # this is a BAD way of defining a data object, here for a demo on how it # will appear on the sample report. Use Data objects. db_to_web.data = 'Results of insert op' -db_to_web = Dataflow(db, web, "Comments contents") -db_to_web.protocol = "MySQL" tm.process() From f85886b512d8fdd749ea3f7cb05abea7b8874de5 Mon Sep 17 00:00:00 2001 From: Izar Tarandach Date: Thu, 26 Aug 2021 09:21:19 -0400 Subject: [PATCH 17/86] initial draft of discussed roadmap --- ROADMAP.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 ROADMAP.md diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 00000000..c6cd02c3 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,16 @@ +# To the end of 2021 + +* add more threat rules +* add debugging capability to threat rules +* merge/close existing PRs + +# 1H2022 + +* add more rules +* move to a more complete rule evaluation engine +* export/import other popular TM tools data formats +* lower barrier of entry by adding a new, natural way of describing systems + +# 2H2022 + +* total world domination via threat modeling From f392ca39516f2b34b4a4f588dc433ba3eeb86898 Mon Sep 17 00:00:00 2001 From: Raphael Ahrens Date: Wed, 1 Sep 2021 10:37:55 +0200 Subject: [PATCH 18/86] Added the --list-elements command to the readme The --list-elements command was mising in the README.md. --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 46b01672..940c82a0 100644 --- a/README.md +++ b/README.md @@ -60,12 +60,13 @@ All available arguments: ```text usage: tm.py [-h] [--sqldump SQLDUMP] [--debug] [--dfd] [--report REPORT] [--exclude EXCLUDE] [--seq] [--list] [--describe DESCRIBE] - [--json JSON] [--levels LEVELS [LEVELS ...]] [--stale STALE] + [--list-elements] [--json JSON] [--levels LEVELS [LEVELS ...]] + [--stale_days STALE_DAYS] optional arguments: -h, --help show this help message and exit - --sqldump SQLDUMP dumps all threat model elements and findings into - the named sqlite file (erased if exists) + --sqldump SQLDUMP dumps all threat model elements and findings into the + named sqlite file (erased if exists) --debug print debug messages --dfd output DFD --report REPORT output report using the named template file (sample @@ -73,15 +74,16 @@ optional arguments: --exclude EXCLUDE specify threat IDs to be ignored --seq output sequential diagram --list list all available threats - --describe DESCRIBE describe the properties available for a given - element + --describe DESCRIBE describe the properties available for a given element + --list-elements list all elements which can be part of a threat model --json JSON output a JSON file --levels LEVELS [LEVELS ...] Select levels to be drawn in the threat model (int separated by comma). - --stale_days STALE_DAYS checks if the delta between the TM script and the - code described by it is bigger than the specified - value in days + --stale_days STALE_DAYS + checks if the delta between the TM script and the code + described by it is bigger than the specified value in + days ``` The *stale_days* argument tries to determine how far apart in days the model script (which you are writing) is from the code that implements the system being modeled. Ideally, they should be pretty close in most cases of an actively developed system. You can run this periodically to measure the pulse of your project and the 'freshness' of your threat model. From 7d850b0dc7c9a1c8710037d5ac2796af69b4d991 Mon Sep 17 00:00:00 2001 From: Raphael Ahrens Date: Thu, 2 Sep 2021 14:09:01 +0200 Subject: [PATCH 19/86] An empty threat model with ignoreUnused throws an error If a model has only elements which will not be displayed because ignoreUnused is set to True the function _sort_elements() fails. This is caused by a call to max(orders.values())` which then is `max([])` which is not a computable value. The error is caused by a module like this or an empty model. ``` from pytm import ( TM, Actor, ) tm = TM("my test tm") tm.ignoreUnused = True actor = Actor('actor') if __name__ == "__main__": tm.process() ``` --- pytm/pytm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytm/pytm.py b/pytm/pytm.py index 6899234c..4a15cb27 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -318,6 +318,8 @@ def _sort(flows, addOrder=False): def _sort_elem(elements): + if len(elements) == 0: + return elements orders = {} for e in elements: try: From 1ccdd9b8267bbee8cbee6b2b5100202f042c6d6f Mon Sep 17 00:00:00 2001 From: Raphael Ahrens Date: Wed, 8 Sep 2021 10:44:20 +0200 Subject: [PATCH 20/86] Removed the obsolete use of Strings as data In the tests some data values where still set to strings --- tests/test_private_func.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_private_func.py b/tests/test_private_func.py index 7fae55d1..98f08a0c 100644 --- a/tests/test_private_func.py +++ b/tests/test_private_func.py @@ -81,16 +81,18 @@ def test_defaults(self): tm = TM("TM") user_data = Data("HTTP") user = Actor("User", data=user_data, authenticatesDestination=True) + json_data = Data("JSON") server = Server( - "Server", port=443, protocol="HTTPS", isEncrypted=True, data="JSON" + "Server", port=443, protocol="HTTPS", isEncrypted=True, data=json_data ) + sql_resp = Data("SQL resp") db = Datastore( "PostgreSQL", isSQL=True, port=5432, protocol="PostgreSQL", isEncrypted=False, - data="SQL resp", + data=sql_resp, ) worker = Process("Task queue worker") @@ -106,8 +108,9 @@ def test_defaults(self): req_post_data = Data("JSON") req_post = Dataflow(user, server, "HTTP POST", data=req_post_data) resp_post = Dataflow(server, user, "HTTP Response", isResponse=True) - - worker_query = Dataflow(worker, db, "Query", data="SQL") + + sql_data = Data("SQL") + worker_query = Dataflow(worker, db, "Query", data=sql_data) Dataflow(db, worker, "Results", isResponse=True) cookie = Data("Auth Cookie", carriedBy=[req_get, req_post]) From dad8fd1111eb5ce7491365d3f71bee4eb9480fb0 Mon Sep 17 00:00:00 2001 From: Izar Tarandach Date: Wed, 15 Sep 2021 11:34:36 -0400 Subject: [PATCH 21/86] HTML escaping missed the 'target' field when cleaning Findings --- pytm/pytm.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pytm/pytm.py b/pytm/pytm.py index 7e351bea..955b9e18 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -659,7 +659,7 @@ def __repr__(self): ) def __str__(self): - return f"{self.target}: {self.description}\n{self.details}\n{self.severity}" + return f"'{self.target}': {self.description}\n{self.details}\n{self.severity}" class TM: @@ -747,6 +747,7 @@ def resolve(self): finding_count += 1 f = Finding(e, id=str(finding_count), threat=t) + logger.debug(f"new finding: {f}") findings.append(f) elements[e].append(f) self.findings = findings @@ -1824,6 +1825,9 @@ def encode_threat_data(obj): "condition", ] + if type(obj[0]) is Finding: + attrs.append("target") + for e in obj: t = copy.deepcopy(e) From 8cc9d03c374ff5c19dfd0f8218308dc330f0f9ed Mon Sep 17 00:00:00 2001 From: Izar Tarandach Date: Wed, 15 Sep 2021 11:34:36 -0400 Subject: [PATCH 22/86] HTML escaping missed the 'target' field when cleaning Findings --- pytm/pytm.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pytm/pytm.py b/pytm/pytm.py index 7e351bea..955b9e18 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -659,7 +659,7 @@ def __repr__(self): ) def __str__(self): - return f"{self.target}: {self.description}\n{self.details}\n{self.severity}" + return f"'{self.target}': {self.description}\n{self.details}\n{self.severity}" class TM: @@ -747,6 +747,7 @@ def resolve(self): finding_count += 1 f = Finding(e, id=str(finding_count), threat=t) + logger.debug(f"new finding: {f}") findings.append(f) elements[e].append(f) self.findings = findings @@ -1824,6 +1825,9 @@ def encode_threat_data(obj): "condition", ] + if type(obj[0]) is Finding: + attrs.append("target") + for e in obj: t = copy.deepcopy(e) From 7d0bbcc461ccab6fe31508132ae50d173ab672ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannik=20Sch=C3=A4fer?= Date: Fri, 17 Sep 2021 17:30:25 +0200 Subject: [PATCH 23/86] Fixed excluded threat IDs being ignored --- pytm/pytm.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pytm/pytm.py b/pytm/pytm.py index 4a15cb27..18e6e366 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -773,6 +773,9 @@ def resolve(self): 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) findings.append(f) @@ -981,6 +984,9 @@ def process(self): 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()) @@ -1005,9 +1011,6 @@ def process(self): 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()) From 063861946850c3b00d1643c8e70f5fd8bcd8dccb Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Mon, 27 Sep 2021 22:21:46 -0400 Subject: [PATCH 24/86] clean up & bugfixes --- docs/advanced_template.md | 11 +++++++---- pytm/template_engine.py | 19 ++++++++++--------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/advanced_template.md b/docs/advanced_template.md index 9d1297df..2fd184b5 100644 --- a/docs/advanced_template.md +++ b/docs/advanced_template.md @@ -14,9 +14,11 @@ Name|From|To |Data|Protocol|Port |:----:|:----:|:---:|:----:|:--------:|:----:| -{dataflows:repeat:|{{item.name}}|{{item.source.name}}|{{item.sink.name}}|{{item.data}}|{{item.protocol}}|{{item.dstPort}}| +{dataflows:repeat:|{{item.display_name:call:}}|{{item.source.name}}|{{item.sink.name}}|{{item.data}}|{{item.protocol}}|{{item.dstPort}}| } +{dataflows:repeat:{{item:call:getElementType}} +} ## Data Dictionary Name|Description|Classification|Carried|Processed @@ -63,7 +65,8 @@ Name|{{item.name}} |:----|:----| Description|{{item.description}}| In Scope|{{item.inScope}}| -Parent|{{item:call:getParentName}}{{item.parents:not:N/A, primary boundary}}| +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}}| @@ -93,7 +96,7 @@ Finding Count|{{item:call:getFindingCount}}| ## Assets {assets:repeat: -|Name|{{item.name}}| +Name|{{item.name}}| |:----|:----| Description|{{item.description}}| In Scope|{{item.inScope}}| @@ -131,7 +134,7 @@ Name|{{item.name}} Description|{{item.description}}| Sink|{{item.sink}}| Source|{{item.source}}| -|Is Response|{{item.isResponse}} +Is Response|{{item.isResponse}}| In Scope|{{item.inScope}}| Finding Count|{{item:call:getFindingCount}}| diff --git a/pytm/template_engine.py b/pytm/template_engine.py index aea40cdb..9118a5d4 100644 --- a/pytm/template_engine.py +++ b/pytm/template_engine.py @@ -22,7 +22,7 @@ def format_field(self, value, spec): elif spec.startswith("call:") and hasattr(value, "__call__"): # Example usage, format, exampple format - # methood:call {item:call:getParentName} + # methood:call {item.display_name:call:} # methood:call:template {item.parents:call:{{item.name}}, } result = value() @@ -36,28 +36,29 @@ def format_field(self, value, spec): # Example usage, format, exampple format # object:call:method_name {item:call:getFindingCount} # object:call:method_name:template {item:call:getNamesOfParents: - # **{{item}}** + # {{item}} # } method_name = spec_parts[1] - template = spec.partition(":")[-1] result = self.call_util_method(method_name, value) if type(result) is list: + template = spec.partition(":")[-1] + template = template.partition(":")[-1] return "".join([self.format(template, item=item) for item in result]) return result elif (spec.startswith("if") or spec.startswith("not")): # Example usage, format, exampple format - # object.bool:if:template {item.inScope:if:

    Is in scope.

    } - # object:if:template {item.findings:if:

    Has Findings

    } - # object.method:if:template {item.parents:if:

    Has Parents

    } + # object.bool:if:template {item.inScope:if:Is in scope} + # object:if:template {item.findings:if:Has Findings} + # object.method:if:template {item.parents:if:Has Parents} # - # object.bool:not:template {item.inScope:not:

    Is not in scope.

    } - # object:not:template {item.findings:not:

    Has No Findings

    } - # object.method:not:template {item.parents:not:

    Has No Parents

    } + # object.bool:not:template {item.inScope:not:Is not in scope} + # object:not:template {item.findings:not:Has No Findings} + # object.method:not:template {item.parents:not:Has No Parents} template = spec.partition(":")[-1] if (hasattr(value, "__call__")): From 9ce94a1c0ea15c4531093456bfeb16c483c2fd8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannik=20Sch=C3=A4fer?= Date: Tue, 28 Sep 2021 19:52:13 +0200 Subject: [PATCH 25/86] Add unittest for excluded threat IDs --- tests/test_pytmfunc.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_pytmfunc.py b/tests/test_pytmfunc.py index f0e3e90a..d390b8fe 100644 --- a/tests/test_pytmfunc.py +++ b/tests/test_pytmfunc.py @@ -186,6 +186,28 @@ def test_dfd_duplicates_raise(self): with self.assertRaisesRegex(ValueError, e): tm.check() + def test_exclude_threats_ignore(self): + random.seed(0) + + TM.reset() + + excluded_threat = "INP03" + remaining_threat = "AA01" + + TM._threatsExcluded = [excluded_threat] + + tm = TM("my test tm", description="aaa") + web = Server("Web") + web.sanitizesInput = False + web.encodesOutput = False + self.assertTrue(threats[excluded_threat].apply(web)) + self.assertTrue(threats[remaining_threat].apply(web)) + + tm.resolve() + + self.assertNotIn(excluded_threat, [t.threat_id for t in tm.findings]) + self.assertIn(remaining_threat, [t.threat_id for t in tm.findings]) + def test_resolve(self): random.seed(0) From cdd4003fffe4635cf35709d3d76773f13ce20727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannik=20Sch=C3=A4fer?= Date: Tue, 28 Sep 2021 20:03:25 +0200 Subject: [PATCH 26/86] Reset '_threatsExcluded' when calling TM.reset() --- pytm/pytm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pytm/pytm.py b/pytm/pytm.py index 18e6e366..5b3ec3e5 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -739,6 +739,7 @@ def reset(cls): cls._threats = [] cls._boundaries = [] cls._data = [] + cls._threatsExcluded = [] def _init_threats(self): TM._threats = [] From beaffa300283f8be7ebcf4f330a3d2125b5a90c3 Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Wed, 29 Sep 2021 00:51:56 -0400 Subject: [PATCH 27/86] Added Controls class as an Element instance variable, moved control based annotations to control class, updated threatlib, enabled json output and updated existing tests. --- pytm/json.py | 1 + pytm/pytm.py | 400 ++++++++----- pytm/threatlib/threats.json | 184 +++--- tests/output.json | 1074 ++++++++++++++++++++++++++--------- tests/test_private_func.py | 31 +- tests/test_pytmfunc.py | 350 ++++++------ tm.py | 16 +- 7 files changed, 1375 insertions(+), 681 deletions(-) diff --git a/pytm/json.py b/pytm/json.py index 1d77ffaa..db0c7af7 100644 --- a/pytm/json.py +++ b/pytm/json.py @@ -14,6 +14,7 @@ SetOfProcesses, Action, Lambda, + Controls, ) diff --git a/pytm/pytm.py b/pytm/pytm.py index 9791b7a4..897c8dc6 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -235,6 +235,14 @@ def __ne__(self, other): def __str__(self): return ", ".join(sorted(set(d.name for d in self))) +class varControls(var): + def __set__(self, instance, value): + if not isinstance(value, Controls): + raise ValueError( + "expecting an Controls " + "value, got a {}".format(type(value)) + ) + super().__set__(instance, value) class Action(Enum): """Action taken when validating a threat model.""" @@ -409,25 +417,25 @@ def _apply_defaults(flows, data): if e.isResponse: e._safeset("protocol", e.source.protocol) e._safeset("srcPort", e.source.port) - e._safeset("isEncrypted", e.source.isEncrypted) + e.controls._safeset("isEncrypted", e.source.controls.isEncrypted) continue e._safeset("protocol", e.sink.protocol) e._safeset("dstPort", e.sink.port) - if hasattr(e.sink, "isEncrypted"): - e._safeset("isEncrypted", e.sink.isEncrypted) - e._safeset("authenticatesDestination", e.source.authenticatesDestination) - e._safeset("checksDestinationRevocation", e.source.checksDestinationRevocation) + if hasattr(e.sink.controls, "isEncrypted"): + e.controls._safeset("isEncrypted", e.sink.controls.isEncrypted) + e.controls._safeset("authenticatesDestination", e.source.controls.authenticatesDestination) + e.controls._safeset("checksDestinationRevocation", e.source.controls.checksDestinationRevocation) for d in e.data: if d.isStored: - if hasattr(e.sink, "isEncryptedAtRest"): + if hasattr(e.sink.controls, "isEncryptedAtRest"): for d in e.data: - d._safeset("isDestEncryptedAtRest", e.sink.isEncryptedAtRest) + d._safeset("isDestEncryptedAtRest", e.sink.controls.isEncryptedAtRest) if hasattr(e.source, "isEncryptedAtRest"): for d in e.data: d._safeset( - "isSourceEncryptedAtRest", e.source.isEncryptedAtRest + "isSourceEncryptedAtRest", e.source.controls.isEncryptedAtRest ) if d.credentialsLife != Lifetime.NONE and not d.isCredentials: d._safeset("isCredentials", True) @@ -703,7 +711,7 @@ class TM: _data = [] _threatsExcluded = [] _sf = None - _duplicate_ignored_attrs = "name", "note", "order", "response", "responseTo" + _duplicate_ignored_attrs = "name", "note", "order", "response", "responseTo", "controls" name = varString("", required=True, doc="Model name") description = varString("", required=True, doc="Model description") threatsFile = varString( @@ -840,6 +848,17 @@ def _check_duplicates(self, flows): right._is_drawn = True continue + left_controls_attrs = left.controls._attr_values() + right_controls_attrs = right.controls._attr_values() + #for a in self._duplicate_ignored_attrs: + # del left_controls_attrs[a], right_controls_attrs[a] + if left_controls_attrs != right_controls_attrs: + continue + if self.onDuplicates == Action.IGNORE: + right._is_drawn = True + continue + + raise ValueError( "Duplicate Dataflow found between {} and {}: " "{} is same as {}".format( @@ -1112,6 +1131,110 @@ def get_table(self, db, klass): ] return db.define_table(name, fields) +class Controls: + """Controls implemented by/on and Element""" + + #allowsClientSideScripting = varBool(False) #NOZ - positive attestation of negative behavior?. not a fan of this one. + authenticatesDestination = varBool( + False, + doc="""Verifies the identity of the destination, +for example by verifying the authenticity of a digital certificate.""", + ) + authenticatesSource = varBool(False) + authenticationScheme = varString("") + authorizesSource = varBool(False) + checksDestinationRevocation = varBool( + False, + doc="""Correctly checks the revocation status +of credentials used to authenticate the destination""", + ) + checksInputBounds = varBool(False) + definesConnectionTimeout = varBool(False) + disablesDTD = varBool(False) + disablesiFrames = varBool(False) + encodesHeaders = varBool(False) + encodesOutput = varBool(False) + encryptsCookies = varBool(False) + encryptsSessionData = varBool(False) + handlesCrashes = varBool(False) + handlesInterruptions = varBool(False) + handlesResourceConsumption = varBool(False) + #handlesResources = varBool(False) #NOZ What is this? + hasAccessControl = varBool(False) + implementsAuthenticationScheme = varBool(False) + implementsCSRFToken = varBool(False) + implementsNonce = varBool( + False, + doc="""Nonce is an arbitrary number +that can be used just once in a cryptographic communication. +It is often a random or pseudo-random number issued in an authentication protocol +to ensure that old communications cannot be reused in replay attacks. +They can also be useful as initialization vectors and in cryptographic +hash functions.""", + ) + implementsPOLP = varBool( + False, + doc="""The principle of least privilege (PoLP), +also known as the principle of minimal privilege or the principle of least authority, +requires that in a particular abstraction layer of a computing environment, +every module (such as a process, a user, or a program, depending on the subject) +must be able to access only the information and resources +that are necessary for its legitimate purpose.""", + ) + implementsServerSideValidation = varBool(False) + implementsStrictHTTPValidation = varBool(False) + invokesScriptFilters = varBool(False) + isEncrypted = varBool(False, doc="Requires incoming data flow to be encrypted") + isEncryptedAtRest = varBool(False, doc="Stored data is encrypted at rest") + isHardened = varBool(False) + isResilient = varBool(False) + providesConfidentiality = varBool(False) + providesIntegrity = varBool(False) + sanitizesInput = varBool(False) + tracksExecutionFlow = varBool(False) + #usesCache = varBool(False) + usesCodeSigning = varBool(False) + usesEncryptionAlgorithm = varString("") + usesMFA = varBool( + False, + doc="""Multi-factor authentication is an authentication method +in which a computer user is granted access only after successfully presenting two +or more pieces of evidence (or factors) to an authentication mechanism: knowledge +(something the user and only the user knows), possession (something the user +and only the user has), and inherence (something the user and only the user is).""", + ) + usesParameterizedInput = varBool(False) + usesSecureFunctions = varBool(False) + #usesSessionTokens = varBool(False) #NOZ - Was this intended as a control? + usesStrongSessionIdentifiers = varBool(False) #NOZ - Was this intended as a control? + usesVPN = varBool(False) + validatesContentType = varBool(False) + validatesHeaders = varBool(False) + validatesInput = varBool(False) + verifySessionIdentifiers = varBool(False) + + def _attr_values(self): + klass = self.__class__ + result = {} + for i in dir(klass): + if i.startswith("_") or callable(getattr(klass, i)): + continue + attr = getattr(klass, i, {}) + if isinstance(attr, var): + value = attr.data.get(self, attr.default) + else: + value = getattr(self, i) + result[i] = value + return result + + + def _safeset(self, attr, value): + try: + setattr(self, attr, value) + except ValueError: + pass + + class Element: """A generic element""" @@ -1142,11 +1265,13 @@ class Element: required=False, doc="Location of the source code that describes this element relative to the directory of the model script.", ) + controls = varControls(None) def __init__(self, name, **kwargs): for key, value in kwargs.items(): setattr(self, key, value) self.name = name + self.controls = Controls() self.uuid = uuid.UUID(int=random.getrandbits(128)) self._is_drawn = False TM._elements.append(self) @@ -1354,47 +1479,47 @@ class Asset(Element): """An asset with outgoing or incoming dataflows""" port = varInt(-1, doc="Default TCP port for incoming data flows") - isEncrypted = varBool(False, doc="Requires incoming data flow to be encrypted") +# isEncrypted = varBool(False, doc="Requires incoming data flow to be encrypted") protocol = varString("", doc="Default network protocol for incoming data flows") data = varData([], doc="pytm.Data object(s) in incoming data flows") inputs = varElements([], doc="incoming Dataflows") outputs = varElements([], doc="outgoing Dataflows") onAWS = varBool(False) - isHardened = varBool(False) - implementsAuthenticationScheme = varBool(False) - implementsNonce = varBool( - False, - doc="""Nonce is an arbitrary number -that can be used just once in a cryptographic communication. -It is often a random or pseudo-random number issued in an authentication protocol -to ensure that old communications cannot be reused in replay attacks. -They can also be useful as initialization vectors and in cryptographic -hash functions.""", - ) +# isHardened = varBool(False) +# implementsAuthenticationScheme = varBool(False) +# implementsNonce = varBool( +# False, +# doc="""Nonce is an arbitrary number +#that can be used just once in a cryptographic communication. +#It is often a random or pseudo-random number issued in an authentication protocol +#to ensure that old communications cannot be reused in replay attacks. +#They can also be useful as initialization vectors and in cryptographic +#hash functions.""", +# ) handlesResources = varBool(False) - definesConnectionTimeout = varBool(False) - authenticatesDestination = varBool( - False, - doc="""Verifies the identity of the destination, -for example by verifying the authenticity of a digital certificate.""", - ) - checksDestinationRevocation = varBool( - False, - doc="""Correctly checks the revocation status -of credentials used to authenticate the destination""", - ) - authenticatesSource = varBool(False) - authorizesSource = varBool(False) - hasAccessControl = varBool(False) - validatesInput = varBool(False) - sanitizesInput = varBool(False) - checksInputBounds = varBool(False) - encodesOutput = varBool(False) - handlesResourceConsumption = varBool(False) - authenticationScheme = varString("") +# definesConnectionTimeout = varBool(False) +# authenticatesDestination = varBool( +# False, +# doc="""Verifies the identity of the destination, +#for example by verifying the authenticity of a digital certificate.""", +# ) +# checksDestinationRevocation = varBool( +# False, +# doc="""Correctly checks the revocation status +#of credentials used to authenticate the destination""", +# ) +# authenticatesSource = varBool(False) +# authorizesSource = varBool(False) +# hasAccessControl = varBool(False) +# validatesInput = varBool(False) +# sanitizesInput = varBool(False) +# checksInputBounds = varBool(False) +# encodesOutput = varBool(False) +# handlesResourceConsumption = varBool(False) +# authenticationScheme = varString("") usesEnvironmentVariables = varBool(False) OS = varString("") - providesIntegrity = varBool(False) +# providesIntegrity = varBool(False) def __init__(self, name, **kwargs): super().__init__(name, **kwargs) @@ -1446,33 +1571,33 @@ def _shape(self): class Server(Asset): """An entity processing data""" - providesConfidentiality = varBool(False) - providesIntegrity = varBool(False) - validatesHeaders = varBool(False) - encodesHeaders = varBool(False) - implementsCSRFToken = varBool(False) - isResilient = varBool(False) + #providesConfidentiality = varBool(False) + #providesIntegrity = varBool(False) + #validatesHeaders = varBool(False) + #encodesHeaders = varBool(False) + #implementsCSRFToken = varBool(False) + #isResilient = varBool(False) usesSessionTokens = varBool(False) - usesEncryptionAlgorithm = varString("") + #usesEncryptionAlgorithm = varString("") usesCache = varBool(False) usesVPN = varBool(False) - usesCodeSigning = varBool(False) - validatesContentType = varBool(False) - invokesScriptFilters = varBool(False) - usesStrongSessionIdentifiers = varBool(False) - implementsServerSideValidation = varBool(False) + #usesCodeSigning = varBool(False) + #validatesContentType = varBool(False) + #invokesScriptFilters = varBool(False) + #usesStrongSessionIdentifiers = varBool(False) + #implementsServerSideValidation = varBool(False) usesXMLParser = varBool(False) - disablesDTD = varBool(False) - implementsStrictHTTPValidation = varBool(False) - implementsPOLP = varBool( - False, - doc="""The principle of least privilege (PoLP), -also known as the principle of minimal privilege or the principle of least authority, -requires that in a particular abstraction layer of a computing environment, -every module (such as a process, a user, or a program, depending on the subject) -must be able to access only the information and resources -that are necessary for its legitimate purpose.""", - ) + #disablesDTD = varBool(False) + #implementsStrictHTTPValidation = varBool(False) + #implementsPOLP = varBool( + # False, + # doc="""The principle of least privilege (PoLP), +#also known as the principle of minimal privilege or the principle of least authority, +#requires that in a particular abstraction layer of a computing environment, +#every module (such as a process, a user, or a program, depending on the subject) +#must be able to access only the information and resources +#that are necessary for its legitimate purpose.""", +# ) def __init__(self, name, **kwargs): super().__init__(name, **kwargs) @@ -1500,24 +1625,24 @@ class Datastore(Asset): ) storesSensitiveData = varBool(False) isSQL = varBool(True) - providesConfidentiality = varBool(False) - providesIntegrity = varBool(False) +# providesConfidentiality = varBool(False) +# providesIntegrity = varBool(False) isShared = varBool(False) hasWriteAccess = varBool(False) - handlesResourceConsumption = varBool(False) - isResilient = varBool(False) - handlesInterruptions = varBool(False) - usesEncryptionAlgorithm = varString("") - implementsPOLP = varBool( - False, - doc="""The principle of least privilege (PoLP), -also known as the principle of minimal privilege or the principle of least authority, -requires that in a particular abstraction layer of a computing environment, -every module (such as a process, a user, or a program, depending on the subject) -must be able to access only the information and resources -that are necessary for its legitimate purpose.""", - ) - isEncryptedAtRest = varBool(False, doc="Stored data is encrypted at rest") +# handlesResourceConsumption = varBool(False) +# isResilient = varBool(False) +# handlesInterruptions = varBool(False) +# usesEncryptionAlgorithm = varString("") +# implementsPOLP = varBool( +# False, +# doc="""The principle of least privilege (PoLP), +#also known as the principle of minimal privilege or the principle of least authority, +#requires that in a particular abstraction layer of a computing environment, +#every module (such as a process, a user, or a program, depending on the subject) +#must be able to access only the information and resources +#that are necessary for its legitimate purpose.""", +# ) +# isEncryptedAtRest = varBool(False, doc="Stored data is encrypted at rest") def __init__(self, name, **kwargs): super().__init__(name, **kwargs) @@ -1562,19 +1687,19 @@ class Actor(Element): data = varData([], doc="pytm.Data object(s) in outgoing data flows") inputs = varElements([], doc="incoming Dataflows") outputs = varElements([], doc="outgoing Dataflows") - authenticatesDestination = varBool( - False, - doc="""Verifies the identity of the destination, -for example by verifying the authenticity of a digital certificate.""", - ) - checksDestinationRevocation = varBool( - False, - doc="""Correctly checks the revocation status -of credentials used to authenticate the destination""", - ) +# authenticatesDestination = varBool( +# False, +# doc="""Verifies the identity of the destination, +#for example by verifying the authenticity of a digital certificate.""", +# ) +# checksDestinationRevocation = varBool( +# False, +# doc="""Correctly checks the revocation status +#of credentials used to authenticate the destination""", +# ) isAdmin = varBool(False) # should not be settable, but accessible - providesIntegrity = False +# providesIntegrity = False def __init__(self, name, **kwargs): super().__init__(name, **kwargs) @@ -1586,41 +1711,41 @@ class Process(Asset): codeType = varString("Unmanaged") implementsCommunicationProtocol = varBool(False) - providesConfidentiality = varBool(False) - providesIntegrity = varBool(False) - isResilient = varBool(False) +# providesConfidentiality = varBool(False) +# providesIntegrity = varBool(False) +# isResilient = varBool(False) tracksExecutionFlow = varBool(False) - implementsCSRFToken = varBool(False) - handlesResourceConsumption = varBool(False) - handlesCrashes = varBool(False) - handlesInterruptions = varBool(False) +# implementsCSRFToken = varBool(False) +# handlesResourceConsumption = varBool(False) +# handlesCrashes = varBool(False) +# handlesInterruptions = varBool(False) implementsAPI = varBool(False) - usesSecureFunctions = varBool(False) +# usesSecureFunctions = varBool(False) environment = varString("") - disablesiFrames = varBool(False) - implementsPOLP = varBool( - False, - doc="""The principle of least privilege (PoLP), -also known as the principle of minimal privilege or the principle of least authority, -requires that in a particular abstraction layer of a computing environment, -every module (such as a process, a user, or a program, depending on the subject) -must be able to access only the information and resources -that are necessary for its legitimate purpose.""", - ) - usesParameterizedInput = varBool(False) +# disablesiFrames = varBool(False) +# implementsPOLP = varBool( +# False, +# doc="""The principle of least privilege (PoLP), +#also known as the principle of minimal privilege or the principle of least authority, +#requires that in a particular abstraction layer of a computing environment, +#every module (such as a process, a user, or a program, depending on the subject) +#must be able to access only the information and resources +#that are necessary for its legitimate purpose.""", +# ) +# usesParameterizedInput = varBool(False) allowsClientSideScripting = varBool(False) - usesStrongSessionIdentifiers = varBool(False) - encryptsCookies = varBool(False) - usesMFA = varBool( - False, - doc="""Multi-factor authentication is an authentication method -in which a computer user is granted access only after successfully presenting two -or more pieces of evidence (or factors) to an authentication mechanism: knowledge -(something the user and only the user knows), possession (something the user -and only the user has), and inherence (something the user and only the user is).""", - ) - encryptsSessionData = varBool(False) - verifySessionIdentifiers = varBool(False) +# usesStrongSessionIdentifiers = varBool(False) +# encryptsCookies = varBool(False) +# usesMFA = varBool( +# False, +# doc="""Multi-factor authentication is an authentication method +#in which a computer user is granted access only after successfully presenting two +#or more pieces of evidence (or factors) to an authentication mechanism: knowledge +#(something the user and only the user knows), possession (something the user +#and only the user has), and inherence (something the user and only the user is).""", +# ) +# encryptsSessionData = varBool(False) +# verifySessionIdentifiers = varBool(False) def __init__(self, name, **kwargs): super().__init__(name, **kwargs) @@ -1647,7 +1772,7 @@ class Dataflow(Element): responseTo = varElement(None, doc="Is a response to this data flow") srcPort = varInt(-1, doc="Source TCP port") dstPort = varInt(-1, doc="Destination TCP port") - isEncrypted = varBool(False, doc="Is the data encrypted") +# isEncrypted = varBool(False, doc="Is the data encrypted") tlsVersion = varTLSVersion( TLSVersion.NONE, required=True, @@ -1655,23 +1780,23 @@ class Dataflow(Element): ) protocol = varString("", doc="Protocol used in this data flow") data = varData([], doc="pytm.Data object(s) in incoming data flows") - authenticatesDestination = varBool( - False, - doc="""Verifies the identity of the destination, -for example by verifying the authenticity of a digital certificate.""", - ) - checksDestinationRevocation = varBool( - False, - doc="""Correctly checks the revocation status -of credentials used to authenticate the destination""", - ) - authenticatedWith = varBool(False) +# authenticatesDestination = varBool( +# False, +# doc="""Verifies the identity of the destination, +#for example by verifying the authenticity of a digital certificate.""", +# ) +# checksDestinationRevocation = varBool( +# False, +# doc="""Correctly checks the revocation status +#of credentials used to authenticate the destination""", +# ) +# authenticatedWith = varBool(False) order = varInt(-1, doc="Number of this data flow in the threat model") - implementsAuthenticationScheme = varBool(False) +# implementsAuthenticationScheme = varBool(False) implementsCommunicationProtocol = varBool(False) note = varString("") usesVPN = varBool(False) - authorizesSource = varBool(False) +# authorizesSource = varBool(False) usesSessionTokens = varBool(False) def __init__(self, source, sink, name, **kwargs): @@ -1794,6 +1919,7 @@ def ts_tm(obj): return serialize(obj, nested=True) +@to_serializable.register(Controls) @to_serializable.register(Data) @to_serializable.register(Threat) @to_serializable.register(Element) diff --git a/pytm/threatlib/threats.json b/pytm/threatlib/threats.json index decb9dba..1a6a450a 100644 --- a/pytm/threatlib/threats.json +++ b/pytm/threatlib/threats.json @@ -9,7 +9,7 @@ "details": "This attack pattern involves causing a buffer overflow through manipulation of environment variables. Once the attacker finds that they can modify an environment variable, they may try to overflow associated buffers. This attack leverages implicit trust often placed in environment variables.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.usesEnvironmentVariables is True and target.sanitizesInput is False and target.checksInputBounds is False", + "condition": "target.usesEnvironmentVariables is True and target.controls.sanitizesInput is False and target.controls.checksInputBounds is False", "prerequisites": "The application uses environment variables.An environment variable exposed to the user is vulnerable to a buffer overflow.The vulnerable environment variable uses untrusted data.Tainted data used in the environment variables is not properly validated. For instance boundary checking is not done before copying the input data to a buffer.", "mitigations": "Do not expose environment variable to the user.Do not use untrusted data in your environment variables. Use a language or compiler that performs automatic bounds checking. There are tools such as Sharefuzz [R.10.3] which is an environment variable fuzzer for Unix that support loading a shared library. You can use Sharefuzz to determine if you are exposing an environment variable vulnerable to buffer overflow.", "example": "Attack Example: Buffer Overflow in $HOME A buffer overflow in sccw allows local users to gain root access via the $HOME environmental variable. Attack Example: Buffer Overflow in TERM A buffer overflow in the rlogin program involves its consumption of the TERM environmental variable.", @@ -24,7 +24,7 @@ "details": "Buffer Overflow attacks target improper or missing bounds checking on buffer operations, typically triggered by input injected by an adversary. As a consequence, an adversary is able to write past the boundaries of allocated buffer regions in memory, causing a program crash or potentially redirection of execution as per the adversaries' choice.", "Likelihood Of Attack": "High", "severity": "Very High", - "condition": "target.checksInputBounds is False", + "condition": "target.controls.checksInputBounds is False", "prerequisites": "Targeted software performs buffer operations.Targeted software inadequately performs bounds-checking on buffer operations.Adversary has the capability to influence the input to buffer operations.", "mitigations": "Use a language or compiler that performs automatic bounds checking. Use secure functions not vulnerable to buffer overflow. If you have to use dangerous functions, make sure that you do boundary checking. Compiler-based canary mechanisms such as StackGuard, ProPolice and the Microsoft Visual Studio /GS flag. Unless this provides automatic bounds checking, it is not a complete solution. Use OS-level preventative functionality. Not a complete solution. Utilize static source code analysis tools to identify potential buffer overflow weaknesses in the software.", "example": "The most straightforward example is an application that reads in input from the user and stores it in an internal buffer but does not check that the size of the input data is less than or equal to the size of the buffer. If the user enters excessive length data, the buffer may overflow leading to the application crashing, or worse, enabling the user to cause execution of injected code.Many web servers enforce security in web applications through the use of filter plugins. An example is the SiteMinder plugin used for authentication. An overflow in such a plugin, possibly through a long URL or redirect parameter, can allow an adversary not only to bypass the security checks but also execute arbitrary code on the target web server in the context of the user that runs the web server process.", @@ -39,7 +39,7 @@ "details": "An attacker can use Server Side Include (SSI) Injection to send code to a web application that then gets executed by the web server. Doing so enables the attacker to achieve similar results to Cross Site Scripting, viz., arbitrary code execution and information disclosure, albeit on a more limited scale, since the SSI directives are nowhere near as powerful as a full-fledged scripting language. Nonetheless, the attacker can conveniently gain access to sensitive files, such as password files, and execute shell commands.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.sanitizesInput is False or target.encodesOutput is False", + "condition": "target.controls.sanitizesInput is False or target.controls.encodesOutput is False", "prerequisites": "A web server that supports server side includes and has them enabledUser controllable input that can carry include directives to the web server", "mitigations": "Set the OPTIONS IncludesNOEXEC in the global access.conf file or local .htaccess (Apache) file to deny SSI execution in directories that do not need them. All user controllable input must be appropriately sanitized before use in the application. This includes omitting, or encoding, certain characters or strings that have the potential of being interpreted as part of an SSI directive. Server Side Includes must be enabled only if there is a strong business reason to do so. Every additional component enabled on the web server increases the attack surface as well as administrative overhead.", "example": "Consider a website hosted on a server that permits Server Side Includes (SSI), such as Apache with the Options Includes directive enabled. Whenever an error occurs, the HTTP Headers along with the entire request are logged, which can then be displayed on a page that allows review of such errors. A malicious user can inject SSI directives in the HTTP Headers of a request designed to create an error. When these logs are eventually reviewed, the server parses the SSI directives and executes them.", @@ -70,7 +70,7 @@ "details": "HTTP Request Splitting (also known as HTTP Request Smuggling) is an attack pattern where an attacker attempts to insert additional HTTP requests in the body of the original (enveloping) HTTP request in such a way that the browser interprets it as one request but the web server interprets it as two. There are several ways to perform HTTP request splitting attacks. One way is to include double Content-Length headers in the request to exploit the fact that the devices parsing the request may each use a different header. Another way is to submit an HTTP request with a Transfer Encoding: chunked in the request header set with setRequestHeader to allow a payload in the HTTP Request that can be considered as another HTTP Request by a subsequent parsing entity. A third way is to use the Double CR in an HTTP header technique. There are also a few less general techniques targeting specific parsing vulnerabilities in certain web servers.", "Likelihood Of Attack": "Medium", "severity": "High", - "condition": "(target.validatesInput is False or target.validatesHeaders is False) and target.protocol =='HTTP'", + "condition": "(target.controls.validatesInput is False or target.controls.validatesHeaders is False) and target.protocol =='HTTP'", "prerequisites": "User-manipulateable HTTP Request headers are processed by the web server", "mitigations": "Make sure to install the latest vendor security patches available for the web server. If possible, make use of SSL. Install a web application firewall that has been secured against HTTP Request Splitting. Use web servers that employ a tight HTTP parsing process.", "example": "Microsoft Internet Explorer versions 5.01 SP4 and prior, 6.0 SP2 and prior, and 7.0 contain a vulnerability that could allow an unauthenticated, remote attacker to conduct HTTP request splitting and smuggling attacks. The vulnerability is due to an input validation error in the browser that allows attackers to manipulate certain headers to expose the browser to HTTP request splitting and smuggling attacks. Attacks may include cross-site scripting, proxy cache poisoning, and session fixation. In certain instances, an exploit could allow the attacker to bypass web application firewalls or other filtering devices. Microsoft has confirmed the vulnerability and released software updates", @@ -86,7 +86,7 @@ "details": "Cross Site Tracing (XST) enables an adversary to steal the victim's session cookie and possibly other authentication credentials transmitted in the header of the HTTP request when the victim's browser communicates to destination system's web server. The adversary first gets a malicious script to run in the victim's browser that induces the browser to initiate an HTTP TRACE request to the web server. If the destination web server allows HTTP TRACE requests, it will proceed to return a response to the victim's web browser that contains the original HTTP request in its body. The function of HTTP TRACE, as defined by the HTTP specification, is to echo the request that the web server receives from the client back to the client. Since the HTTP header of the original request had the victim's session cookie in it, that session cookie can now be picked off the HTTP TRACE response and sent to the adversary's malicious site. XST becomes relevant when direct access to the session cookie via the document.cookie object is disabled with the use of httpOnly attribute which ensures that the cookie can be transmitted in HTTP requests but cannot be accessed in other ways. Using SSL does not protect against XST. If the system with which the victim is interacting is susceptible to XSS, an adversary can exploit that weakness directly to get his or her malicious script to issue an HTTP TRACE request to the destination system's web server. In the absence of an XSS weakness on the site with which the victim is interacting, an adversary can get the script to come from the site that he controls and get it to execute in the victim's browser (if he can trick the victim's into visiting his malicious website or clicking on the link that he supplies). However, in that case, due to the same origin policy protection mechanism in the browser, the adversary's malicious script cannot directly issue an HTTP TRACE request to the destination system's web server because the malicious script did not originate at that domain. An adversary will then need to find a way to exploit another weakness that would enable him or her to get around the same origin policy protection.", "Likelihood Of Attack": "Medium", "severity": "Very High", - "condition": "(target.protocol == 'HTTP' and target.usesSessionTokens is True) and (target.sanitizesInput is False or target.validatesInput is False)", + "condition": "(target.protocol == 'HTTP' and target.usesSessionTokens is True) and (target.controls.sanitizesInput is False or target.controls.validatesInput is False)", "prerequisites": "HTTP TRACE is enabled on the web serverThe destination system is susceptible to XSS or an adversary can leverage some other weakness to bypass the same origin policyScripting is enabled in the client's browserHTTP is used as the communication protocol between the server and the client", "mitigations": "Administrators should disable support for HTTP TRACE at the destination's web server. Vendors should disable TRACE by default. Patch web browser against known security origin policy bypass exploits.", "example": "An adversary determines that a particular system is vulnerable to reflected cross-site scripting (XSS) and endeavors to leverage this weakness to steal the victim's authentication cookie. An adversary realizes that since httpOnly attribute is set on the user's cookie, it is not possible to steal it directly with his malicious script. Instead, the adversary has their script use XMLHTTP ActiveX control in the victim's IE browser to issue an HTTP TRACE to the target system's server which has HTTP TRACE enabled. The original HTTP TRACE request contains the session cookie and so does the echoed response. The adversary picks the session cookie from the body of HTTP TRACE response and ships it to the adversary. The adversary then uses the newly acquired victim's session cookie to impersonate the victim in the target system.", @@ -101,7 +101,7 @@ "details": "An attacker uses standard SQL injection methods to inject data into the command line for execution. This could be done directly through misuse of directives such as MSSQL_xp_cmdshell or indirectly through injection of data into the database that would be interpreted as shell commands. Sometime later, an unscrupulous backend application (or could be part of the functionality of the same application) fetches the injected data stored in the database and uses this data as command line arguments without performing proper validation. The malicious data escapes that data plane by spawning new commands to be executed on the host.", "Likelihood Of Attack": "Low", "severity": "Very High", - "condition": "target.validatesInput is False", + "condition": "target.controls.validatesInput is False", "prerequisites": "The application does not properly validate data before storing in the databaseBackend application implicitly trusts the data stored in the databaseMalicious data is used on the backend as a command line argument", "mitigations": "Disable MSSQL xp_cmdshell directive on the databaseProperly validate the data (syntactically and semantically) before writing it to the database. Do not implicitly trust the data stored in the database. Re-validate it prior to usage to make sure that it is safe to use in a given context (e.g. as a command line argument).", "example": "SQL injection vulnerability in Cacti 0.8.6i and earlier, when register_argc_argv is enabled, allows remote attackers to execute arbitrary SQL commands via the (1) second or (2) third arguments to cmd.php. NOTE: this issue can be leveraged to execute arbitrary commands since the SQL query results are later used in the polling_items array and popen function", @@ -116,7 +116,7 @@ "details": "An attacker modifies the parameters of the SOAP message that is sent from the service consumer to the service provider to initiate a SQL injection attack. On the service provider side, the SOAP message is parsed and parameters are not properly validated before being used to access a database in a way that does not use parameter binding, thus enabling the attacker to control the structure of the executed SQL query. This pattern describes a SQL injection attack with the delivery mechanism being a SOAP message.", "Likelihood Of Attack": "High", "severity": "Very High", - "condition": "target.protocol == 'SOAP' and (target.sanitizesInput is False or target.validatesInput is False)", + "condition": "target.protocol == 'SOAP' and (target.controls.sanitizesInput is False or target.controls.validatesInput is False)", "prerequisites": "SOAP messages are used as a communication mechanism in the systemSOAP parameters are not properly validated at the service providerThe service provider does not properly utilize parameter binding when building SQL queries", "mitigations": "Properly validate and sanitize/reject user input at the service provider. Ensure that prepared statements or other mechanism that enables parameter binding is used when accessing the database in a way that would prevent the attackers' supplied data from controlling the structure of the executed query. At the database level, ensure that the database user used by the application in a particular context has the minimum needed privileges to the database that are needed to perform the operation. When possible, run queries against pre-generated views rather than the tables directly.", "example": "An attacker uses a travel booking system that leverages SOAP communication between the client and the travel booking service. An attacker begins to tamper with the outgoing SOAP messages by modifying their parameters to include characters that would break a dynamically constructed SQL query. He notices that the system fails to respond when these malicious inputs are injected in certain parameters transferred in a SOAP message. The attacker crafts a SQL query that modifies his payment amount in the travel system's database and passes it as one of the parameters . A backend batch payment system later fetches the payment amount from the database (the modified payment amount) and sends to the credit card processor, enabling the attacker to purchase the airfare at a lower price. An attacker needs to have some knowledge of the system's database, perhaps by exploiting another weakness that results in information disclosure.", @@ -131,7 +131,7 @@ "details": "An attacker targets a system that uses JavaScript Object Notation (JSON) as a transport mechanism between the client and the server (common in Web 2.0 systems using AJAX) to steal possibly confidential information transmitted from the server back to the client inside the JSON object by taking advantage of the loophole in the browser's Same Origin Policy that does not prohibit JavaScript from one website to be included and executed in the context of another website. An attacker gets the victim to visit his or her malicious page that contains a script tag whose source points to the vulnerable system with a URL that requests a response from the server containing a JSON object with possibly confidential information. The malicious page also contains malicious code to capture the JSON object returned by the server before any other processing on it can take place, typically by overriding the JavaScript function used to create new objects. This hook allows the malicious code to get access to the creation of each object and transmit the possibly sensitive contents of the captured JSON object to the attackers' server. There is nothing in the browser's security model to prevent the attackers' malicious JavaScript code (originating from attacker's domain) to set up an environment (as described above) to intercept a JSON object response (coming from the vulnerable target system's domain), read its contents and transmit to the attackers' controlled site. The same origin policy protects the domain object model (DOM), but not the JSON.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.implementsNonce is False and any(d.format == 'JSON' for d in target.data)", + "condition": "target.controls.implementsNonce is False and any(d.format == 'JSON' for d in target.data)", "prerequisites": "JSON is used as a transport mechanism between the client and the serverThe target server cannot differentiate real requests from forged requestsThe JSON object returned from the server can be accessed by the attackers' malicious code via a script tag", "mitigations": "Ensure that server side code can differentiate between legitimate requests and forged requests. The solution is similar to protection against Cross Site Request Forger (CSRF), which is to use a hard to guess random nonce (that is unique to the victim's session with the server) that the attacker has no way of knowing (at least in the absence of other weaknesses). Each request from the client to the server should contain this nonce and the server should reject all requests that do not contain the nonce. On the client side, the system's design could make it difficult to get access to the JSON object content via the script tag. Since the JSON object is never assigned locally to a variable, it cannot be readily modified by the attacker before being used by a script tag. For instance, if while(1) was added to the beginning of the JavaScript returned by the server, trying to access it with a script tag would result in an infinite loop. On the other hand, legitimate client side code can remove the while(1) statement after which the JavaScript can be evaluated. A similar result can be achieved by surrounding the returned JavaScript with comment tags, or using other similar techniques (e.g. wrapping the JavaScript with HTML tags). Make the URLs in the system used to retrieve JSON objects unpredictable and unique for each user session. 4. Ensure that to the extent possible, no sensitive data is passed from the server to the client via JSON objects. JavaScript was never intended to play that role, hence the same origin policy does not adequate address this scenario.", "example": "Gmail service was found to be vulnerable to a JSON Hijacking attack that enabled an attacker to get the contents of the victim's address book. An attacker could send an e-mail to the victim's Gmail account (which ensures that the victim is logged in to Gmail when he or she receives it) with a link to the attackers' malicious site. If the victim clicked on the link, a request (containing the victim's authenticated session cookie) would be sent to the Gmail servers to fetch the victim's address book. This functionality is typically used by the Gmail service to get this data on the fly so that the user can be provided a list of contacts from which to choose the recipient of the e-mail. When the JSON object with the contacts came back, it was loaded into the JavaScript space via a script tag on the attackers' malicious page. Since the JSON object was never assigned to a local variable (which would have prevented a script from a different domain accessing it due to the browser's same origin policy), another mechanism was needed to access the data that it contained. That mechanism was overwriting the internal array constructor with the attackers' own constructor in order to gain access to the JSON object's contents. These contents could then be transferred to the site controlled by the attacker.", @@ -147,7 +147,7 @@ "details": "An adversary manipulates the use or processing of an Application Programming Interface (API) resulting in an adverse impact upon the security of the system implementing the API. This can allow the adversary to execute functionality not intended by the API implementation, possibly compromising the system which integrates the API. API manipulation can take on a number of forms including forcing the unexpected use of an API, or the use of an API in an unintended way. For example, an adversary may make a request to an application that leverages a non-standard API that is known to incorrectly validate its data and thus it may be manipulated by supplying metacharacters or alternate encodings as input, resulting in any number of injection flaws, including SQL injection, cross-site scripting, or command execution. Another example could be API methods that should be disabled in a production application but were not, thus exposing dangerous functionality within a production environment.", "Likelihood Of Attack": "Medium", "severity": "Medium", - "condition": "target.implementsAPI is True and (target.validatesInput is False or target.sanitizesInput is False)", + "condition": "target.implementsAPI is True and (target.controls.validatesInput is False or target.controls.sanitizesInput is False)", "prerequisites": "The target system must expose API functionality in a manner that can be discovered and manipulated by an adversary. This may require reverse engineering the API syntax or decrypting/de-obfuscating client-server exchanges.", "mitigations": "Always use HTTPS and SSL Certificates. Firewall optimizations to prevent unauthorized access to or from a private network. Use strong authentication and authorization mechanisms. A proven protocol is OAuth 2.0, which enables a third-party application to obtain limited access to an API. Use IP whitelisting and rate limiting.", "example": "Since APIs can be accessed over the internet just like any other URI with some sensitive data attached to the request, they share the vulnerabilities of any other resource accessible on the internet like Man-in-the-middle, CSRF Attack, Denial of Services, etc.", @@ -163,7 +163,7 @@ "details": "An attacker obtains unauthorized access to an application, service or device either through knowledge of the inherent weaknesses of an authentication mechanism, or by exploiting a flaw in the authentication scheme's implementation. In such an attack an authentication mechanism is functioning but a carefully controlled sequence of events causes the mechanism to grant access to the attacker. This attack may exploit assumptions made by the target's authentication procedures, such as assumptions regarding trust relationships or assumptions regarding the generation of secret values. This attack differs from Authentication Bypass attacks in that Authentication Abuse allows the attacker to be certified as a valid user through illegitimate means, while Authentication Bypass allows the user to access protected material without ever being certified as an authenticated user. This attack does not rely on prior sessions established by successfully authenticating users, as relied upon for the Exploitation of Session Variables, Resource IDs and other Trusted Credentials attack patterns.", "Likelihood Of Attack": "", "severity": "Medium", - "condition": "target.authenticatesSource is False", + "condition": "target.controls.authenticatesSource is False", "prerequisites": "An authentication mechanism or subsystem implementing some form of authentication such as passwords, digest authentication, security certificates, etc. which is flawed in some way.", "mitigations": "Use strong authentication and authorization mechanisms. A proven protocol is OAuth 2.0, which enables a third-party application to obtain limited access to an API.", "example": "An adversary that has previously obtained unauthorized access to certain device resources, uses that access to obtain information such as location and network information.", @@ -178,7 +178,7 @@ "details": "An adversary actively probes the target in a manner that is designed to solicit information that could be leveraged for malicious purposes. This is achieved by exploring the target via ordinary interactions for the purpose of gathering intelligence about the target, or by sending data that is syntactically invalid or non-standard in an attempt to produce a response that contains the desired data. As a result of these interactions, the adversary is able to obtain information from the target that aids the attacker in making inferences about its security, configuration, or potential vulnerabilities. Examplar exchanges with the target may trigger unhandled exceptions or verbose error messages that reveal information like stack traces, configuration information, path information, or database design. This type of attack also includes the manipulation of query strings in a URI to produce invalid SQL queries, or by trying alternative path values in the hope that the server will return useful information.", "Likelihood Of Attack": "High", "severity": "Medium", - "condition": "(target.sanitizesInput is False or target.validatesInput is False) or target.encodesOutput is False", + "condition": "(target.controls.sanitizesInput is False or target.controls.validatesInput is False) or target.controls.encodesOutput is False", "prerequisites": "An adversary requires some way of interacting with the system.", "mitigations": "Minimize error/response output to only what is necessary for functional use or corrective language. Remove potentially sensitive information that is not necessary for the application's functionality.", "example": "The adversary may collect this information through a variety of methods including active querying as well as passive observation. By exploiting weaknesses in the design or configuration of the target and its communications, an adversary is able to get the target to reveal more information than intended. Information retrieved may aid the adversary in making inferences about potential weaknesses, vulnerabilities, or techniques that assist the adversary's objectives. This information may include details regarding the configuration or capabilities of the target, clues as to the timing or nature of activities, or otherwise sensitive information. Often this sort of attack is undertaken in preparation for some other type of attack, although the collection of information by itself may in some cases be the end goal of the adversary.", @@ -193,7 +193,7 @@ "details": "An adversary monitors data streams to or from the target for information gathering purposes. This attack may be undertaken to solely gather sensitive information or to support a further attack against the target. This attack pattern can involve sniffing network traffic as well as other types of data streams (e.g. radio). The adversary can attempt to initiate the establishment of a data stream, influence the nature of the data transmitted, or passively observe the communications as they unfold. In all variants of this attack, the adversary is not the intended recipient of the data stream. In contrast to other means of gathering information (e.g., targeting data leaks), the adversary must actively position himself so as to observe explicit data channels (e.g. network traffic) and read the content.", "Likelihood Of Attack": "Medium", "severity": "Medium", - "condition": "not target.isEncrypted or (target.source.inScope and not target.isResponse and (not target.authenticatesDestination or not target.checksDestinationRevocation)) or target.tlsVersion < target.sink.minTLSVersion", + "condition": "not target.controls.isEncrypted or (target.source.inScope and not target.isResponse and (not target.controls.authenticatesDestination or not target.checksDestinationRevocation)) or target.tlsVersion < target.sink.minTLSVersion", "prerequisites": "The target must transmit data over a medium that is accessible to the adversary.", "mitigations": "Leverage encryption to encode the transmission of data thus making it accessible only to authorized parties.", "example": "Adversary tries to block, manipulate, and steal communications in an attempt to achieve a desired negative technical impact.", @@ -209,7 +209,7 @@ "details": "The adversary utilizes a repeating of the encoding process for a set of characters (that is, character encoding a character encoding of a character) to obfuscate the payload of a particular request. This may allow the adversary to bypass filters that attempt to detect illegal characters or strings, such as those that might be used in traversal or injection attacks. Filters may be able to catch illegal encoded strings, but may not catch doubly encoded strings. For example, a dot (.), often used in path traversal attacks and therefore often blocked by filters, could be URL encoded as %2E. However, many filters recognize this encoding and would still block the request. In a double encoding, the % in the above URL encoding would be encoded again as %25, resulting in %252E which some filters might not catch, but which could still be interpreted as a dot (.) by interpreters on the target.", "Likelihood Of Attack": "Low", "severity": "Medium", - "condition": "target.validatesInput is False or target.sanitizesInput is False", + "condition": "target.controls.validatesInput is False or target.controls.sanitizesInput is False", "prerequisites": "The target's filters must fail to detect that a character has been doubly encoded but its interpreting engine must still be able to convert a doubly encoded character to an un-encoded character.The application accepts and decodes URL string request.The application performs insufficient filtering/canonicalization on the URLs.", "mitigations": "Assume all input is malicious. Create a white list that defines all valid input to the software system based on the requirements specifications. Input that does not match against the white list should not be permitted to enter into the system. Test your decoding process against malicious input. Be aware of the threat of alternative method of data encoding and obfuscation technique such as IP address encoding. When client input is required from web-based forms, avoid using the GET method to submit data, as the method causes the form data to be appended to the URL and is easily manipulated. Instead, use the POST method whenever possible. Any security checks should occur after the data has been decoded and validated as correct data format. Do not repeat decoding process, if bad character are left after decoding process, treat the data as suspicious, and fail the validation process.Refer to the RFCs to safely decode URL. Regular expression can be used to match safe URL patterns. However, that may discard valid URL requests if the regular expression is too restrictive. There are tools to scan HTTP requests to the server for valid URL such as URLScan from Microsoft (http://www.microsoft.com/technet/security/tools/urlscan.mspx).", "example": "Double Enconding Attacks can often be used to bypass Cross Site Scripting (XSS) detection and execute XSS attacks. The use of double encouding prevents the filter from working as intended and allows the XSS to bypass dectection. This can allow an adversary to execute malicious code.", @@ -242,7 +242,7 @@ "details": "An adversary is able to exploit features of the target that should be reserved for privileged users or administrators but are exposed to use by lower or non-privileged accounts. Access to sensitive information and functionality must be controlled to ensure that only authorized users are able to access these resources. If access control mechanisms are absent or misconfigured, a user may be able to access resources that are intended only for higher level users. An adversary may be able to exploit this to utilize a less trusted account to gain information and perform activities reserved for more trusted accounts. This attack differs from privilege escalation and other privilege stealing attacks in that the adversary never actually escalates their privileges but instead is able to use a lesser degree of privilege to access resources that should be (but are not) reserved for higher privilege accounts. Likewise, the adversary does not exploit trust or subvert systems - all control functionality is working as configured but the configuration does not adequately protect sensitive resources at an appropriate level.", "Likelihood Of Attack": "", "severity": "Medium", - "condition": "target.hasAccessControl is False or target.authorizesSource is False", + "condition": "target.controls.hasAccessControl is False or target.controls.authorizesSource is False", "prerequisites": "The target must have misconfigured their access control mechanisms such that sensitive information, which should only be accessible to more trusted users, remains accessible to less trusted users.The adversary must have access to the target, albeit with an account that is less privileged than would be appropriate for the targeted resources.", "mitigations": "Use strong authentication and authorization mechanisms. A proven protocol is OAuth 2.0, which enables a third-party application to obtain limited access to an API.", "example": "An adversary that has previously obtained unauthorized access to certain device resources, uses that access to obtain information such as location and network information.", @@ -257,7 +257,7 @@ "details": "An adversary manipulates an application's interaction with a buffer in an attempt to read or modify data they shouldn't have access to. Buffer attacks are distinguished in that it is the buffer space itself that is the target of the attack rather than any code responsible for interpreting the content of the buffer. In virtually all buffer attacks the content that is placed in the buffer is immaterial. Instead, most buffer attacks involve retrieving or providing more input than can be stored in the allocated buffer, resulting in the reading or overwriting of other unintended program memory.", "Likelihood Of Attack": "High", "severity": "Very High", - "condition": "target.usesSecureFunctions is False", + "condition": "target.controls.usesSecureFunctions is False", "prerequisites": "The adversary must identify a programmatic means for interacting with a buffer, such as vulnerable C code, and be able to provide input to this interaction.", "mitigations": "To help protect an application from buffer manipulation attacks, a number of potential mitigations can be leveraged. Before starting the development of the application, consider using a code language (e.g., Java) or compiler that limits the ability of developers to act beyond the bounds of a buffer. If the chosen language is susceptible to buffer related issues (e.g., C) then consider using secure functions instead of those vulnerable to buffer manipulations. If a potentially dangerous function must be used, make sure that proper boundary checking is performed. Additionally, there are often a number of compiler-based mechanisms (e.g., StackGuard, ProPolice and the Microsoft Visual Studio /GS flag) that can help identify and protect against potential buffer issues. Finally, there may be operating system level preventative functionality that can be applied.", "example": "Attacker identifies programmatic means for interacting with a buffer, such as vulnerable C code, and is able to provide input to this interaction.", @@ -288,7 +288,7 @@ "details": "An adversary consumes the resources of a target by rapidly engaging in a large number of interactions with the target. This type of attack generally exposes a weakness in rate limiting or flow. When successful this attack prevents legitimate users from accessing the service and can cause the target to crash. This attack differs from resource depletion through leaks or allocations in that the latter attacks do not rely on the volume of requests made to the target but instead focus on manipulation of the target's operations. The key factor in a flooding attack is the number of requests the adversary can make in a given period of time. The greater this number, the more likely an attack is to succeed against a given target.", "Likelihood Of Attack": "High", "severity": "Medium", - "condition": "target.handlesResourceConsumption is False or target.isResilient is False", + "condition": "target.controls.handlesResourceConsumption is False or target.controls.isResilient is False", "prerequisites": "Any target that services requests is vulnerable to this attack on some level of scale.", "mitigations": "Ensure that protocols have specific limits of scale configured. Specify expectations for capabilities and dictate which behaviors are acceptable when resource allocation reaches limits. Uniformly throttle all requests in order to make it more difficult to consume resources more quickly than they can again be freed.", "example": "Adversary tries to bring a network or service down by flooding it with large amounts of traffic.", @@ -303,7 +303,7 @@ "details": "An adversary uses path manipulation methods to exploit insufficient input validation of a target to obtain access to data that should be not be retrievable by ordinary well-formed requests. A typical variety of this attack involves specifying a path to a desired file together with dot-dot-slash characters, resulting in the file access API or function traversing out of the intended directory structure and into the root file system. By replacing or modifying the expected path information the access function or API retrieves the file desired by the attacker. These attacks either involve the attacker providing a complete path to a targeted file or using control characters (e.g. path separators (/ or ) and/or dots (.)) to reach desired directories or files.", "Likelihood Of Attack": "High", "severity": "Very High", - "condition": "target.validatesInput is False and target.sanitizesInput is False", + "condition": "target.controls.validatesInput is False and target.controls.sanitizesInput is False", "prerequisites": "The attacker must be able to control the path that is requested of the target.The target must fail to adequately sanitize incoming paths", "mitigations": "Design: Configure the access control correctly. Design: Enforce principle of least privilege. Design: Execute programs with constrained privileges, so parent process does not open up further vulnerabilities. Ensure that all directories, temporary directories and files, and memory are executing with limited privileges to protect against remote execution. Design: Input validation. Assume that user inputs are malicious. Utilize strict type, character, and encoding enforcement. Design: Proxy communication to host, so that communications are terminated at the proxy, sanitizing the requests before forwarding to server host. 6. Design: Run server interfaces with a non-root account and/or utilize chroot jails or other configuration techniques to constrain privileges even if attacker gains some limited access to commands. Implementation: Host integrity monitoring for critical files, directories, and processes. The goal of host integrity monitoring is to be aware when a security issue has occurred so that incident response and other forensic activities can begin. Implementation: Perform input validation for all remote content, including remote and user-generated content. Implementation: Perform testing such as pen-testing and vulnerability scanning to identify directories, programs, and interfaces that grant direct access to executables. Implementation: Use indirect references rather than actual file names. Implementation: Use possible permissions on file access when developing and deploying web applications. Implementation: Validate user input by only accepting known good. Ensure all content that is delivered to client is sanitized against an acceptable content specification -- whitelisting approach.", "example": "An example of using path traversal to attack some set of resources on a web server is to use a standard HTTP request http://example/../../../../../etc/passwd From an attacker point of view, this may be sufficient to gain access to the password file on a poorly protected system. If the attacker can list directories of critical resources then read only access is not sufficient to protect the system.", @@ -319,7 +319,7 @@ "details": "The attacker directly or indirectly modifies environment variables used by or controlling the target software. The attacker's goal is to cause the target software to deviate from its expected operation in a manner that benefits the attacker.", "Likelihood Of Attack": "High", "severity": "Very High", - "condition": "target.usesEnvironmentVariables is True and (target.implementsAuthenticationScheme is False or target.validatesInput is False or target.authorizesSource is False)", + "condition": "target.usesEnvironmentVariables is True and (target.controls.implementsAuthenticationScheme is False or target.controls.validatesInput is False or target.controls.authorizesSource is False)", "prerequisites": "An environment variable is accessible to the user.An environment variable used by the application can be tainted with user supplied data.Input data used in an environment variable is not validated properly.The variables encapsulation is not done properly. For instance setting a variable as public in a class makes it visible and an attacker may attempt to manipulate that variable.", "mitigations": "Protect environment variables against unauthorized read and write access. Protect the configuration files which contain environment variables against illegitimate read and write access. Assume all input is malicious. Create a white list that defines all valid input to the software system based on the requirements specifications. Input that does not match against the white list should not be permitted to enter into the system. Apply the least privilege principles. If a process has no legitimate reason to read an environment variable do not give that privilege.", "example": "Changing the LD_LIBRARY_PATH environment variable in TELNET will cause TELNET to use an alternate (possibly Trojan) version of a function library. The Trojan library must be accessible using the target file system and should include Trojan code that will allow the user to log in with a bad password. This requires that the attacker upload the Trojan library to a specific location on the target. As an alternative to uploading a Trojan file, some file systems support file paths that include remote addresses, such as 172.16.2.100shared_filestrojan_dll.dll. See also: Path Manipulation (CVE-1999-0073). The HISTCONTROL environment variable keeps track of what should be saved by the history command and eventually into the ~/.bash_history file when a user logs out. This setting can be configured to ignore commands that start with a space by simply setting it to ignorespace. HISTCONTROL can also be set to ignore duplicate commands by setting it to ignoredups. In some Linux systems, this is set by default to ignoreboth which covers both of the previous examples. This means that “ ls” will not be saved, but “ls” would be saved by history. HISTCONTROL does not exist by default on macOS, but can be set by the user and will be respected. Adversaries can use this to operate without leaving traces by simply prepending a space to all of their terminal commands.", @@ -337,7 +337,7 @@ "details": "An adversary causes the target to allocate excessive resources to servicing the attackers' request, thereby reducing the resources available for legitimate services and degrading or denying services. Usually, this attack focuses on memory allocation, but any finite resource on the target could be the attacked, including bandwidth, processing cycles, or other resources. This attack does not attempt to force this allocation through a large number of requests (that would be Resource Depletion through Flooding) but instead uses one or a small number of requests that are carefully formatted to force the target to allocate excessive resources to service this request(s). Often this attack takes advantage of a bug in the target to cause the target to allocate resources vastly beyond what would be needed for a normal request.", "Likelihood Of Attack": "Medium", "severity": "Medium", - "condition": "target.handlesResourceConsumption is False", + "condition": "target.controls.handlesResourceConsumption is False", "prerequisites": "The target must accept service requests from the attacker and the adversary must be able to control the resource allocation associated with this request to be in excess of the normal allocation. The latter is usually accomplished through the presence of a bug on the target that allows the adversary to manipulate variables used in the allocation.", "mitigations": "Limit the amount of resources that are accessible to unprivileged users. Assume all input is malicious. Consider all potentially relevant properties when validating input. Consider uniformly throttling all requests in order to make it more difficult to consume resources more quickly than they can again be freed. Use resource-limiting settings, if possible.", "example": "In an Integer Attack, the adversary could cause a variable that controls allocation for a request to hold an excessively large value. Excessive allocation of resources can render a service degraded or unavailable to legitimate users and can even lead to crashing of the target.", @@ -370,7 +370,7 @@ "details": "An adversary includes formatting characters in a string input field on the target application. Most applications assume that users will provide static text and may respond unpredictably to the presence of formatting character. For example, in certain functions of the C programming languages such as printf, the formatting character %s will print the contents of a memory location expecting this location to identify a string and the formatting character %n prints the number of DWORD written in the memory. An adversary can use this to read or write to memory locations or files, or simply to manipulate the value of the resulting text in unexpected ways. Reading or writing memory may result in program crashes and writing memory could result in the execution of arbitrary code if the adversary can write to the program stack.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.validatesInput is False or target.sanitizesInput is False", + "condition": "target.controls.validatesInput is False or target.controls.sanitizesInput is False", "prerequisites": "The target application must accept a strings as user input, fail to sanitize string formatting characters in the user input, and process this string using functions that interpret string formatting characters.", "mitigations": "Limit the usage of formatting string functions. Strong input validation - All user-controllable input must be validated and filtered for illegal formatting characters.", "example": "Untrusted search path vulnerability in the add_filename_to_string function in intl/gettext/loadmsgcat.c for Elinks 0.11.1 allows local users to cause Elinks to use an untrusted gettext message catalog (.po file) in a ../po directory, which can be leveraged to conduct format string attacks.", @@ -385,7 +385,7 @@ "details": "An attacker manipulates or crafts an LDAP query for the purpose of undermining the security of the target. Some applications use user input to create LDAP queries that are processed by an LDAP server. For example, a user might provide their username during authentication and the username might be inserted in an LDAP query during the authentication process. An attacker could use this input to inject additional commands into an LDAP query that could disclose sensitive information. For example, entering a * in the aforementioned query might return information about all users on the system. This attack is very similar to an SQL injection attack in that it manipulates a query to gather additional information or coerce a particular return value.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.validatesInput is False", + "condition": "target.controls.validatesInput is False", "prerequisites": "The target application must accept a string as user input, fail to sanitize characters that have a special meaning in LDAP queries in the user input, and insert the user-supplied string in an LDAP query which is then processed.", "mitigations": "Strong input validation - All user-controllable input must be validated and filtered for illegal characters as well as LDAP content. Use of custom error pages - Attackers can glean information about the nature of queries from descriptive error messages. Input validation must be coupled with customized error pages that inform about an error without disclosing information about the LDAP or application.", "example": "PowerDNS before 2.9.18, when running with an LDAP backend, does not properly escape LDAP queries, which allows remote attackers to cause a denial of service (failure to answer ldap questions) and possibly conduct an LDAP injection attack.", @@ -400,7 +400,7 @@ "details": "An adversary manipulates the content of request parameters for the purpose of undermining the security of the target. Some parameter encodings use text characters as separators. For example, parameters in a HTTP GET message are encoded as name-value pairs separated by an ampersand (&). If an attacker can supply text strings that are used to fill in these parameters, then they can inject special characters used in the encoding scheme to add or modify parameters. For example, if user input is fed directly into an HTTP GET request and the user provides the value myInput&new_param=myValue, then the input parameter is set to myInput, but a new parameter (new_param) is also added with a value of myValue. This can significantly change the meaning of the query that is processed by the server. Any encoding scheme where parameters are identified and separated by text characters is potentially vulnerable to this attack - the HTTP GET encoding used above is just one example.", "Likelihood Of Attack": "Medium", "severity": "Medium", - "condition": "target.validatesInput is False", + "condition": "target.controls.validatesInput is False", "prerequisites": "The target application must use a parameter encoding where separators and parameter identifiers are expressed in regular text.The target application must accept a string as user input, fail to sanitize characters that have a special meaning in the parameter encoding, and insert the user-supplied string in an encoding which is then processed.", "mitigations": "Implement an audit log written to a separate host. In the event of a compromise, the audit log may be able to provide evidence and details of the compromise. Treat all user input as untrusted data that must be validated before use.", "example": "The target application accepts a string as user input, fails to sanitize characters that have a special meaning in the parameter encoding, and inserts the user-supplied string in an encoding which is then processed.", @@ -415,7 +415,7 @@ "details": "An attacker exploits a weakness in input validation on the target by supplying a specially constructed path utilizing dot and slash characters for the purpose of obtaining access to arbitrary files or resources. An attacker modifies a known path on the target in order to reach material that is not available through intended channels. These attacks normally involve adding additional path separators (/ or ) and/or dots (.), or encodings thereof, in various combinations in order to reach parent directories or entirely separate trees of the target's directory structure.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.validatesInput is False or target.sanitizesInput is False", + "condition": "target.controls.validatesInput is False or target.controls.sanitizesInput is False", "prerequisites": "The target application must accept a string as user input, fail to sanitize combinations of characters in the input that have a special meaning in the context of path navigation, and insert the user-supplied string into path navigation commands.", "mitigations": "Design: Input validation. Assume that user inputs are malicious. Utilize strict type, character, and encoding enforcement. Implementation: Perform input validation for all remote content, including remote and user-generated content. Implementation: Validate user input by only accepting known good. Ensure all content that is delivered to client is sanitized against an acceptable content specification -- whitelisting approach. Implementation: Prefer working without user input when using file system calls. Implementation: Use indirect references rather than actual file names. Implementation: Use possible permissions on file access when developing and deploying web applications.", "example": "The attacker uses relative path traversal to access files in the application. This is an example of accessing user's password file. http://www.example.com/getProfile.jsp?filename=../../../../etc/passwd However, the target application employs regular expressions to make sure no relative path sequences are being passed through the application to the web page. The application would replace all matches from this regex with the empty string. Then an attacker creates special payloads to bypass this filter: http://www.example.com/getProfile.jsp?filename=%2e%2e/%2e%2e/%2e%2e/%2e%2e /etc/passwd When the application gets this input string, it will be the desired vector by the attacker.", @@ -431,7 +431,7 @@ "details": "This type of attack exploits a buffer overflow vulnerability in targeted client software through injection of malicious content from a custom-built hostile service.", "Likelihood Of Attack": "Medium", "severity": "High", - "condition": "target.checksInputBounds is False and target.validatesInput is False", + "condition": "target.controls.checksInputBounds is False and target.controls.validatesInput is False", "prerequisites": "The targeted client software communicates with an external server.The targeted client software has a buffer overflow vulnerability.", "mitigations": "The client software should not install untrusted code from a non-authenticated server. The client software should have the latest patches and should be audited for vulnerabilities before being used to communicate with potentially hostile servers. Perform input validation for length of buffer inputs. Use a language or compiler that performs automatic bounds checking. Use an abstraction library to abstract away risky APIs. Not a complete solution. Compiler-based canary mechanisms such as StackGuard, ProPolice and the Microsoft Visual Studio /GS flag. Unless this provides automatic bounds checking, it is not a complete solution. Ensure all buffer uses are consistently bounds-checked. Use OS-level preventative functionality. Not a complete solution.", "example": "Attack Example: Buffer Overflow in Internet Explorer 4.0 Via EMBED Tag Authors often use EMBED tags in HTML documents. For example ]> John Smith 555-1234 jsmith@email.com
    1 Example Lane
    If the 'name' attribute is required in all submitted documents and this field is removed by the adversary, the application may enter an unexpected state or record incomplete data. Additionally, if this data is needed to perform additional functions, a Denial of Service (DOS) may occur.XML Schema Poisoning Attacks can also be executed remotely if the HTTP protocol is being used to transport data. : John Smith 555-1234 jsmith@email.com
    1 Example Lane
    The HTTP protocol does not encrypt the traffic it transports, so all communication occurs in plaintext. This traffic can be observed and modified by the adversary during transit to alter the XML schema before it reaches the end user. The adversary can perform a Man-in-the-Middle (MITM) Attack to alter the schema in the same way as the previous example and to acheive the same results.", @@ -476,7 +476,7 @@ "details": "An adversary modifies content to make it contain something other than what the original content producer intended while keeping the apparent source of the content unchanged. The term content spoofing is most often used to describe modification of web pages hosted by a target to display the adversary's content instead of the owner's content. However, any content can be spoofed, including the content of email messages, file transfers, or the content of other network communication protocols. Content can be modified at the source (e.g. modifying the source file for a web page) or in transit (e.g. intercepting and modifying a message between the sender and recipient). Usually, the adversary will attempt to hide the fact that the content has been modified, but in some cases, such as with web site defacement, this is not necessary. Content Spoofing can lead to malware exposure, financial fraud (if the content governs financial transactions), privacy violations, and other unwanted outcomes.", "Likelihood Of Attack": "Medium", "severity": "Medium", - "condition": "((not target.source.providesIntegrity or not target.sink.providesIntegrity) and not target.isEncrypted) or (target.source.inScope and not target.isResponse and (not target.authenticatesDestination or not target.checksDestinationRevocation))", + "condition": "((not target.source.controls.providesIntegrity or not target.sink.controls.providesIntegrity) and not target.controls.isEncrypted) or (target.source.inScope and not target.isResponse and (not target.controls.authenticatesDestination or not target.checksDestinationRevocation))", "prerequisites": "The target must provide content but fail to adequately protect it against modification.The adversary must have the means to alter data to which he/she is not authorized.If the content is to be modified in transit, the adversary must be able to intercept the targeted messages.", "mitigations": "Validation of user input for type, length, data-range, format, etc. Encoding any user input that will be output by the web application.", "example": "An attacker finds a site which is vulnerable to HTML Injection. He sends a URL with malicious code injected in the URL to the user of the website(victim) via email or some other social networking site. User visits the page because the page is located within trusted domain. When the victim accesses the page, the injected HTML code is rendered and presented to the user asking for username and password. The username and password are both sent to the attacker's server.", @@ -492,7 +492,7 @@ "details": "An attack of this type exploits a programs' vulnerabilities that allows an attacker's commands to be concatenated onto a legitimate command with the intent of targeting other resources such as the file system or database. The system that uses a filter or a blacklist input validation, as opposed to whitelist validation is vulnerable to an attacker who predicts delimiters (or combinations of delimiters) not present in the filter or blacklist. As with other injection attacks, the attacker uses the command delimiter payload as an entry point to tunnel through the application and activate additional attacks through SQL queries, shell commands, network scanning, and so on.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.validatesInput is False", + "condition": "target.controls.validatesInput is False", "prerequisites": "Software's input validation or filtering must not detect and block presence of additional malicious command.", "mitigations": "Design: Perform whitelist validation against a positive specification for command length, type, and parameters.Design: Limit program privileges, so if commands circumvent program input validation or filter routines then commands do not running under a privileged accountImplementation: Perform input validation for all remote content.Implementation: Use type conversions such as JDBC prepared statements.", "example": "By appending special characters, such as a semicolon or other commands that are executed by the target process, the attacker is able to execute a wide variety of malicious commands in the target process space, utilizing the target's inherited permissions, against any resource the host has access to. The possibilities are vast including injection attacks against RDBMS (SQL Injection), directory servers (LDAP Injection), XML documents (XPath and XQuery Injection), and command line shells. In many injection attacks, the results are converted back to strings and displayed to the client process such as a web browser without tripping any security alarms, so the network firewall does not log any out of the ordinary behavior. LDAP servers house critical identity assets such as user, profile, password, and group information that is used to authenticate and authorize users. An attacker that can query the directory at will and execute custom commands against the directory server is literally working with the keys to the kingdom in many enterprises. When user, organizational units, and other directory objects are queried by building the query string directly from user input with no validation, or other conversion, then the attacker has the ability to use any LDAP commands to query, filter, list, and crawl against the LDAP server directly in the same manner as SQL injection gives the ability to the attacker to run SQL commands on the database.", @@ -509,7 +509,7 @@ "details": "An attacker exploits a weakness in input validation by controlling the format, structure, and composition of data to an input-processing interface. By supplying input of a non-standard or unexpected form an attacker can adversely impact the security of the target. For example, using a different character encoding might cause dangerous text to be treated as safe text. Alternatively, the attacker may use certain flags, such as file extensions, to make a target application believe that provided data should be handled using a certain interpreter when the data is not actually of the appropriate type. This can lead to bypassing protection mechanisms, forcing the target to use specific components for input processing, or otherwise causing the user's data to be handled differently than might otherwise be expected. This attack differs from Variable Manipulation in that Variable Manipulation attempts to subvert the target's processing through the value of the input while Input Data Manipulation seeks to control how the input is processed.", "Likelihood Of Attack": "", "severity": "Medium", - "condition": "target.validatesInput is False", + "condition": "target.controls.validatesInput is False", "prerequisites": "The target must accept user data for processing and the manner in which this data is processed must depend on some aspect of the format or flags that the attacker can control.", "mitigations": "Validation of user input for type, length, data-range, format, etc.", "example": "A target application has an integer variable for which only some integer values are expected by the application. But since it does not have any checks in place to validate the value of the input, the attacker is able to manipulate the targeted integer variable such that normal operations result in non-standard values.", @@ -524,7 +524,7 @@ "details": "In this attack pattern, the adversary intercepts information transmitted between two third parties. The adversary must be able to observe, read, and/or hear the communication traffic, but not necessarily block the communication or change its content. The adversary may precipitate or indirectly influence the content of the observed transaction, but is never the intended recipient of the information. Any transmission medium can theoretically be sniffed if the adversary can examine the contents between the sender and recipient.", "Likelihood Of Attack": "", "severity": "Medium", - "condition": "(target.protocol == 'HTTP' or target.isEncrypted is False) or target.usesVPN is False", + "condition": "(target.protocol == 'HTTP' or target.controls.isEncrypted is False) or target.usesVPN is False", "prerequisites": "The target data stream must be transmitted on a medium to which the adversary has access.", "mitigations": "Encrypt sensitive information when transmitted on insecure mediums to prevent interception.", "example": "Attacker knows that the computer/OS/application can request new applications to install, or it periodically checks for an available update. The attacker loads the sniffer set up during Explore phase, and extracts the application code from subsequent communication. The attacker then proceeds to reverse engineer the captured code.", @@ -540,7 +540,7 @@ "details": "An attacker tries each of the words in a dictionary as passwords to gain access to the system via some user's account. If the password chosen by the user was a word within the dictionary, this attack will be successful (in the absence of other mitigations). This is a specific instance of the password brute forcing attack pattern.", "Likelihood Of Attack": "Medium", "severity": "High", - "condition": "target.implementsAuthenticationScheme is False", + "condition": "target.controls.implementsAuthenticationScheme is False", "prerequisites": "The system uses one factor password based authentication.The system does not have a sound password policy that is being enforced.The system does not implement an effective password throttling mechanism.", "mitigations": "Create a strong password policy and ensure that your system enforces this policy.Implement an intelligent password throttling mechanism. Care must be taken to assure that these mechanisms do not excessively enable account lockout attacks such as CAPEC-02.", "example": "A system user selects the word treacherous as their passwords believing that it would be very difficult to guess. The password-based dictionary attack is used to crack this password and gain access to the account.The Cisco LEAP challenge/response authentication mechanism uses passwords in a way that is susceptible to dictionary attacks, which makes it easier for remote attackers to gain privileges via brute force password guessing attacks. Cisco LEAP is a mutual authentication algorithm that supports dynamic derivation of session keys. With Cisco LEAP, mutual authentication relies on a shared secret, the user's logon password (which is known by the client and the network), and is used to respond to challenges between the user and the Remote Authentication Dial-In User Service (RADIUS) server. Methods exist for someone to write a tool to launch an offline dictionary attack on password-based authentications that leverage Microsoft MS-CHAP, such as Cisco LEAP. The tool leverages large password lists to efficiently launch offline dictionary attacks against LEAP user accounts, collected through passive sniffing or active techniques.See also: CVE-2003-1096", @@ -556,7 +556,7 @@ "details": "Some APIs support scripting instructions as arguments. Methods that take scripted instructions (or references to scripted instructions) can be very flexible and powerful. However, if an attacker can specify the script that serves as input to these methods they can gain access to a great deal of functionality. For example, HTML pages support A similar example uses session ID as an argument of the URL. http://www.example.com/index.php/sessionid=0123456789 Once the victim clicks the links, the attacker may be able to bypass authentication or piggy-back off some other authenticated victim's session.", @@ -767,7 +767,7 @@ "details": "An adversary distributes a link (or possibly some other query structure) with a request to a third party web server that is malformed and also contains a block of exploit code in order to have the exploit become live code in the resulting error page. When the third party web server receives the crafted request and notes the error it then creates an error message that echoes the malformed message, including the exploit. Doing this converts the exploit portion of the message into to valid language elements that are executed by the viewing browser. When a victim executes the query provided by the attacker the infected error message error message is returned including the exploit code which then runs in the victim's browser. XSS can result in execution of code as well as data leakage (e.g. session cookies can be sent to the attacker). This type of attack is especially dangerous since the exploit appears to come from the third party web server, who the victim may trust and hence be more vulnerable to deception.", "Likelihood Of Attack": "", "severity": "Medium", - "condition": "target.encodesOutput is False or target.validatesInput is False or target.sanitizesInput is False", + "condition": "target.controls.encodesOutput is False or target.controls.validatesInput is False or target.controls.sanitizesInput is False", "prerequisites": "A third party web server which fails to adequately sanitize messages sent in error pages.The victim must be made to execute a query crafted by the attacker which results in the infected error report.", "mitigations": "Design: Use libraries and templates that minimize unfiltered input.Implementation: Normalize, filter and white list any input that will be used in error messages.Implementation: The victim should configure the browser to minimize active content from untrusted sources.", "example": "A third party web server fails to adequately sanitize messages sent in error pages. Adversary takes advantage of the data displayed in the error message.", @@ -782,7 +782,7 @@ "details": "An adversary uses alternate forms of keywords or commands that result in the same action as the primary form but which may not be caught by filters. For example, many keywords are processed in a case insensitive manner. If the site's web filtering algorithm does not convert all tags into a consistent case before the comparison with forbidden keywords it is possible to bypass filters (e.g., incomplete black lists) by using an alternate case structure. For example, the script tag using the alternate forms of Script or ScRiPt may bypass filters where script is the only form tested. Other variants using different syntax representations are also possible as well as using pollution meta-characters or entities that are eventually ignored by the rendering engine. The attack can result in the execution of otherwise prohibited functionality.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.sanitizesInput is False or target.validatesInput is False or target.encodesOutput is False", + "condition": "target.controls.sanitizesInput is False or target.controls.validatesInput is False or target.controls.encodesOutput is False", "prerequisites": "Target client software must allow scripting such as JavaScript.", "mitigations": "Design: Use browser technologies that do not allow client side scripting.Design: Utilize strict type, character, and encoding enforcementImplementation: Ensure all content that is delivered to client is sanitized against an acceptable content specification.Implementation: Ensure all content coming from the client is using the same encoding; if not, the server-side application must canonicalize the data before applying any filtering.Implementation: Perform input validation for all remote content, including remote and user-generated contentImplementation: Perform output validation for all remote content.Implementation: Disable scripting languages such as JavaScript in browserImplementation: Patching software. There are many attack vectors for XSS on the client side and the server side. Many vulnerabilities are fixed in service packs for browser, web servers, and plug in technologies, staying current on patch release that deal with XSS countermeasures mitigates this.", "example": "In this example, the attacker tries to get a script executed by the victim's browser. The target application employs regular expressions to make sure no script is being passed through the application to the web page; such a regular expression could be ((?i)script), and the application would replace all matches by this regex by the empty string. An attacker will then create a special payload to bypass this filter: alert(1) when the applications gets this input string, it will replace all script (case insensitive) by the empty string and the resulting input will be the desired vector by the attacker. In this example, we assume that the application needs to write a particular string in a client-side JavaScript context (e.g., ). For the attacker to execute the same payload as in the previous example, he would need to send alert(1) if there was no filtering. The application makes use of the following regular expression as filter ((w+)s*(.*)|alert|eval|function|document) and replaces all matches by the empty string. For example each occurrence of alert(), eval(), foo() or even the string alert would be stripped. An attacker will then create a special payload to bypass this filter: this['al' + 'ert'](1) when the applications gets this input string, it won't replace anything and this piece of JavaScript has exactly the same runtime meaning as alert(1). The attacker could also have used non-alphanumeric XSS vectors to bypass the filter; for example, ($=[$=[]][(__=!$+$)[_=-~-~-~$]+({}+$)[_/_]+($$=($_=!''+$)[_/_]+$_[+$])])()[__[_/_]+__[_+~$]+$_[_]+$$](_/_) would be executed by the JavaScript engine like alert(1) is.", @@ -798,7 +798,7 @@ "details": "An attacker, armed with the cipher text and the encryption algorithm used, performs an exhaustive (brute force) search on the key space to determine the key that decrypts the cipher text to obtain the plaintext.", "Likelihood Of Attack": "Low", "severity": "Low", - "condition": "target.usesEncryptionAlgorithm != 'RSA' and target.usesEncryptionAlgorithm != 'AES'", + "condition": "target.controls.usesEncryptionAlgorithm != 'RSA' and target.controls.usesEncryptionAlgorithm != 'AES'", "prerequisites": "Ciphertext is known.Encryption algorithm and key size are known.", "mitigations": "Use commonly accepted algorithms and recommended key sizes. The key size used will depend on how important it is to keep the data confidential and for how long.In theory a brute force attack performing an exhaustive key space search will always succeed, so the goal is to have computational security. Moore's law needs to be taken into account that suggests that computing resources double every eighteen months.", "example": "In 1997 the original DES challenge used distributed net computing to brute force the encryption key and decrypt the ciphertext to obtain the original plaintext. Each machine was given its own section of the key space to cover. The ciphertext was decrypted in 96 days.", @@ -813,7 +813,7 @@ "details": "An adversary exploits a weakness in authorization in order to modify content within a registry (e.g., Windows Registry, Mac plist, application registry). Editing registry information can permit the adversary to hide configuration information or remove indicators of compromise to cover up activity. Many applications utilize registries to store configuration and service information. As such, modification of registry information can affect individual services (affecting billing, authorization, or even allowing for identity spoofing) or the overall configuration of a targeted application. For example, both Java RMI and SOAP use registries to track available services. Changing registry values is sometimes a preliminary step towards completing another attack pattern, but given the long term usage of many registry values, manipulation of registry information could be its own end.", "Likelihood Of Attack": "", "severity": "Medium", - "condition": "target.hasAccessControl is False", + "condition": "target.controls.hasAccessControl is False", "prerequisites": "The targeted application must rely on values stored in a registry.The adversary must have a means of elevating permissions in order to access and modify registry content through either administrator privileges (e.g., credentialed access), or a remote access tool capable of editing a registry through an API.", "mitigations": "Ensure proper permissions are set for Registry hives to prevent users from modifying keys.Employ a robust and layered defensive posture in order to prevent unauthorized users on your system.Employ robust identification and audit/blocking via whitelisting of applications on your system. Unnecessary applications, utilities, and configurations will have a presence in the system registry that can be leveraged by an adversary through this attack pattern.", "example": "Manipulating registration information can be undertaken in advance of a path traversal attack (inserting relative path modifiers) or buffer overflow attack (enlarging a registry value beyond an application's ability to store it).", @@ -842,7 +842,7 @@ "details": "An attacker removes or disables functionality on the client that the server assumes to be present and trustworthy. Attackers can, in some cases, get around logic put in place to 'guard' sensitive functionality or data. Client applications may include functionality that a server relies on for correct and secure operation. This functionality can include, but is not limited to, filters to prevent the sending of dangerous content to the server, logical functionality such as price calculations, and authentication logic to ensure that only authorized users are utilizing the client. If an attacker can disable this functionality on the client, they can perform actions that the server believes are prohibited. This can result in client behavior that violates assumptions by the server leading to a variety of possible attacks. In the above examples, this could include the sending of dangerous content (such as scripts) to the server, incorrect price calculations, or unauthorized access to server resources.", "Likelihood Of Attack": "Medium", "severity": "High", - "condition": "target.providesIntegrity is False or target.usesCodeSigning is False", + "condition": "target.controls.providesIntegrity is False or target.controls.usesCodeSigning is False", "prerequisites": "The targeted server must assume the client performs important actions to protect the server or the server functionality. For example, the server may assume the client filters outbound traffic or that the client performs all price calculations correctly. Moreover, the server must fail to detect when these assumptions are violated by a client.", "mitigations": "Design: For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side.Design: Ship client-side application with integrity checks (code signing) when possible.Design: Use obfuscation and other techniques to prevent reverse engineering the client code.", "example": "Attacker reverse engineers a Java binary (by decompiling it) and identifies where license management code exists. Noticing that the license manager returns TRUE or FALSE as to whether or not the user is licensed, the Attacker simply overwrites both branch targets to return TRUE, recompiles, and finally redeploys the binary.Attacker uses click-through exploration of a Servlet-based website to map out its functionality, taking note of its URL-naming conventions and Servlet mappings. Using this knowledge and guessing the Servlet name of functionality they're not authorized to use, the Attacker directly navigates to the privileged functionality around the authorizing single-front controller (implementing programmatic authorization checks).Attacker reverse-engineers a Java binary (by decompiling it) and identifies where license management code exists. Noticing that the license manager returns TRUE or FALSE as to whether or not the user is licensed, the Attacker simply overwrites both branch targets to return TRUE, recompiles, and finally redeploys the binary.", @@ -857,7 +857,7 @@ "details": "An adversary creates a file with scripting content but where the specified MIME type of the file is such that scripting is not expected. The adversary tricks the victim into accessing a URL that responds with the script file. Some browsers will detect that the specified MIME type of the file does not match the actual type of its content and will automatically switch to using an interpreter for the real content type. If the browser does not invoke script filters before doing this, the adversary's script may run on the target unsanitized, possibly revealing the victim's cookies or executing arbitrary script in their browser.", "Likelihood Of Attack": "Medium", "severity": "Medium", - "condition": "target.validatesContentType is False or target.invokesScriptFilters is False", + "condition": "target.controls.validatesContentType is False or target.controls.invokesScriptFilters is False", "prerequisites": "The victim must follow a crafted link that references a scripting file that is mis-typed as a non-executable file.The victim's browser must detect the true type of a mis-labeled scripting file and invoke the appropriate script interpreter without first performing filtering on the content.", "mitigations": "Design: Browsers must invoke script filters to detect that the specified MIME type of the file matches the actual type of its content before deciding which script interpreter to use.", "example": "For example, the MIME type text/plain may be used where the actual content is text/javascript or text/html. Since text does not contain scripting instructions, the stated MIME type would indicate that filtering is unnecessary. However, if the target application subsequently determines the file's real type and invokes the appropriate interpreter, scripted content could be invoked.In another example, img tags in HTML content could reference a renderable type file instead of an expected image file. The file extension and MIME type can describe an image file, but the file content can be text/javascript or text/html resulting in script execution. If the browser assumes all references in img tags are images, and therefore do not need to be filtered for scripts, this would bypass content filters.", @@ -872,7 +872,7 @@ "details": "Attacks on session IDs and resource IDs take advantage of the fact that some software accepts user input without verifying its authenticity. For example, a message queuing system that allows service requesters to post messages to its queue through an open channel (such as anonymous FTP), authorization is done through checking group or role membership contained in the posted message. However, there is no proof that the message itself, the information in the message (such group or role membership), or indeed the process that wrote the message to the queue are authentic and authorized to do so. Many server side processes are vulnerable to these attacks because the server to server communications have not been analyzed from a security perspective or the processes trust other systems because they are behind a firewall. In a similar way servers that use easy to guess or spoofable schemes for representing digital identity can also be vulnerable. Such systems frequently use schemes without cryptography and digital signatures (or with broken cryptography). Session IDs may be guessed due to insufficient randomness, poor protection (passed in the clear), lack of integrity (unsigned), or improperly correlation with access control policy enforcement points. Exposed configuration and properties files that contain system passwords, database connection strings, and such may also give an attacker an edge to identify these identifiers. The net result is that spoofing and impersonation is possible leading to an attacker's ability to break authentication, authorization, and audit controls on the system.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.providesIntegrity is False or target.authenticatesSource is False or target.usesStrongSessionIdentifiers is False", + "condition": "target.controls.providesIntegrity is False or target.controls.authenticatesSource is False or target.controls.usesStrongSessionIdentifiers is False", "prerequisites": "Server software must rely on weak session IDs proof and/or verification schemes", "mitigations": "Design: utilize strong federated identity such as SAML to encrypt and sign identity tokens in transit.Implementation: Use industry standards session key generation mechanisms that utilize high amount of entropy to generate the session key. Many standard web and application servers will perform this task on your behalf.Implementation: If the session identifier is used for authentication, such as in the so-called single sign on use cases, then ensure that it is protected at the same level of assurance as authentication tokens.Implementation: If the web or application server supports it, then encrypting and/or signing the session ID (such as cookie) can protect the ID if intercepted.Design: Use strong session identifiers that are protected in transit and at rest.Implementation: Utilize a session timeout for all sessions, for example 20 minutes. If the user does not explicitly logout, the server terminates their session after this period of inactivity. If the user logs back in then a new session key is generated.Implementation: Verify of authenticity of all session IDs at runtime.", "example": "Thin client applications like web applications are particularly vulnerable to session ID attacks. Since the server has very little control over the client, but still must track sessions, data, and objects on the server side, cookies and other mechanisms have been used to pass the key to the session data between the client and server. When these session keys are compromised it is trivial for an attacker to impersonate a user's session in effect, have the same capabilities as the authorized user. There are two main ways for an attacker to exploit session IDs. A brute force attack involves an attacker repeatedly attempting to query the system with a spoofed session header in the HTTP request. A web server that uses a short session ID can be easily spoofed by trying many possible combinations so the parameters session-ID= 1234 has few possible combinations, and an attacker can retry several hundred or thousand request with little to no issue on their side. The second method is interception, where a tool such as wireshark is used to sniff the wire and pull off any unprotected session identifiers. The attacker can then use these variables and access the application.", @@ -887,7 +887,7 @@ "details": "An adversary leverages a legitimate capability of an application in such a way as to achieve a negative technical impact. The system functionality is not altered or modified but used in a way that was not intended. This is often accomplished through the overuse of a specific functionality or by leveraging functionality with design flaws that enables the adversary to gain access to unauthorized, sensitive data.", "Likelihood Of Attack": "Medium", "severity": "Medium", - "condition": "target.hasAccessControl is False or target.authorizesSource is False", + "condition": "target.controls.hasAccessControl is False or target.controls.authorizesSource is False", "prerequisites": "The adversary has the capability to interact with the application directly.The target system does not adequately implement safeguards to prevent misuse of authorized actions/processes.", "mitigations": "Perform comprehensive threat modeling, a process of identifying, evaluating, and mitigating potential threats to the application. This effort can help reveal potentially obscure application functionality that can be manipulated for malicious purposes.When implementing security features, consider how they can be misused and compromised.", "example": "An attacker clicks on the 'forgot password' and is presented with a single security question. The question is regarding the name of the first dog of the user. The system does not limit the number of attempts to provide the dog's name. An attacker goes through a list of 100 most popular dog names and finds the right name, thus getting the ability to reset the password and access the system.", @@ -902,7 +902,7 @@ "details": "An attacker sends random, malformed, or otherwise unexpected messages to a target application and observes the application's log or error messages returned. Fuzzing techniques involve sending random or malformed messages to a target and monitoring the target's response. The attacker does not initially know how a target will respond to individual messages but by attempting a large number of message variants they may find a variant that trigger's desired behavior. In this attack, the purpose of the fuzzing is to observe the application's log and error messages, although fuzzing a target can also sometimes cause the target to enter an unstable state, causing a crash. By observing logs and error messages, the attacker can learn details about the configuration of the target application and might be able to cause the target to disclose sensitive information.", "Likelihood Of Attack": "High", "severity": "Low", - "condition": "target.sanitizesInput is False or target.encodesOutput is False", + "condition": "target.controls.sanitizesInput is False or target.controls.encodesOutput is False", "prerequisites": "The target application must fail to sanitize incoming messages adequately before processing.", "mitigations": "Design: Construct a 'code book' for error messages. When using a code book, application error messages aren't generated in string or stack trace form, but are catalogued and replaced with a unique (often integer-based) value 'coding' for the error. Such a technique will require helpdesk and hosting personnel to use a 'code book' or similar mapping to decode application errors/logs in order to respond to them normally.Design: wrap application functionality (preferably through the underlying framework) in an output encoding scheme that obscures or cleanses error messages to prevent such attacks. Such a technique is often used in conjunction with the above 'code book' suggestion.Implementation: Obfuscate server fields of HTTP response.Implementation: Hide inner ordering of HTTP response header.Implementation: Customizing HTTP error codes such as 404 or 500.Implementation: Hide HTTP response header software information filed.Implementation: Hide cookie's software information filed.Implementation: Obfuscate database type in Database API's error message.", "example": "The following code generates an error message that leaks the full pathname of the configuration file. $ConfigDir = /home/myprog/config;$uname = GetUserInput(username);ExitError(Bad hacker!) if ($uname !~ /^w+$/);$file = $ConfigDir/$uname.txt;if (! (-e $file)) { ExitError(Error: $file does not exist); }... If this code is running on a server, such as a web application, then the person making the request should not know what the full pathname of the configuration directory is. By submitting a username that does not produce a $file that exists, an attacker could get this pathname. It could then be used to exploit path traversal or symbolic link following problems that may exist elsewhere in the application.", @@ -917,7 +917,7 @@ "details": "An adversary manipulates a setting or parameter on communications channel in order to compromise its security. This can result in information exposure, insertion/removal of information from the communications stream, and/or potentially system compromise.", "Likelihood Of Attack": "Medium", "severity": "High", - "condition": "(target.protocol != 'HTTPS' or target.usesVPN is False) and (target.implementsAuthenticationScheme is False or target.authorizesSource is False)", + "condition": "(target.protocol != 'HTTPS' or target.usesVPN is False) and (target.controls.implementsAuthenticationScheme is False or target.controls.authorizesSource is False)", "prerequisites": "The target application must leverage an open communications channel.The channel on which the target communicates must be vulnerable to interception (e.g., man in the middle attack).", "mitigations": "Encrypt all sensitive communications using properly-configured cryptography.Design the communication system such that it associates proper authentication/authorization with each channel/message.", "example": "Using MITM techniques, an attacker launches a blockwise chosen-boundary attack to obtain plaintext HTTP headers by taking advantage of an SSL session using an encryption protocol in CBC mode with chained initialization vectors (IV). This allows the attacker to recover session IDs, authentication cookies, and possibly other valuable data that can be used for further exploitation. Additionally this could allow for the insertion of data into the stream, allowing for additional attacks (CSRF, SQL inject, etc) to occur.", @@ -932,7 +932,7 @@ "details": "An adversary takes advantage of incorrectly configured SSL communications that enables access to data intended to be encrypted. The adversary may also use this type of attack to inject commands or other traffic into the encrypted stream to cause compromise of either the client or server.", "Likelihood Of Attack": "Low", "severity": "High", - "condition": "target.checkTLSVersion(target.inputs) and (not target.implementsAuthenticationScheme or not target.authorizesSource)", + "condition": "target.checkTLSVersion(target.inputs) and (not target.controls.implementsAuthenticationScheme or not target.controls.authorizesSource)", "prerequisites": "Access to the client/server stream.", "mitigations": "Usage of configuration settings, such as stream ciphers vs. block ciphers and setting timeouts on SSL sessions to extremely low values lessens the potential impact. Use of later versions of TLS (e.g. TLS 1.1+) can also be effective, but not all clients or servers support the later versions.", "example": "Using MITM techniques, an attacker launches a blockwise chosen-boundary attack to obtain plaintext HTTP headers by taking advantage of an SSL session using an encryption protocol in CBC mode with chained initialization vectors (IV). This allows the attacker to recover session IDs, authentication cookies, and possibly other valuable data that can be used for further exploitation. Additionally this could allow for the insertion of data into the stream, allowing for additional attacks (CSRF, SQL inject, etc) to occur.", @@ -962,7 +962,7 @@ "details": "An attack of this type exploits vulnerabilities in client/server communication channel authentication and data integrity. It leverages the implicit trust a server places in the client, or more importantly, that which the server believes is the client. An attacker executes this type of attack by placing themselves in the communication channel between client and server such that communication directly to the server is possible where the server believes it is communicating only with a valid client. There are numerous variations of this type of attack.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.implementsServerSideValidation is False and (target.providesIntegrity is False or target.authorizesSource is False)", + "condition": "target.controls.implementsServerSideValidation is False and (target.controls.providesIntegrity is False or target.controls.authorizesSource is False)", "prerequisites": "Server software must rely on client side formatted and validated values, and not reinforce these checks on the server side.", "mitigations": "Design: Ensure that client process and/or message is authenticated so that anonymous communications and/or messages are not accepted by the system.Design: Do not rely on client validation or encoding for security purposes.Design: Utilize digital signatures to increase authentication assurance.Design: Utilize two factor authentication to increase authentication assurance.Implementation: Perform input validation for all remote content.", "example": "Web applications may use JavaScript to perform client side validation, request encoding/formatting, and other security functions, which provides some usability benefits and eliminates some client-server round-tripping. However, the web server cannot assume that the requests it receives have been subject to those validations, because an attacker can use an alternate method for crafting the HTTP Request and submit data that contains poisoned values designed to spoof a user and/or get the web server to disclose information.Web 2.0 style applications may be particularly vulnerable because they in large part rely on existing infrastructure which provides scalability without the ability to govern the clients. Attackers identify vulnerabilities that either assume the client side is responsible for some security services (without the requisite ability to ensure enforcement of these checks) and/or the lack of a hardened, default deny server configuration that allows for an attacker probing for weaknesses in unexpected ways. Client side validation, request formatting and other services may be performed, but these are strictly usability enhancements not security enhancements.Many web applications use client side scripting like JavaScript to enforce authentication, authorization, session state and other variables, but at the end of day they all make requests to the server. These client side checks may provide usability and performance gains, but they lack integrity in terms of the http request. It is possible for an attacker to post variables directly to the server without using any of the client script security checks and customize the patterns to impersonate other users or probe for more information.Many message oriented middleware systems like MQ Series are rely on information that is passed along with the message request for making authorization decisions, for example what group or role the request should be passed. However, if the message server does not or cannot authenticate the authorization information in the request then the server's policy decisions about authorization are trivial to subvert because the client process can simply elevate privilege by passing in elevated group or role information which the message server accepts and acts on.", @@ -977,7 +977,7 @@ "details": "An adversary takes advantage of weaknesses in the protocol by which a client and server are communicating to perform unexpected actions. Communication protocols are necessary to transfer messages between client and server applications. Moreover, different protocols may be used for different types of interactions. For example, an authentication protocol might be used to establish the identities of the server and client while a separate messaging protocol might be used to exchange data. If there is a weakness in a protocol used by the client and server, an attacker might take advantage of this to perform various types of attacks. For example, if the attacker is able to manipulate an authentication protocol, the attacker may be able spoof other clients or servers. If the attacker is able to manipulate a messaging protocol, the may be able to read sensitive information or modify message contents. This attack is often made easier by the fact that many clients and servers support multiple protocols to perform similar roles. For example, a server might support several different authentication protocols in order to support a wide range of clients, including legacy clients. Some of the older protocols may have vulnerabilities that allow an attacker to manipulate client-server interactions.", "Likelihood Of Attack": "Medium", "severity": "Medium", - "condition": "not target.isEncrypted or target.tlsVersion < target.sink.minTLSVersion", + "condition": "not target.controls.isEncrypted or target.tlsVersion < target.sink.minTLSVersion", "prerequisites": "The client and/or server must utilize a protocol that has a weakness allowing manipulation of the interaction.", "mitigations": "Use strong authentication protocols.", "example": "An adversary could exploit existing communication protocol vulnerabilities and can launch MITM attacks and gain sensitive information or spoof client/server identities.", @@ -992,7 +992,7 @@ "details": "This attack takes advantage of the entity replacement property of XML where the value of the replacement is a URI. A well-crafted XML document could have the entity refer to a URI that consumes a large amount of resources to create a denial of service condition. This can cause the system to either freeze, crash, or execute arbitrary code depending on the URI.", "Likelihood Of Attack": "Low", "severity": "Medium", - "condition": "target.usesXMLParser is False or target.disablesDTD is False", + "condition": "target.usesXMLParser is False or target.controls.disablesDTD is False", "prerequisites": "A server that has an implementation that accepts entities containing URI values.", "mitigations": "This attack may be mitigated by tweaking the XML parser to not resolve external entities. If external entities are needed, then implement a custom XmlResolver that has a request timeout, data retrieval limit, and restrict resources it can retrieve locally.", "example": "In this example, the XML parser parses the attacker's XML and opens the malicious URI where the attacker controls the server and writes a massive amount of data to the response stream. In this example the malicious URI is a large file transfer. < !DOCTYPE bomb []>&detonate;", @@ -1007,7 +1007,7 @@ "details": "In an iFrame overlay attack the victim is tricked into unknowingly initiating some action in one system while interacting with the UI from seemingly completely different system. While being logged in to some target system, the victim visits the attackers' malicious site which displays a UI that the victim wishes to interact with. In reality, the iFrame overlay page has a transparent layer above the visible UI with action controls that the attacker wishes the victim to execute. The victim clicks on buttons or other UI elements they see on the page which actually triggers the action controls in the transparent overlaying layer. Depending on what that action control is, the attacker may have just tricked the victim into executing some potentially privileged (and most undesired) functionality in the target system to which the victim is authenticated. The basic problem here is that there is a dichotomy between what the victim thinks he or she is clicking on versus what he or she is actually clicking on.", "Likelihood Of Attack": "Medium", "severity": "High", - "condition": "target.disablesiFrames is False", + "condition": "target.controls.disablesiFrames is False", "prerequisites": "The victim is communicating with the target application via a web based UI and not a thick client. The victim's browser security policies allow iFrames. The victim uses a modern browser that supports UI elements like clickable buttons (i.e. not using an old text only browser). The victim has an active session with the target system. The target system's interaction window is open in the victim's browser and supports the ability for initiating sensitive actions on behalf of the user in the target system.", "mitigations": "Configuration: Disable iFrames in the Web browser.Operation: When maintaining an authenticated session with a privileged target system, do not use the same browser to navigate to unfamiliar sites to perform other activities. Finish working with the target system and logout first before proceeding to other tasks.Operation: If using the Firefox browser, use the NoScript plug-in that will help forbid iFrames.", "example": "The following example is a real-world iFrame overlay attack [2]. In this attack, the malicious page embeds Twitter.com on a transparent IFRAME. The status-message field is initialized with the URL of the malicious page itself. To provoke the click, which is necessary to publish the entry, the malicious page displays a button labeled Don't Click. This button is aligned with the invisible Update button of Twitter. Once the user performs the click, the status message (i.e., a link to the malicious page itself) is posted to his/ her Twitter profile.", @@ -1022,7 +1022,7 @@ "details": "An attacker manipulates an existing credential in order to gain access to a target application. Session credentials allow users to identify themselves to a service after an initial authentication without needing to resend the authentication information (usually a username and password) with every message. An attacker may be able to manipulate a credential sniffed from an existing connection in order to gain access to a target server. For example, a credential in the form of a web cookie might have a field that indicates the access rights of a user. By manually tweaking this cookie, a user might be able to increase their access rights to the server. Alternately an attacker may be able to manipulate an existing credential to appear as a different user. This attack differs from falsification through prediction in that the user bases their modified credentials off existing credentials instead of using patterns detected in prior credentials to create a new credential that is accepted because it fits the pattern. As a result, an attacker may be able to impersonate other users or elevate their permissions to a targeted service.", "Likelihood Of Attack": "Medium", "severity": "Medium", - "condition": "target.usesStrongSessionIdentifiers is False", + "condition": "target.controls.usesStrongSessionIdentifiers is False", "prerequisites": "The targeted application must use session credentials to identify legitimate users.", "mitigations": "Implementation: Use session IDs that are difficult to guess or brute-force: One way for the attackers to obtain valid session IDs is by brute-forcing or guessing them. By choosing session identifiers that are sufficiently random, brute-forcing or guessing becomes very difficult. Implementation: Regenerate and destroy session identifiers when there is a change in the level of privilege: This ensures that even though a potential victim may have followed a link with a fixated identifier, a new one is issued when the level of privilege changes.", "example": "An adversary uses client side scripting(JavaScript) to set session ID in the victim's browser using document.cookie. This fixates a falsified session credential into victim's browser with the help of a crafted URL link. Once the victim clicks on the link, the attacker is able to bypass authentication or piggyback off some other authenticated victim's session.", @@ -1037,7 +1037,7 @@ "details": "An attacker injects malicious content into an application's DTD in an attempt to produce a negative technical impact. DTDs are used to describe how XML documents are processed. Certain malformed DTDs (for example, those with excessive entity expansion as described in CAPEC 197) can cause the XML parsers that process the DTDs to consume excessive resources resulting in resource depletion.", "Likelihood Of Attack": "Medium", "severity": "Medium", - "condition": "target.usesXMLParser is False or target.disablesDTD is False", + "condition": "target.usesXMLParser is False or target.controls.disablesDTD is False", "prerequisites": "The target must be running an XML based application that leverages DTDs.", "mitigations": "Design: Sanitize incoming DTDs to prevent excessive expansion or other actions that could result in impacts like resource depletion.Implementation: Disallow the inclusion of DTDs as part of incoming messages.Implementation: Use XML parsing tools that protect against DTD attacks.", "example": "Adversary injects XML External Entity (XEE) attack that can cause the disclosure of confidential information, execute abitrary code, create a Denial of Service of the targeted server, or several other malicious impacts.", @@ -1052,7 +1052,7 @@ "details": "This attack exploits certain XML parsers which manage data in an inefficient manner. The attacker crafts an XML document with many attributes in the same XML node. In a vulnerable parser, this results in a denial of service condition owhere CPU resources are exhausted because of the parsing algorithm.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.usesXMLParser is False or target.disablesDTD is False", + "condition": "target.usesXMLParser is False or target.controls.disablesDTD is False", "prerequisites": "The server accepts XML input and is using a parser with a runtime longer than O(n) for the insertion of a new attribute in the data container.(examples are .NET framework 1.0 and 1.1)", "mitigations": "This attack may be mitigated completely by using a parser that is not using a vulnerable container. Mitigation may also limit the number of attributes per XML element.", "example": "In this example, assume that the victim is running a vulnerable parser such as .NET framework 1.0. This results in a quadratic runtime of O(n^2). A document with n attributes results in (n^2)/2 operations to be performed. If an operation takes 100 nanoseconds then a document with 100,000 operations would take 500s to process. In this fashion a small message of less than 1MB causes a denial of service condition on the CPU resources.", @@ -1067,7 +1067,7 @@ "details": "An attack of this type exploits the host's trust in executing remote content, including binary files. The files are poisoned with a malicious payload (targeting the file systems accessible by the target software) by the adversary and may be passed through standard channels such as via email, and standard web content like PDF and multimedia files. The adversary exploits known vulnerabilities or handling routines in the target processes. Vulnerabilities of this type have been found in a wide variety of commercial applications from Microsoft Office to Adobe Acrobat and Apple Safari web browser. When the adversary knows the standard handling routines and can identify vulnerabilities and entry points, they can be exploited by otherwise seemingly normal content. Once the attack is executed, the adversary's program can access relative directories such as C:Program Files or other standard system directories to launch further attacks. In a worst case scenario, these programs are combined with other propagation logic and work as a virus.", "Likelihood Of Attack": "High", "severity": "Very High", - "condition": "target.hasAccessControl is False and (target.sanitizesInput is False or target.validatesInput is False)", + "condition": "target.controls.hasAccessControl is False and (target.controls.sanitizesInput is False or target.controls.validatesInput is False)", "prerequisites": "The target software must consume files.The adversary must have access to modify files that the target software will consume.", "mitigations": "Design: Enforce principle of least privilegeDesign: Validate all input for content including files. Ensure that if files and remote content must be accepted that once accepted, they are placed in a sandbox type location so that lower assurance clients cannot write up to higher assurance processes (like Web server processes for example)Design: Execute programs with constrained privileges, so parent process does not open up further vulnerabilities. Ensure that all directories, temporary directories and files, and memory are executing with limited privileges to protect against remote execution.Design: Proxy communication to host, so that communications are terminated at the proxy, sanitizing the requests before forwarding to server host.Implementation: Virus scanning on hostImplementation: Host integrity monitoring for critical files, directories, and processes. The goal of host integrity monitoring is to be aware when a security issue has occurred so that incident response and other forensic activities can begin.", "example": "PHP is a very popular language used for developing web applications. When PHP is used with global variables, a vulnerability may be opened that affects the file system. A standard HTML form that allows for remote users to upload files, may also place those files in a public directory where the adversary can directly access and execute them through a browser. This vulnerability allows remote adversaries to execute arbitrary code on the system, and can result in the adversary being able to erase intrusion evidence from system and application logs. [R.23.2]", @@ -1082,7 +1082,7 @@ "details": "Applications often need to transform data in and out of the XML format by using an XML parser. It may be possible for an attacker to inject data that may have an adverse effect on the XML parser when it is being processed. By nesting XML data and causing this data to be continuously self-referential, an attacker can cause the XML parser to consume more resources while processing, causing excessive memory consumption and CPU utilization. An attacker's goal is to leverage parser failure to his or her advantage. In most cases this type of an attack will result in a denial of service due to an application becoming unstable, freezing, or crash. However it may be possible to cause a crash resulting in arbitrary code execution, leading to a jump from the data plane to the control plane [R.230.1].", "Likelihood Of Attack": "Medium", "severity": "High", - "condition": "target.usesXMLParser is True and (target.validatesInput is False or target.sanitizesInput is False)", + "condition": "target.usesXMLParser is True and (target.controls.validatesInput is False or target.controls.sanitizesInput is False)", "prerequisites": "An application uses an XML parser to perform transformation on user-controllable data.An application does not perform sufficient validation to ensure that user-controllable data is safe for an XML parser.", "mitigations": "Carefully validate and sanitize all user-controllable data prior to passing it to the XML parser routine. Ensure that the resultant data is safe to pass to the XML parser.Perform validation on canonical data.Pick a robust implementation of an XML parser.Validate XML against a valid schema or DTD prior to parsing.", "example": "An adversary crafts input data that may have an adverse effect on the operation of the XML parser when the data is parsed on the victim's system.", @@ -1097,7 +1097,7 @@ "details": "An adversary exploits a weakness enabling them to elevate their privilege and perform an action that they are not supposed to be authorized to perform.", "Likelihood Of Attack": "Medium", "severity": "High", - "condition": "target.hasAccessControl is False or target.implementsPOLP is False", + "condition": "target.controls.hasAccessControl is False or target.controls.implementsPOLP is False", "prerequisites": "", "mitigations": "Very carefully manage the setting, management, and handling of privileges. Explicitly manage trust zones in the software. Follow the principle of least privilege when assigning access rights to entities in a software system. Implement separation of privilege - Require multiple conditions to be met before permitting access to a system resource.", "example": "The software does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor. As a result, the program is indefinitely operating in a raised privilege state, possibly allowing further exploitation to occur.", @@ -1112,7 +1112,7 @@ "details": "An attacker gains control of a process that is assigned elevated privileges in order to execute arbitrary code with those privileges. Some processes are assigned elevated privileges on an operating system, usually through association with a particular user, group, or role. If an attacker can hijack this process, they will be able to assume its level of privilege in order to execute their own code. Processes can be hijacked through improper handling of user input (for example, a buffer overflow or certain types of injection attacks) or by utilizing system utilities that support process control that have been inadequately secured.", "Likelihood Of Attack": "Medium", "severity": "Medium", - "condition": "target.hasAccessControl is False or target.implementsPOLP is False", + "condition": "target.controls.hasAccessControl is False or target.controls.implementsPOLP is False", "prerequisites": "The targeted process or operating system must contain a bug that allows attackers to hijack the targeted process.", "mitigations": "Very carefully manage the setting, management, and handling of privileges. Explicitly manage trust zones in the software. Follow the principle of least privilege when assigning access rights to entities in a software system. Implement separation of privilege - Require multiple conditions to be met before permitting access to a system resource.", "example": "The software does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor. As a result, the program is indefinitely operating in a raised privilege state, possibly allowing further exploitation to occur.", @@ -1127,7 +1127,7 @@ "details": "Attackers can sometimes hijack a privileged thread from the underlying system through synchronous (calling a privileged function that returns incorrectly) or asynchronous (callbacks, signal handlers, and similar) means. Having done so, the Attacker may not only likely access functionality the system's designer didn't intend for them, but they may also go undetected or deny other users essential service in a catastrophic (or insidiously subtle) way.", "Likelihood Of Attack": "Low", "severity": "Very High", - "condition": "target.implementsPOLP is False and (target.usesEnvironmentVariables is True or target.validatesInput is False)", + "condition": "target.controls.implementsPOLP is False and (target.usesEnvironmentVariables is True or target.controls.validatesInput is False)", "prerequisites": "The application in question employs a threaded model of execution with the threads operating at, or having the ability to switch to, a higher privilege level than normal usersIn order to feasibly execute this class of attacks, the attacker must have the ability to hijack a privileged thread.This ability includes, but is not limited to, modifying environment variables that affect the process the thread belongs to, or providing malformed user-controllable input that causes the executing thread to fault and return to a higher privilege level or such.This does not preclude network-based attacks, but makes them conceptually more difficult to identify and execute.", "mitigations": "Application Architects must be careful to design callback, signal, and similar asynchronous constructs such that they shed excess privilege prior to handing control to user-written (thus untrusted) code.Application Architects must be careful to design privileged code blocks such that upon return (successful, failed, or unpredicted) that privilege is shed prior to leaving the block/scope.", "example": "Attacker targets an application written using Java's AWT, with the 1.2.2 era event model. In this circumstance, any AWTEvent originating in the underlying OS (such as a mouse click) would return a privileged thread. The Attacker could choose to not return the AWT-generated thread upon consuming the event, but instead leveraging its privilege to conduct privileged operations.", @@ -1143,7 +1143,7 @@ "details": "In this attack, the idea is to cause an active filter to fail by causing an oversized transaction. An attacker may try to feed overly long input strings to the program in an attempt to overwhelm the filter (by causing a buffer overflow) and hoping that the filter does not fail securely (i.e. the user input is let into the system unfiltered).", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.checksInputBounds is False or target.validatesInput is False", + "condition": "target.controls.checksInputBounds is False or target.controls.validatesInput is False", "prerequisites": "Ability to control the length of data passed to an active filter.", "mitigations": "Make sure that ANY failure occurring in the filtering or input validation routine is properly handled and that offending input is NOT allowed to go through. Basically make sure that the vault is closed when failure occurs.Pre-design: Use a language or compiler that performs automatic bounds checking.Pre-design through Build: Compiler-based canary mechanisms such as StackGuard, ProPolice and the Microsoft Visual Studio /GS flag. Unless this provides automatic bounds checking, it is not a complete solution.Operational: Use OS-level preventative functionality. Not a complete solution.Design: Use an abstraction library to abstract away risky APIs. Not a complete solution.", "example": "Attack Example: Filter Failure in Taylor UUCP Daemon Sending in arguments that are too long to cause the filter to fail open is one instantiation of the filter failure attack. The Taylor UUCP daemon is designed to remove hostile arguments before they can be executed. If the arguments are too long, however, the daemon fails to remove them. This leaves the door open for attack.A filter is used by a web application to filter out characters that may allow the input to jump from the data plane to the control plane when data is used in a SQL statement (chaining this attack with the SQL injection attack). Leveraging a buffer overflow the attacker makes the filter fail insecurely and the tainted data is permitted to enter unfiltered into the system, subsequently causing a SQL injection.Audit Truncation and Filters with Buffer Overflow. Sometimes very large transactions can be used to destroy a log file or cause partial logging failures. In this kind of attack, log processing code might be examining a transaction in real-time processing, but the oversized transaction causes a logic branch or an exception of some kind that is trapped. In other words, the transaction is still executed, but the logging or filtering mechanism still fails. This has two consequences, the first being that you can run transactions that are not logged in any way (or perhaps the log entry is completely corrupted). The second consequence is that you might slip through an active filter that otherwise would stop your attack.", @@ -1159,7 +1159,7 @@ "details": "An adversary exploits weaknesses in input validation by manipulating resource identifiers enabling the unintended modification or specification of a resource.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.validatesInput is False or target.sanitizesInput is False", + "condition": "target.controls.validatesInput is False or target.controls.sanitizesInput is False", "prerequisites": "The target application allows the user to both specify the identifier used to access a system resource. Through this permission, the user gains the capability to perform actions on that resource (e.g., overwrite the file)", "mitigations": "Ensure all input content that is delivered to client is sanitized against an acceptable content specification.Perform input validation for all content.Enforce regular patching of software.", "example": "A Java code uses input from an HTTP request to create a file name. The programmer has not considered the possibility that an attacker could provide a file name such as '../../tomcat/confserver.xml', which causes the application to delete one of its own configuration files.", @@ -1175,7 +1175,7 @@ "details": "An adversary exploits a weakness in input validation on the target to inject new code into that which is currently executing. This differs from code inclusion in that code inclusion involves the addition or replacement of a reference to a code file, which is subsequently loaded by the target and used as part of the code of some application.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.validatesInput is False or target.sanitizesInput is False", + "condition": "target.controls.validatesInput is False or target.controls.sanitizesInput is False", "prerequisites": "The target software does not validate user-controlled input such that the execution of a process may be altered by sending code in through legitimate data channels, using no other mechanism.", "mitigations": "Utilize strict type, character, and encoding enforcementEnsure all input content that is delivered to client is sanitized against an acceptable content specification.Perform input validation for all content.Enforce regular patching of software.", "example": "When a developer uses the PHP eval() function and passes it untrusted data that an attacker can modify, code injection could be possible.", @@ -1190,7 +1190,7 @@ "details": "An adversary inserts commands to perform cross-site scripting (XSS) actions in HTML attributes. Many filters do not adequately sanitize attributes against the presence of potentially dangerous commands even if they adequately sanitize tags. For example, dangerous expressions could be inserted into a style attribute in an anchor tag, resulting in the execution of malicious code when the resulting page is rendered. If a victim is tricked into viewing the rendered page the attack proceeds like a normal XSS attack, possibly resulting in the loss of sensitive cookies or other malicious activities.", "Likelihood Of Attack": "", "severity": "Medium", - "condition": "target.validatesInput is False or target.sanitizesInput is False", + "condition": "target.controls.validatesInput is False or target.controls.sanitizesInput is False", "prerequisites": "The target application must fail to adequately sanitize HTML attributes against the presence of dangerous commands.", "mitigations": "Design: Use libraries and templates that minimize unfiltered input.Implementation: Normalize, filter and white list all input including that which is not expected to have any scripting content.Implementation: The victim should configure the browser to minimize active content from untrusted sources.", "example": "Application allows execution of any Javascript they want on the browser which enables the adversary to steal session tokens and perform malicious activities.", @@ -1206,7 +1206,7 @@ "details": "An attack of this type exploits the ability of most browsers to interpret data, javascript or other URI schemes as client-side executable content placeholders. This attack consists of passing a malicious URI in an anchor tag HREF attribute or any other similar attributes in other HTML tags. Such malicious URI contains, for example, a base64 encoded HTML content with an embedded cross-site scripting payload. The attack is executed when the browser interprets the malicious content i.e., for example, when the victim clicks on the malicious link.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.validatesInput is False or target.sanitizesInput is False or target.encodesOutput is False", + "condition": "target.controls.validatesInput is False or target.controls.sanitizesInput is False or target.controls.encodesOutput is False", "prerequisites": "Target client software must allow scripting such as JavaScript and allows executable content delivered using a data URI scheme.", "mitigations": "Design: Use browser technologies that do not allow client side scripting.Design: Utilize strict type, character, and encoding enforcement.Implementation: Ensure all content that is delivered to client is sanitized against an acceptable content specification.Implementation: Ensure all content coming from the client is using the same encoding; if not, the server-side application must canonicalize the data before applying any filtering.Implementation: Perform input validation for all remote content, including remote and user-generated contentImplementation: Perform output validation for all remote content.Implementation: Disable scripting languages such as JavaScript in browserImplementation: Patching software. There are many attack vectors for XSS on the client side and the server side. Many vulnerabilities are fixed in service packs for browser, web servers, and plug in technologies, staying current on patch release that deal with XSS countermeasures mitigates this.", "example": "The following payload data: text/html;base64,PGh0bWw+PGJvZHk+PHNjcmlwdD52YXIgaW1nID0gbmV3IEltYWdlKCk7IGltZy5zcmMgPSAiaHR0cDovL2F0dGFja2VyLmNvbS9jb29raWVncmFiYmVyPyIrIGVuY29kZVVSSUNvbXBvbmVudChkb2N1bWVudC5jb29raWVzKTs8L3NjcmlwdD48L2JvZHk+PC9odG1sPg== represents a base64 encoded HTML and uses the data URI scheme to deliver it to the browser. The decoded payload is the following piece of HTML code: Web applications that take user controlled inputs and reflect them in URI HTML placeholder without a proper validation are at risk for such an attack. An attacker could inject the previous payload that would be placed in a URI placeholder (for example in the anchor tag HREF attribute): My Link Once the victim clicks on the link, the browser will decode and execute the content from the payload. This will result on the execution of the cross-site scripting attack.", @@ -1222,7 +1222,7 @@ "details": "The attacker bypasses input validation by using doubled characters in order to perform a cross-site scripting attack. Some filters fail to recognize dangerous sequences if they are preceded by repeated characters. For example, by doubling the < before a script command, (< The script itself will be invisible to anybody viewing the logs in a web browser (unless they view the source for the page).", @@ -1342,7 +1342,7 @@ "details": "An adversary corrupts or modifies the content of a schema for the purpose of undermining the security of the target. Schemas provide the structure and content definitions for resources used by an application. By replacing or modifying a schema, the adversary can affect how the application handles or interprets a resource, often leading to possible denial of service, entering into an unexpected state, or recording incomplete data.", "Likelihood Of Attack": "Low", "severity": "High", - "condition": "target.implementsPOLP is False", + "condition": "target.controls.implementsPOLP is False", "prerequisites": "Some level of access to modify the target schema.The schema used by the target application must be improperly secured against unauthorized modification and manipulation.", "mitigations": "Design: Protect the schema against unauthorized modification.Implementation: For applications that use a known schema, use a local copy or a known good repository instead of the schema reference supplied in the schema document.Implementation: For applications that leverage remote schemas, use the HTTPS protocol to prevent modification of traffic in transit and to avoid unauthorized modification.", "example": "In a JSON Schema Poisoning Attack, an adervary modifies the JSON schema to cause a Denial of Service (DOS) or to submit malicious input: { title: Contact, type: object, properties: { Name: { type: string }, Phone: { type: string }, Email: { type: string }, Address: { type: string } }, required: [Name, Phone, Email, Address] } If the 'name' attribute is required in all submitted documents and this field is removed by the adversary, the application may enter an unexpected state or record incomplete data. Additionally, if this data is needed to perform additional functions, a Denial of Service (DOS) may occur.In a Database Schema Poisoning Attack, an adversary alters the database schema being used to modify the database in some way. This can result in loss of data, DOS, or malicious input being submitted. Assuming there is a column named name, an adversary could make the following schema change: ALTER TABLE Contacts MODIFY Name VARCHAR(65353); The Name field of the Conteacts table now allows the storing of names up to 65353 characters in length. This could allow the adversary to store excess data within the database to consume system resource or to execute a DOS.", @@ -1357,7 +1357,7 @@ "details": "An attacker injects content into a server response that is interpreted differently by intermediaries than it is by the target browser. To do this, it takes advantage of inconsistent or incorrect interpretations of the HTTP protocol by various applications. For example, it might use different block terminating characters (CR or LF alone), adding duplicate header fields that browsers interpret as belonging to separate responses, or other techniques. Consequences of this attack can include response-splitting, cross-site scripting, apparent defacement of targeted sites, cache poisoning, or similar actions.", "Likelihood Of Attack": "Medium", "severity": "Medium", - "condition": "target.implementsStrictHTTPValidation is False and target.encodesHeaders is False", + "condition": "target.controls.implementsStrictHTTPValidation is False and target.controls.encodesHeaders is False", "prerequisites": "The targeted server must allow the attacker to insert content that will appear in the server's response.", "mitigations": "Design: Employ strict adherence to interpretations of HTTP messages wherever possible.Implementation: Encode header information provided by user input so that user-supplied content is not interpreted by intermediaries.", "example": "The attacker targets the cache service used by the organization to reduce load on the internet bandwidth. This server can be a cache server on the LAN or other application server caching the static WebPages. The attacker sends three different HTTP request as shown - Request 1: POST request for http://www.netbanking.com, Request 2: GET request for http:www.netbanking.com/FD.html, Request 3: GET request for http://www.netbanking.com/FD-Rates.html. Due to malformed request cache server assumes request 1 and 3 as valid request and forwards the entire request to the webserver. Webserver which strictly follow then HTTP parsing rule responds with the http://www.netbanking.com/FD.html HTML page. This is happened because webserver consider request 1 and 2 as valid one. Cache server stores this response against the request 3. When normal users request for page http://www.netbanking.com/FD-Rates.html, cache server responds with the page http://www.netbanking.com/FD.html.Hence attacker will succeeds in cache poisoning.", @@ -1372,7 +1372,7 @@ "details": "HTTP Request Smuggling results from the discrepancies in parsing HTTP requests between HTTP entities such as web caching proxies or application firewalls. Entities such as web servers, web caching proxies, application firewalls or simple proxies often parse HTTP requests in slightly different ways. Under specific situations where there are two or more such entities in the path of the HTTP request, a specially crafted request is seen by two attacked entities as two different sets of requests. This allows certain requests to be smuggled through to a second entity without the first one realizing it.", "Likelihood Of Attack": "Medium", "severity": "High", - "condition": "target.implementsStrictHTTPValidation is False and target.encodesHeaders is False", + "condition": "target.controls.implementsStrictHTTPValidation is False and target.controls.encodesHeaders is False", "prerequisites": "An additional HTTP entity such as an application firewall or a web caching proxy between the attacker and the second entity such as a web serverDifferences in the way the two HTTP entities parse HTTP requests", "mitigations": "HTTP Request Smuggling is usually targeted at web servers. Therefore, in such cases, careful analysis of the entities must occur during system design prior to deployment. If there are known differences in the way the entities parse HTTP requests, the choice of entities needs consideration.Employing an application firewall can help. However, there are instances of the firewalls being susceptible to HTTP Request Smuggling as well.", "example": "When using Sun Java System Web Proxy Server 3.x or 4.x in conjunction with Sun ONE/iPlanet 6.x, Sun Java System Application Server 7.x or 8.x, it is possible to bypass certain application firewall protections, hijack web sessions, perform Cross Site Scripting or poison the web proxy cache using HTTP Request Smuggling. Differences in the way HTTP requests are parsed by the Proxy Server and the Application Server enable malicious requests to be smuggled through to the Application Server, thereby exposing the Application Server to aforementioned attacks. See also: CVE-2006-6276Apache server 2.0.45 and version before 1.3.34, when used as a proxy, easily lead to web cache poisoning and bypassing of application firewall restrictions because of non-standard HTTP behavior. Although the HTTP/1.1 specification clearly states that a request with both Content-Length and a Transfer-Encoding: chunked headers is invalid, vulnerable versions of Apache accept such requests and reassemble the ones with Transfer-Encoding: chunked header without replacing the existing Content-Length header or adding its own. This leads to HTTP Request Smuggling using a request with a chunked body and a header with Content-Length: 0. See also: CVE-2005-2088", @@ -1387,7 +1387,7 @@ "details": "This type of attack is a form of Cross-Site Scripting (XSS) where a malicious script is inserted into the client-side HTML being parsed by a web browser. Content served by a vulnerable web application includes script code used to manipulate the Document Object Model (DOM). This script code either does not properly validate input, or does not perform proper output encoding, thus creating an opportunity for an adversary to inject a malicious script launch a XSS attack. A key distinction between other XSS attacks and DOM-based attacks is that in other XSS attacks, the malicious script runs when the vulnerable web page is initially loaded, while a DOM-based attack executes sometime after the page loads. Another distinction of DOM-based attacks is that in some cases, the malicious script is never sent to the vulnerable web server at all. An attack like this is guaranteed to bypass any server-side filtering attempts to protect users.", "Likelihood Of Attack": "High", "severity": "Very High", - "condition": "target.allowsClientSideScripting is True and (target.sanitizesInput is False or target.validatesInput is False)", + "condition": "target.allowsClientSideScripting is True and (target.controls.sanitizesInput is False or target.controls.validatesInput is False)", "prerequisites": "An application that leverages a client-side web browser with scripting enabled.An application that manipulates the DOM via client-side scripting.An application that fails to adequately sanitize or encode untrusted input.", "mitigations": "Use browser technologies that do not allow client-side scripting.Utilize proper character encoding for all output produced within client-site scripts manipulating the DOM.Ensure that all user-supplied input is validated before use.", "example": "Consider a web application that enables or disables some of the fields of a form on the page via the use of a mode parameter provided on the query string. http://my.site.com/aform.html?mode=full The application’s client-side code may want to print this mode value to the screen to give the users an understanding of what mode they are in. In this example, JavaScript is used to pull the value from the URL and update the HTML by dynamically manipulating the DOM via a document.write() call. Notice how the value provided on the URL is used directly with no input validation performed and no output encoding in place. A maliciously crafted URL can thus be formed such that if a victim clicked on the URL, a malicious script would then be executed by the victim’s browser: http://my.site.com/aform.html?mode=In some DOM-based attacks, the malicious script never gets sent to the web server at all, thus bypassing any server-side protections that might be in place. Consider the previously used web application that displays the mode value. Since the HTML is being generated dynamically through DOM manipulations, a URL fragment (i.e., the part of a URL after the '#' character) can be used. http://my.site.com/aform.html#mode= In this variation of a DOM-based XSS attack, the malicious script will not be sent to the web server, but will instead be managed by the victim's browser and is still available to the client-side script code.", @@ -1402,7 +1402,7 @@ "details": "This attack targets predictable session ID in order to gain privileges. The attacker can predict the session ID used during a transaction to perform spoofing and session hijacking.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.usesStrongSessionIdentifiers is False", + "condition": "target.controls.usesStrongSessionIdentifiers is False", "prerequisites": "The target host uses session IDs to keep track of the users.Session IDs are used to control access to resources.The session IDs used by the target host are predictable. For example, the session IDs are generated using predictable information (e.g., time).", "mitigations": "Use a strong source of randomness to generate a session ID.Use adequate length session IDs. Do not use information available to the user in order to generate session ID (e.g., time).Ideas for creating random numbers are offered by Eastlake [RFC1750]. Encrypt the session ID if you expose it to the user. For instance session ID can be stored in a cookie in encrypted format.", "example": "Jetty before 4.2.27, 5.1 before 5.1.12, 6.0 before 6.0.2, and 6.1 before 6.1.0pre3 generates predictable session identifiers using java.util.random, which makes it easier for remote attackers to guess a session identifier through brute force attacks, bypass authentication requirements, and possibly conduct cross-site request forgery attacks. See also: CVE-2006-6969mod_usertrack in Apache 1.3.11 through 1.3.20 generates session ID's using predictable information including host IP address, system time and server process ID, which allows local users to obtain session ID's and bypass authentication when these session ID's are used for authentication. See also: CVE-2001-1534", @@ -1417,7 +1417,7 @@ "details": "This type of attack is a form of Cross-Site Scripting (XSS) where a malicious script is “reflected” off a vulnerable web application and then executed by a victim's browser. The process starts with an adversary delivering a malicious script to a victim and convincing the victim to send the script to the vulnerable web application. The most common method of this is through a phishing email where the adversary embeds the malicious script with a URL that the victim then clicks on. In processing the subsequent request, the vulnerable web application incorrectly considers the malicious script as valid input and uses it to creates a reposnse that is then sent back to the victim. To launch a successful Reflected XSS attack, an adversary looks for places where user-input is used directly in the generation of a response. This often involves elements that are not expected to host scripts such as image tags (), or the addition of event attibutes such as onload and onmouseover. These elements are often not subject to the same input validation, output encoding, and other content filtering and checking routines.", "Likelihood Of Attack": "High", "severity": "Very High", - "condition": "target.allowsClientSideScripting is True and (target.sanitizesInput is False or target.validatesInput is False)", + "condition": "target.allowsClientSideScripting is True and (target.controls.sanitizesInput is False or target.controls.validatesInput is False)", "prerequisites": "An application that leverages a client-side web browser with scripting enabled.An application that fail to adequately sanitize or encode untrusted input.", "mitigations": "Use browser technologies that do not allow client-side scripting.Utilize strict type, character, and encoding enforcement.Ensure that all user-supplied input is validated before use.", "example": "Consider a web application that enables or disables some of the fields of a form on the page via the use of a mode parameter provided on the query string. http://my.site.com/aform.html?mode=full The application’s server-side code may want to display this mode value in the HTML page being created to give the users an understanding of what mode they are in. In this example, PHP is used to pull the value from the URL and generate the desired HTML. Notice how the value provided on the URL is used directly with no input validation performed and no output encoding in place. A maliciously crafted URL can thus be formed such that if a victim clicked on the URL, a malicious script would then be executed by the victim’s browser: http://my.site.com/aform.html?mode=Reflected XSS attacks can take advantage of HTTP headers to compromise a victim. For example, assume a vulnerable web application called ‘mysite’ dynamically generates a link using an HTTP header such as HTTP_REFERER. Code somewhere in the application could look like: Test URL?> The HTTP_REFERER header is populated with the URI that linked to the currently executing page. A web site can be created and hosted by an adversary that takes advantage of this by adding a reference to the vulnerable web application. By tricking a victim into clicking a link that executes the attacker’s web page, such as: http://attackerswebsite.com? The vulnerable web application (‘mysite’) is now called via the attacker’s web site, initiated by the victim’s web browser. The HTTP_REFERER header will contain a malicious script, which is embedded into the page by the vulnerable application and served to the victim. The victim’s web browser then executes the injected script, thus compromising the victim’s machine.", @@ -1432,7 +1432,7 @@ "details": "This type of attack is a form of Cross-site Scripting (XSS) where a malicious script is persistenly “stored” within the data storage of a vulnerable web application. Initially presented by an adversary to the vulnerable web application, the malicious script is incorrectly considered valid input and is not properly encoded by the web application. A victim is then convinced to use the web application in a way that creates a response that includes the malicious script. This response is subsequently sent to the victim and the malicious script is executed by the victim's browser. To launch a successful Stored XSS attack, an adversary looks for places where stored input data is used in the generation of a response. This often involves elements that are not expected to host scripts such as image tags (), or the addition of event attibutes such as onload and onmouseover. These elements are often not subject to the same input validation, output encoding, and other content filtering and checking routines.", "Likelihood Of Attack": "High", "severity": "Very High", - "condition": "target.allowsClientSideScripting is True and (target.sanitizesInput is False or target.validatesInput is False)", + "condition": "target.allowsClientSideScripting is True and (target.controls.sanitizesInput is False or target.controls.validatesInput is False)", "prerequisites": "An application that leverages a client-side web browser with scripting enabled.An application that fails to adequately sanitize or encode untrusted input.An application that stores information provided by the user in data storage of some kind.", "mitigations": "Use browser technologies that do not allow client-side scripting.Utilize strict type, character, and encoding enforcement.Ensure that all user-supplied input is validated before being stored.", "example": "An adversary determines that a system uses a web based interface for administration. The adversary creates a new user record and supplies a malicious script in the user name field. The user name field is not validated by the system and a new log entry is created detailing the creation of the new user. Later, an administrator reviews the log in the administrative console. When the administrator comes across the new user entry, the browser sees a script and executes it, stealing the administrator's authentication cookie and forwarding it to the adversary. An adversary then uses the received authentication cookie to log in to the system as an administrator, provided that the administrator console can be accessed remotely.An online discussion forum allows its members to post HTML-enabled messages, which can also include image tags. An adversary embeds JavaScript in the image tags of his message. The adversary then sends the victim an email advertising free goods and provides a link to the form for how to collect. When the victim visits the forum and reads the message, the malicious script is executed within the victim's browser.", @@ -1448,7 +1448,7 @@ "Likelihood Of Attack": "High", "severity": "Very High", "prerequisites": "An application that leverages sessions to perform authentication.", - "condition": "target.usesStrongSessionIdentifiers is False", + "condition": "target.controls.usesStrongSessionIdentifiers is False", "mitigations": "Properly encrypt and sign identity tokens in transit, and use industry standard session key generation mechanisms that utilize high amount of entropy to generate the session key. Many standard web and application servers will perform this task on your behalf. Utilize a session timeout for all sessions. If the user does not explicitly logout, terminate their session after this period of inactivity. If the user logs back in then a new session key should be generated.", "example": "Cross Site Injection Attack is a great example of Session Hijacking. Attacker can capture victim’s Session ID using XSS attack by using javascript. If an attacker sends a crafted link to the victim with the malicious JavaScript, when the victim clicks on the link, the JavaScript will run and complete the instructions made by the attacker.", "references": "https://capec.mitre.org/data/definitions/593.html" @@ -1463,7 +1463,7 @@ "Likelihood Of Attack": "High", "severity": "Very High", "prerequisites": "An application that leverages sessions to perform authentication.", - "condition": "(target.usesStrongSessionIdentifiers is False or target.encryptsCookies is False) and target.definesConnectionTimeout is False", + "condition": "(target.controls.usesStrongSessionIdentifiers is False or target.controls.encryptsCookies is False) and target.controls.definesConnectionTimeout is False", "mitigations": "Properly encrypt and sign identity tokens in transit, and use industry standard session key generation mechanisms that utilize high amount of entropy to generate the session key. Many standard web and application servers will perform this task on your behalf. Utilize a session timeout for all sessions. If the user does not explicitly logout, terminate their session after this period of inactivity. If the user logs back in then a new session key should be generated.", "example": "Cross Site Injection Attack is a great example of Session Hijacking. Attacker can capture victim’s Session ID using XSS attack by using javascript. If an attacker sends a crafted link to the victim with the malicious JavaScript, when the victim clicks on the link, the JavaScript will run and complete the instructions made by the attacker.", "references": "https://capec.mitre.org/data/definitions/593.html" @@ -1477,7 +1477,7 @@ "details": "An attacker changes the behavior or state of a targeted application through injecting data or command syntax through the targets use of non-validated and non-filtered arguments of exposed services or methods.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.validatesInput is False or target.sanitizesInput is False", + "condition": "target.controls.validatesInput is False or target.controls.sanitizesInput is False", "prerequisites": "Target software fails to strip all user-supplied input of any content that could cause the shell to perform unexpected actions.Software must allow for unvalidated or unfiltered input to be executed on operating system shell, and, optionally, the system configuration must allow for output to be sent back to client.", "mitigations": "Design: Do not program input values directly on command shell, instead treat user input as guilty until proven innocent. Build a function that takes user input and converts it to applications specific types and values, stripping or filtering out all unauthorized commands and characters in the process.Design: Limit program privileges, so if metacharacters or other methods circumvent program input validation routines and shell access is attained then it is not running under a privileged account. chroot jails create a sandbox for the application to execute in, making it more difficult for an attacker to elevate privilege even in the case that a compromise has occurred.Implementation: Implement an audit log that is written to a separate host, in the event of a compromise the audit log may be able to provide evidence and details of the compromise.", "example": "A recent example instance of argument injection occurred against Java Web Start technology, which eases the client side deployment for Java programs. The JNLP files that are used to describe the properties for the program. The client side Java runtime used the arguments in the property setting to define execution parameters, but if the attacker appends commands to an otherwise legitimate property file, then these commands are sent to the client command shell. [R.6.2]", @@ -1492,7 +1492,7 @@ "details": "This attack targets the reuse of valid session ID to spoof the target system in order to gain privileges. The attacker tries to reuse a stolen session ID used previously during a transaction to perform spoofing and session hijacking. Another name for this type of attack is Session Replay.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.usesSessionTokens is True and target.implementsNonce is False", + "condition": "target.usesSessionTokens is True and target.controls.implementsNonce is False", "prerequisites": "The target host uses session IDs to keep track of the users.Session IDs are used to control access to resources.The session IDs used by the target host are not well protected from session theft.", "mitigations": "Always invalidate a session ID after the user logout.Setup a session time out for the session IDs.Protect the communication between the client and server. For instance it is best practice to use SSL to mitigate man in the middle attack.Do not code send session ID with GET method, otherwise the session ID will be copied to the URL. In general avoid writing session IDs in the URLs. URLs can get logged in log files, which are vulnerable to an attacker.Encrypt the session data associated with the session ID.Use multifactor authentication.", "example": "OpenSSL and SSLeay allow remote attackers to reuse SSL sessions and bypass access controls. See also: CVE-1999-0428Merak Mail IceWarp Web Mail uses a static identifier as a user session ID that does not change across sessions, which could allow remote attackers with access to the ID to gain privileges as that user, e.g. by extracting the ID from the user's answer or forward URLs. See also: CVE-2002-0258", @@ -1507,7 +1507,7 @@ "details": "This attack targets the reuse of valid session ID to spoof the target system in order to gain privileges. The attacker tries to reuse a stolen session ID used previously during a transaction to perform spoofing and session hijacking. Another name for this type of attack is Session Replay.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.definesConnectionTimeout is False and (target.usesMFA is False or target.encryptsSessionData is False)", + "condition": "target.controls.definesConnectionTimeout is False and (target.controls.usesMFA is False or target.controls.encryptsSessionData is False)", "prerequisites": "The target host uses session IDs to keep track of the users.Session IDs are used to control access to resources.The session IDs used by the target host are not well protected from session theft.", "mitigations": "Always invalidate a session ID after the user logout.Setup a session time out for the session IDs.Protect the communication between the client and server. For instance it is best practice to use SSL to mitigate man in the middle attack.Do not code send session ID with GET method, otherwise the session ID will be copied to the URL. In general avoid writing session IDs in the URLs. URLs can get logged in log files, which are vulnerable to an attacker.Encrypt the session data associated with the session ID.Use multifactor authentication.", "example": "OpenSSL and SSLeay allow remote attackers to reuse SSL sessions and bypass access controls. See also: CVE-1999-0428Merak Mail IceWarp Web Mail uses a static identifier as a user session ID that does not change across sessions, which could allow remote attackers with access to the ID to gain privileges as that user, e.g. by extracting the ID from the user's answer or forward URLs. See also: CVE-2002-0258", @@ -1523,7 +1523,7 @@ "Likelihood Of Attack": "High", "severity": "Very High", "prerequisites": "", - "condition": "target.implementsCSRFToken is False or target.verifySessionIdentifiers is False", + "condition": "target.controls.implementsCSRFToken is False or target.controls.verifySessionIdentifiers is False", "mitigations": "Use cryptographic tokens to associate a request with a specific action. The token can be regenerated at every request so that if a request with an invalid token is encountered, it can be reliably discarded. The token is considered invalid if it arrived with a request other than the action it was supposed to be associated with.Although less reliable, the use of the optional HTTP Referrer header can also be used to determine whether an incoming request was actually one that the user is authorized for, in the current context.Additionally, the user can also be prompted to confirm an action every time an action concerning potentially sensitive data is invoked. This way, even if the attacker manages to get the user to click on a malicious link and request the desired action, the user has a chance to recover by denying confirmation. This solution is also implicitly tied to using a second factor of authentication before performing such actions.In general, every request must be checked for the appropriate authentication token as well as authorization in the current session context.", "example": "While a user is logged into his bank account, an attacker can send an email with some potentially interesting content and require the user to click on a link in the email. The link points to or contains an attacker setup script, probably even within an iFrame, that mimics an actual user form submission to perform a malicious activity, such as transferring funds from the victim's account. The attacker can have the script embedded in, or targeted by, the link perform any arbitrary action as the authenticated user. When this script is executed, the targeted application authenticates and accepts the actions based on the victims existing session cookie.See also: Cross-site request forgery (CSRF) vulnerability in util.pl in @Mail WebMail 4.51 allows remote attackers to modify arbitrary settings and perform unauthorized actions as an arbitrary user, as demonstrated using a settings action in the SRC attribute of an IMG element in an HTML e-mail.", "references": "https://capec.mitre.org/data/definitions/62.html" @@ -1553,7 +1553,7 @@ "Likelihood Of Attack": "Low", "severity": "High", "prerequisites": "", - "condition": "(target.hasDataLeaks() or any(d.isCredentials or d.isPII for d in target.data)) and (not target.isEncrypted or (not target.isResponse and any(d.isStored and d.isDestEncryptedAtRest for d in target.data)) or (target.isResponse and any(d.isStored and d.isSourceEncryptedAtRest for d in target.data)))", + "condition": "(target.hasDataLeaks() or any(d.isCredentials or d.isPII for d in target.data)) and (not target.controls.isEncrypted or (not target.isResponse and any(d.isStored and d.isDestEncryptedAtRest for d in target.data)) or (target.isResponse and any(d.isStored and d.isSourceEncryptedAtRest for d in target.data)))", "mitigations": "All data should be encrypted in transit. All PII and restricted data must be encrypted at rest. If a service is storing credentials used to authenticate users or incoming connections, it must only store hashes of them created using cryptographic functions, so it is only possible to compare them against user input, without fully decoding them. If a client is storing credentials in either files or other data store, access to them must be as restrictive as possible, including using proper file permissions, database users with restricted access or separate storage.", "example": "", "references": "https://cwe.mitre.org/data/definitions/311.html, https://cwe.mitre.org/data/definitions/312.html, https://cwe.mitre.org/data/definitions/916.html, https://cwe.mitre.org/data/definitions/653.html" diff --git a/tests/output.json b/tests/output.json index 0adee11b..c2eeaeda 100644 --- a/tests/output.json +++ b/tests/output.json @@ -2,8 +2,51 @@ "actors": [ { "__class__": "Actor", - "authenticatesDestination": false, - "checksDestinationRevocation": false, + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "data": [], "description": "", "findings": [], @@ -25,7 +68,6 @@ "overrides": [], "port": -1, "protocol": "", - "providesIntegrity": false, "sourceFiles": [] } ], @@ -33,38 +75,61 @@ { "OS": "", "__class__": "Server", - "authenticatesDestination": false, - "authenticatesSource": false, - "authenticationScheme": "", - "authorizesSource": false, - "checksDestinationRevocation": false, - "checksInputBounds": false, + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "data": [], - "definesConnectionTimeout": false, "description": "", - "disablesDTD": false, - "encodesHeaders": false, - "encodesOutput": false, "findings": [], - "handlesResourceConsumption": false, "handlesResources": false, - "hasAccessControl": false, - "implementsAuthenticationScheme": false, - "implementsCSRFToken": false, - "implementsNonce": false, - "implementsPOLP": false, - "implementsServerSideValidation": false, - "implementsStrictHTTPValidation": false, "inBoundary": null, "inScope": true, "inputs": [ "User enters comments (*)", "Retrieve comments" ], - "invokesScriptFilters": false, - "isEncrypted": false, - "isHardened": false, - "isResilient": false, "levels": [ 0 ], @@ -80,50 +145,72 @@ "overrides": [], "port": -1, "protocol": "", - "providesConfidentiality": false, - "providesIntegrity": false, - "sanitizesInput": false, "sourceFiles": [], "usesCache": false, - "usesCodeSigning": false, - "usesEncryptionAlgorithm": "", "usesEnvironmentVariables": false, "usesSessionTokens": false, - "usesStrongSessionIdentifiers": false, "usesVPN": false, - "usesXMLParser": false, - "validatesContentType": false, - "validatesHeaders": false, - "validatesInput": false + "usesXMLParser": false }, { "OS": "", "__class__": "Lambda", - "authenticatesDestination": false, - "authenticatesSource": false, - "authenticationScheme": "", - "authorizesSource": false, - "checksDestinationRevocation": false, - "checksInputBounds": false, + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "data": [], - "definesConnectionTimeout": false, "description": "", - "encodesOutput": false, "environment": "", "findings": [], - "handlesResourceConsumption": false, "handlesResources": false, - "hasAccessControl": false, "implementsAPI": false, - "implementsAuthenticationScheme": false, - "implementsNonce": false, "inBoundary": null, "inScope": true, "inputs": [ "Call func" ], - "isEncrypted": false, - "isHardened": false, "levels": [ 0 ], @@ -135,49 +222,69 @@ "overrides": [], "port": -1, "protocol": "", - "providesIntegrity": false, - "sanitizesInput": false, "sourceFiles": [], - "usesEnvironmentVariables": false, - "validatesInput": false + "usesEnvironmentVariables": false }, { "OS": "", "__class__": "Process", "allowsClientSideScripting": false, - "authenticatesDestination": false, - "authenticatesSource": false, - "authenticationScheme": "", - "authorizesSource": false, - "checksDestinationRevocation": false, - "checksInputBounds": false, "codeType": "Unmanaged", + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "data": [], - "definesConnectionTimeout": false, "description": "", - "disablesiFrames": false, - "encodesOutput": false, - "encryptsCookies": false, - "encryptsSessionData": false, "environment": "", "findings": [], - "handlesCrashes": false, - "handlesInterruptions": false, - "handlesResourceConsumption": false, "handlesResources": false, - "hasAccessControl": false, "implementsAPI": false, - "implementsAuthenticationScheme": false, - "implementsCSRFToken": false, "implementsCommunicationProtocol": false, - "implementsNonce": false, - "implementsPOLP": false, "inBoundary": null, "inScope": true, "inputs": [], - "isEncrypted": false, - "isHardened": false, - "isResilient": false, "levels": [ 0 ], @@ -191,51 +298,69 @@ "overrides": [], "port": -1, "protocol": "", - "providesConfidentiality": false, - "providesIntegrity": false, - "sanitizesInput": false, "sourceFiles": [], "tracksExecutionFlow": false, - "usesEnvironmentVariables": false, - "usesMFA": false, - "usesParameterizedInput": false, - "usesSecureFunctions": false, - "usesStrongSessionIdentifiers": false, - "validatesInput": false, - "verifySessionIdentifiers": false + "usesEnvironmentVariables": false }, { "OS": "", "__class__": "Datastore", - "authenticatesDestination": false, - "authenticatesSource": false, - "authenticationScheme": "", - "authorizesSource": false, - "checksDestinationRevocation": false, - "checksInputBounds": false, + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "data": [], - "definesConnectionTimeout": false, "description": "", - "encodesOutput": false, "findings": [], - "handlesInterruptions": false, - "handlesResourceConsumption": false, "handlesResources": false, - "hasAccessControl": false, "hasWriteAccess": false, - "implementsAuthenticationScheme": false, - "implementsNonce": false, - "implementsPOLP": false, "inBoundary": "Server/DB", "inScope": true, "inputs": [ "Insert query with comments", "Query for tasks" ], - "isEncrypted": false, - "isEncryptedAtRest": false, - "isHardened": false, - "isResilient": false, "isSQL": true, "isShared": false, "levels": [ @@ -252,20 +377,60 @@ "overrides": [], "port": -1, "protocol": "", - "providesConfidentiality": false, - "providesIntegrity": false, - "sanitizesInput": false, "sourceFiles": [], "storesLogData": false, "storesPII": false, "storesSensitiveData": false, - "usesEncryptionAlgorithm": "", - "usesEnvironmentVariables": false, - "validatesInput": false + "usesEnvironmentVariables": false } ], "boundaries": [ { + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "description": "", "findings": [], "inBoundary": null, @@ -280,6 +445,51 @@ "sourceFiles": [] }, { + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "description": "", "findings": [], "inBoundary": null, @@ -319,8 +529,51 @@ "elements": [ { "__class__": "Actor", - "authenticatesDestination": false, - "checksDestinationRevocation": false, + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "data": [], "description": "", "findings": [], @@ -342,44 +595,66 @@ "overrides": [], "port": -1, "protocol": "", - "providesIntegrity": false, "sourceFiles": [] }, { "OS": "", "__class__": "Server", - "authenticatesDestination": false, - "authenticatesSource": false, - "authenticationScheme": "", - "authorizesSource": false, - "checksDestinationRevocation": false, - "checksInputBounds": false, + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "data": [], - "definesConnectionTimeout": false, "description": "", - "disablesDTD": false, - "encodesHeaders": false, - "encodesOutput": false, "findings": [], - "handlesResourceConsumption": false, "handlesResources": false, - "hasAccessControl": false, - "implementsAuthenticationScheme": false, - "implementsCSRFToken": false, - "implementsNonce": false, - "implementsPOLP": false, - "implementsServerSideValidation": false, - "implementsStrictHTTPValidation": false, "inBoundary": null, "inScope": true, "inputs": [ "User enters comments (*)", "Retrieve comments" ], - "invokesScriptFilters": false, - "isEncrypted": false, - "isHardened": false, - "isResilient": false, "levels": [ 0 ], @@ -395,50 +670,72 @@ "overrides": [], "port": -1, "protocol": "", - "providesConfidentiality": false, - "providesIntegrity": false, - "sanitizesInput": false, "sourceFiles": [], "usesCache": false, - "usesCodeSigning": false, - "usesEncryptionAlgorithm": "", "usesEnvironmentVariables": false, "usesSessionTokens": false, - "usesStrongSessionIdentifiers": false, "usesVPN": false, - "usesXMLParser": false, - "validatesContentType": false, - "validatesHeaders": false, - "validatesInput": false + "usesXMLParser": false }, { "OS": "", "__class__": "Lambda", - "authenticatesDestination": false, - "authenticatesSource": false, - "authenticationScheme": "", - "authorizesSource": false, - "checksDestinationRevocation": false, - "checksInputBounds": false, + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "data": [], - "definesConnectionTimeout": false, "description": "", - "encodesOutput": false, "environment": "", "findings": [], - "handlesResourceConsumption": false, "handlesResources": false, - "hasAccessControl": false, "implementsAPI": false, - "implementsAuthenticationScheme": false, - "implementsNonce": false, "inBoundary": null, "inScope": true, "inputs": [ "Call func" ], - "isEncrypted": false, - "isHardened": false, "levels": [ 0 ], @@ -450,49 +747,69 @@ "overrides": [], "port": -1, "protocol": "", - "providesIntegrity": false, - "sanitizesInput": false, "sourceFiles": [], - "usesEnvironmentVariables": false, - "validatesInput": false + "usesEnvironmentVariables": false }, { "OS": "", "__class__": "Process", "allowsClientSideScripting": false, - "authenticatesDestination": false, - "authenticatesSource": false, - "authenticationScheme": "", - "authorizesSource": false, - "checksDestinationRevocation": false, - "checksInputBounds": false, "codeType": "Unmanaged", + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "data": [], - "definesConnectionTimeout": false, "description": "", - "disablesiFrames": false, - "encodesOutput": false, - "encryptsCookies": false, - "encryptsSessionData": false, "environment": "", "findings": [], - "handlesCrashes": false, - "handlesInterruptions": false, - "handlesResourceConsumption": false, "handlesResources": false, - "hasAccessControl": false, "implementsAPI": false, - "implementsAuthenticationScheme": false, - "implementsCSRFToken": false, "implementsCommunicationProtocol": false, - "implementsNonce": false, - "implementsPOLP": false, "inBoundary": null, "inScope": true, "inputs": [], - "isEncrypted": false, - "isHardened": false, - "isResilient": false, "levels": [ 0 ], @@ -506,51 +823,69 @@ "overrides": [], "port": -1, "protocol": "", - "providesConfidentiality": false, - "providesIntegrity": false, - "sanitizesInput": false, "sourceFiles": [], "tracksExecutionFlow": false, - "usesEnvironmentVariables": false, - "usesMFA": false, - "usesParameterizedInput": false, - "usesSecureFunctions": false, - "usesStrongSessionIdentifiers": false, - "validatesInput": false, - "verifySessionIdentifiers": false + "usesEnvironmentVariables": false }, { "OS": "", "__class__": "Datastore", - "authenticatesDestination": false, - "authenticatesSource": false, - "authenticationScheme": "", - "authorizesSource": false, - "checksDestinationRevocation": false, - "checksInputBounds": false, + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "data": [], - "definesConnectionTimeout": false, "description": "", - "encodesOutput": false, "findings": [], - "handlesInterruptions": false, - "handlesResourceConsumption": false, "handlesResources": false, - "hasAccessControl": false, "hasWriteAccess": false, - "implementsAuthenticationScheme": false, - "implementsNonce": false, - "implementsPOLP": false, "inBoundary": "Server/DB", "inScope": true, "inputs": [ "Insert query with comments", "Query for tasks" ], - "isEncrypted": false, - "isEncryptedAtRest": false, - "isHardened": false, - "isResilient": false, "isSQL": true, "isShared": false, "levels": [ @@ -567,36 +902,70 @@ "overrides": [], "port": -1, "protocol": "", - "providesConfidentiality": false, - "providesIntegrity": false, - "sanitizesInput": false, "sourceFiles": [], "storesLogData": false, "storesPII": false, "storesSensitiveData": false, - "usesEncryptionAlgorithm": "", - "usesEnvironmentVariables": false, - "validatesInput": false + "usesEnvironmentVariables": false } ], "findings": [], "flows": [ { - "authenticatedWith": false, - "authenticatesDestination": false, - "authorizesSource": false, - "checksDestinationRevocation": false, + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "data": [ "auth cookie" ], "description": "", "dstPort": -1, "findings": [], - "implementsAuthenticationScheme": false, "implementsCommunicationProtocol": false, "inBoundary": null, "inScope": true, - "isEncrypted": false, "isResponse": false, "levels": [ 0 @@ -619,19 +988,58 @@ "usesVPN": false }, { - "authenticatedWith": false, - "authenticatesDestination": false, - "authorizesSource": false, - "checksDestinationRevocation": false, + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "data": [], "description": "", "dstPort": -1, "findings": [], - "implementsAuthenticationScheme": false, "implementsCommunicationProtocol": false, "inBoundary": null, "inScope": true, - "isEncrypted": false, "isResponse": false, "levels": [ 0 @@ -654,19 +1062,58 @@ "usesVPN": false }, { - "authenticatedWith": false, - "authenticatesDestination": false, - "authorizesSource": false, - "checksDestinationRevocation": false, + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "data": [], "description": "", "dstPort": -1, "findings": [], - "implementsAuthenticationScheme": false, "implementsCommunicationProtocol": false, "inBoundary": null, "inScope": true, - "isEncrypted": false, "isResponse": false, "levels": [ 0 @@ -689,19 +1136,58 @@ "usesVPN": false }, { - "authenticatedWith": false, - "authenticatesDestination": false, - "authorizesSource": false, - "checksDestinationRevocation": false, + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "data": [], "description": "", "dstPort": -1, "findings": [], - "implementsAuthenticationScheme": false, "implementsCommunicationProtocol": false, "inBoundary": null, "inScope": true, - "isEncrypted": false, "isResponse": false, "levels": [ 0 @@ -724,19 +1210,58 @@ "usesVPN": false }, { - "authenticatedWith": false, - "authenticatesDestination": false, - "authorizesSource": false, - "checksDestinationRevocation": false, + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "data": [], "description": "", "dstPort": -1, "findings": [], - "implementsAuthenticationScheme": false, "implementsCommunicationProtocol": false, "inBoundary": null, "inScope": true, - "isEncrypted": false, "isResponse": false, "levels": [ 0 @@ -759,19 +1284,58 @@ "usesVPN": false }, { - "authenticatedWith": false, - "authenticatesDestination": false, - "authorizesSource": false, - "checksDestinationRevocation": false, + "controls": { + "authenticatesDestination": false, + "authenticatesSource": false, + "authenticationScheme": "", + "authorizesSource": false, + "checksDestinationRevocation": false, + "checksInputBounds": false, + "definesConnectionTimeout": false, + "disablesDTD": false, + "disablesiFrames": false, + "encodesHeaders": false, + "encodesOutput": false, + "encryptsCookies": false, + "encryptsSessionData": false, + "handlesCrashes": false, + "handlesInterruptions": false, + "handlesResourceConsumption": false, + "hasAccessControl": false, + "implementsAuthenticationScheme": false, + "implementsCSRFToken": false, + "implementsNonce": false, + "implementsPOLP": false, + "implementsServerSideValidation": false, + "implementsStrictHTTPValidation": false, + "invokesScriptFilters": false, + "isEncrypted": false, + "isEncryptedAtRest": false, + "isHardened": false, + "isResilient": false, + "providesConfidentiality": false, + "providesIntegrity": false, + "sanitizesInput": false, + "tracksExecutionFlow": false, + "usesCodeSigning": false, + "usesEncryptionAlgorithm": "", + "usesMFA": false, + "usesParameterizedInput": false, + "usesSecureFunctions": false, + "usesStrongSessionIdentifiers": false, + "usesVPN": false, + "validatesContentType": false, + "validatesHeaders": false, + "validatesInput": false, + "verifySessionIdentifiers": false + }, "data": [], "description": "", "dstPort": -1, "findings": [], - "implementsAuthenticationScheme": false, "implementsCommunicationProtocol": false, "inBoundary": null, "inScope": true, - "isEncrypted": false, "isResponse": false, "levels": [ 0 @@ -801,4 +1365,4 @@ "onDuplicates": "Action.NO_ACTION", "threatsExcluded": [], "threatsFile": "pytm/threatlib/threats.json" -} +} \ No newline at end of file diff --git a/tests/test_private_func.py b/tests/test_private_func.py index 98f08a0c..17951fd4 100644 --- a/tests/test_private_func.py +++ b/tests/test_private_func.py @@ -80,7 +80,9 @@ def test_responses(self): def test_defaults(self): tm = TM("TM") user_data = Data("HTTP") - user = Actor("User", data=user_data, authenticatesDestination=True) + user = Actor("User", data=user_data) + user.controls.authenticatesDestination=True + json_data = Data("JSON") server = Server( "Server", port=443, protocol="HTTPS", isEncrypted=True, data=json_data @@ -91,9 +93,10 @@ def test_defaults(self): isSQL=True, port=5432, protocol="PostgreSQL", - isEncrypted=False, data=sql_resp, ) + db.controls.isEncrypted=False + worker = Process("Task queue worker") req_get_data = Data("HTTP GET") @@ -119,49 +122,49 @@ def test_defaults(self): self.assertEqual(req_get.srcPort, -1) self.assertEqual(req_get.dstPort, server.port) - self.assertEqual(req_get.isEncrypted, server.isEncrypted) + self.assertEqual(req_get.controls.isEncrypted, server.controls.isEncrypted) self.assertEqual( - req_get.authenticatesDestination, user.authenticatesDestination + req_get.controls.authenticatesDestination, user.controls.authenticatesDestination ) self.assertEqual(req_get.protocol, server.protocol) self.assertTrue(user.data.issubset(req_get.data)) self.assertEqual(server_query.srcPort, -1) self.assertEqual(server_query.dstPort, db.port) - self.assertEqual(server_query.isEncrypted, db.isEncrypted) + self.assertEqual(server_query.controls.isEncrypted, db.controls.isEncrypted) self.assertEqual( - server_query.authenticatesDestination, server.authenticatesDestination + server_query.controls.authenticatesDestination, server.controls.authenticatesDestination ) self.assertEqual(server_query.protocol, db.protocol) self.assertTrue(server.data.issubset(server_query.data)) self.assertEqual(result.srcPort, db.port) self.assertEqual(result.dstPort, -1) - self.assertEqual(result.isEncrypted, db.isEncrypted) - self.assertEqual(result.authenticatesDestination, False) + self.assertEqual(result.controls.isEncrypted, db.controls.isEncrypted) + self.assertEqual(result.controls.authenticatesDestination, False) self.assertEqual(result.protocol, db.protocol) self.assertTrue(db.data.issubset(result.data)) self.assertEqual(resp_get.srcPort, server.port) self.assertEqual(resp_get.dstPort, -1) - self.assertEqual(resp_get.isEncrypted, server.isEncrypted) - self.assertEqual(resp_get.authenticatesDestination, False) + self.assertEqual(resp_get.controls.isEncrypted, server.controls.isEncrypted) + self.assertEqual(resp_get.controls.authenticatesDestination, False) self.assertEqual(resp_get.protocol, server.protocol) self.assertTrue(server.data.issubset(resp_get.data)) self.assertEqual(req_post.srcPort, -1) self.assertEqual(req_post.dstPort, server.port) - self.assertEqual(req_post.isEncrypted, server.isEncrypted) + self.assertEqual(req_post.controls.isEncrypted, server.controls.isEncrypted) self.assertEqual( - req_post.authenticatesDestination, user.authenticatesDestination + req_post.controls.authenticatesDestination, user.controls.authenticatesDestination ) self.assertEqual(req_post.protocol, server.protocol) self.assertTrue(user.data.issubset(req_post.data)) self.assertEqual(resp_post.srcPort, server.port) self.assertEqual(resp_post.dstPort, -1) - self.assertEqual(resp_post.isEncrypted, server.isEncrypted) - self.assertEqual(resp_post.authenticatesDestination, False) + self.assertEqual(resp_post.controls.isEncrypted, server.controls.isEncrypted) + self.assertEqual(resp_post.controls.authenticatesDestination, False) self.assertEqual(resp_post.protocol, server.protocol) self.assertTrue(server.data.issubset(resp_post.data)) diff --git a/tests/test_pytmfunc.py b/tests/test_pytmfunc.py index f0e3e90a..28d400da 100644 --- a/tests/test_pytmfunc.py +++ b/tests/test_pytmfunc.py @@ -437,25 +437,25 @@ def test_INP01(self): lambda1 = Lambda("mylambda") process1 = Process("myprocess") lambda1.usesEnvironmentVariables = True - lambda1.sanitizesInput = False - lambda1.checksInputBounds = False + lambda1.controls.sanitizesInput = False + lambda1.controls.checksInputBounds = False process1.usesEnvironmentVariables = True - process1.sanitizesInput = False - process1.checksInputBounds = False + process1.controls.sanitizesInput = False + process1.controls.checksInputBounds = False threat = threats["INP01"] self.assertTrue(threat.apply(lambda1)) self.assertTrue(threat.apply(process1)) def test_INP02(self): process1 = Process("myprocess") - process1.checksInputBounds = False + process1.controls.checksInputBounds = False threat = threats["INP02"] self.assertTrue(threat.apply(process1)) def test_INP03(self): web = Server("Web") - web.sanitizesInput = False - web.encodesOutput = False + web.controls.sanitizesInput = False + web.controls.encodesOutput = False threat = threats["INP03"] self.assertTrue(threat.apply(web)) @@ -475,8 +475,8 @@ def test_CR01(self): def test_INP04(self): web = Server("Web Server") - web.validatesInput = False - web.validatesHeaders = False + web.controls.validatesInput = False + web.controls.validatesHeaders = False web.protocol = "HTTP" threat = threats["INP04"] self.assertTrue(threat.apply(web)) @@ -485,13 +485,13 @@ def test_CR02(self): user = Actor("User") web = Server("Web Server") web.protocol = "HTTP" - web.sanitizesInput = False - web.validatesInput = False + web.controls.sanitizesInput = False + web.controls.validatesInput = False web.usesSessionTokens = True user_to_web = Dataflow(user, web, "User enters comments (*)") user_to_web.protocol = "HTTP" - user_to_web.sanitizesInput = False - user_to_web.validatesInput = False + user_to_web.controls.sanitizesInput = False + user_to_web.controls.validatesInput = False user_to_web.usesSessionTokens = True threat = threats["CR02"] self.assertTrue(threat.apply(web)) @@ -499,15 +499,15 @@ def test_CR02(self): def test_INP05(self): web = Server("Web Server") - web.validatesInput = False + web.controls.validatesInput = False threat = threats["INP05"] self.assertTrue(threat.apply(web)) def test_INP06(self): web = Server("Web Server") web.protocol = "SOAP" - web.sanitizesInput = False - web.validatesInput = False + web.controls.sanitizesInput = False + web.controls.validatesInput = False threat = threats["INP06"] self.assertTrue(threat.apply(web)) @@ -522,12 +522,12 @@ def test_SC01(self): def test_LB01(self): process1 = Process("Process1") process1.implementsAPI = True - process1.validatesInput = False - process1.sanitizesInput = False + process1.controls.validatesInput = False + process1.controls.sanitizesInput = False lambda1 = Lambda("Lambda1") lambda1.implementsAPI = True - lambda1.validatesInput = False - lambda1.sanitizesInput = False + lambda1.controls.validatesInput = False + lambda1.controls.sanitizesInput = False threat = threats["LB01"] self.assertTrue(threat.apply(process1)) self.assertTrue(threat.apply(lambda1)) @@ -543,9 +543,9 @@ def test_AA01(self): def test_DS01(self): web = Server("Web Server") - web.sanitizesInput = False - web.validatesInput = False - web.encodesOutput = False + web.controls.sanitizesInput = False + web.controls.validatesInput = False + web.controls.encodesOutput = False threat = threats["DS01"] self.assertTrue(threat.apply(web)) @@ -554,17 +554,17 @@ def test_DE01(self): web = Server("Web Server") user_to_web = Dataflow(user, web, "User enters comments (*)") user_to_web.protocol = "HTTP" - user_to_web.isEncrypted = False + user_to_web.controls.isEncrypted = False threat = threats["DE01"] self.assertTrue(threat.apply(user_to_web)) def test_DE02(self): web = Server("Web Server") process1 = Process("Process1") - web.validatesInput = False - web.sanitizesInput = False - process1.validatesInput = False - process1.sanitizesInput = False + web.controls.validatesInput = False + web.controls.sanitizesInput = False + process1.controls.validatesInput = False + process1.controls.sanitizesInput = False threat = threats["DE02"] self.assertTrue(threat.apply(web)) self.assertTrue(threat.apply(process1)) @@ -582,12 +582,12 @@ def test_AC01(self): web = Server("Web Server") process1 = Process("Process1") db = Datastore("DB") - web.hasAccessControl = False - web.authorizesSource = True - process1.hasAccessControl = False - process1.authorizesSource = False - db.hasAccessControl = False - db.authorizesSource = False + web.controls.hasAccessControl = False + web.controls.authorizesSource = True + process1.controls.hasAccessControl = False + process1.controls.authorizesSource = False + db.controls.hasAccessControl = False + db.controls.authorizesSource = False threat = threats["AC01"] self.assertTrue(threat.apply(process1)) self.assertTrue(threat.apply(web)) @@ -595,7 +595,7 @@ def test_AC01(self): def test_INP07(self): process1 = Process("Process1") - process1.usesSecureFunctions = False + process1.controls.usesSecureFunctions = False threat = threats["INP07"] self.assertTrue(threat.apply(process1)) @@ -608,8 +608,8 @@ def test_AC02(self): def test_DO01(self): process1 = Process("Process1") web = Server("Web Server") - process1.handlesResourceConsumption = False - process1.isResilient = False + process1.controls.handlesResourceConsumption = False + process1.controls.isResilient = False web.handlesResourceConsumption = True threat = threats["DO01"] self.assertTrue(threat.apply(process1)) @@ -617,8 +617,8 @@ def test_DO01(self): def test_HA01(self): web = Server("Web Server") - web.validatesInput = False - web.sanitizesInput = False + web.controls.validatesInput = False + web.controls.sanitizesInput = False threat = threats["HA01"] self.assertTrue(threat.apply(web)) @@ -626,13 +626,13 @@ def test_AC03(self): process1 = Process("Process1") lambda1 = Lambda("Lambda1") process1.usesEnvironmentVariables = True - process1.implementsAuthenticationScheme = False - process1.validatesInput = False - process1.authorizesSource = False + process1.controls.implementsAuthenticationScheme = False + process1.controls.validatesInput = False + process1.controls.authorizesSource = False lambda1.usesEnvironmentVariables = True - lambda1.implementsAuthenticationScheme = False - lambda1.validatesInput = False - lambda1.authorizesSource = False + lambda1.controls.implementsAuthenticationScheme = False + lambda1.controls.validatesInput = False + lambda1.controls.authorizesSource = False threat = threats["AC03"] self.assertTrue(threat.apply(process1)) self.assertTrue(threat.apply(lambda1)) @@ -642,10 +642,10 @@ def test_DO02(self): lambda1 = Lambda("Lambda1") web = Server("Web Server") db = Datastore("DB") - process1.handlesResourceConsumption = False - lambda1.handlesResourceConsumption = False - web.handlesResourceConsumption = False - db.handlesResourceConsumption = False + process1.controls.handlesResourceConsumption = False + lambda1.controls.handlesResourceConsumption = False + web.controls.handlesResourceConsumption = False + db.controls.handlesResourceConsumption = False threat = threats["DO02"] self.assertTrue(threat.apply(process1)) self.assertTrue(threat.apply(lambda1)) @@ -665,12 +665,12 @@ def test_INP08(self): process1 = Process("Process1") lambda1 = Lambda("Lambda1") web = Server("Web Server") - process1.validatesInput = False - process1.sanitizesInput = False - lambda1.validatesInput = False - lambda1.sanitizesInput = False - web.validatesInput = False - web.sanitizesInput = False + process1.controls.validatesInput = False + process1.controls.sanitizesInput = False + lambda1.controls.validatesInput = False + lambda1.controls.sanitizesInput = False + web.controls.validatesInput = False + web.controls.sanitizesInput = False threat = threats["INP08"] self.assertTrue(threat.apply(process1)) self.assertTrue(threat.apply(lambda1)) @@ -678,30 +678,30 @@ def test_INP08(self): def test_INP09(self): web = Server("Web Server") - web.validatesInput = False + web.controls.validatesInput = False threat = threats["INP09"] self.assertTrue(threat.apply(web)) def test_INP10(self): web = Server("Web Server") - web.validatesInput = False + web.controls.validatesInput = False threat = threats["INP10"] self.assertTrue(threat.apply(web)) def test_INP11(self): web = Server("Web Server") - web.validatesInput = False - web.sanitizesInput = False + web.controls.validatesInput = False + web.controls.sanitizesInput = False threat = threats["INP11"] self.assertTrue(threat.apply(web)) def test_INP12(self): process1 = Process("Process1") lambda1 = Lambda("Lambda1") - process1.checksInputBounds = False - process1.validatesInput = False - lambda1.checksInputBounds = False - lambda1.validatesInput = False + process1.controls.checksInputBounds = False + process1.controls.validatesInput = False + lambda1.controls.checksInputBounds = False + lambda1.controls.validatesInput = False threat = threats["INP12"] self.assertTrue(threat.apply(process1)) self.assertTrue(threat.apply(lambda1)) @@ -732,15 +732,15 @@ def test_AC05(self): process1.authenticatesDestination = False proc_to_web = Dataflow(process1, web, "Process calls a web API") proc_to_web.protocol = "HTTPS" - proc_to_web.isEncrypted = True + proc_to_web.controls.isEncrypted = True threat = threats["AC05"] self.assertTrue(threat.apply(proc_to_web)) def test_INP13(self): process1 = Process("Process1") lambda1 = Lambda("Lambda1") - process1.validatesInput = False - lambda1.validatesInput = False + process1.controls.validatesInput = False + lambda1.controls.validatesInput = False threat = threats["INP13"] self.assertTrue(threat.apply(process1)) self.assertTrue(threat.apply(lambda1)) @@ -749,9 +749,9 @@ def test_INP14(self): process1 = Process("Process1") lambda1 = Lambda("Lambda1") web = Server("Web Server") - process1.validatesInput = False - lambda1.validatesInput = False - web.validatesInput = False + process1.controls.validatesInput = False + lambda1.controls.validatesInput = False + web.controls.validatesInput = False threat = threats["INP14"] self.assertTrue(threat.apply(process1)) self.assertTrue(threat.apply(lambda1)) @@ -762,7 +762,7 @@ def test_DE03(self): web = Server("Web Server") user_to_web = Dataflow(user, web, "User enters comments (*)") user_to_web.protocol = "HTTP" - user_to_web.isEncrypted = False + user_to_web.controls.isEncrypted = False user_to_web.usesVPN = False threat = threats["DE03"] self.assertTrue(threat.apply(user_to_web)) @@ -780,9 +780,9 @@ def test_API02(self): process1 = Process("Process1") lambda1 = Lambda("Lambda1") process1.implementsAPI = True - process1.validatesInput = False + process1.controls.validatesInput = False lambda1.implementsAPI = True - lambda1.validatesInput = False + lambda1.controls.validatesInput = False threat = threats["API02"] self.assertTrue(threat.apply(process1)) self.assertTrue(threat.apply(lambda1)) @@ -802,35 +802,35 @@ def test_DS03(self): def test_AC06(self): web = Server("Web Server") web.isHardened = False - web.hasAccessControl = False + web.controls.hasAccessControl = False threat = threats["AC06"] self.assertTrue(threat.apply(web)) def test_HA03(self): web = Server("Web Server") - web.validatesHeaders = False - web.encodesOutput = False + web.controls.validatesHeaders = False + web.controls.encodesOutput = False web.isHardened = False threat = threats["HA03"] self.assertTrue(threat.apply(web)) def test_SC02(self): web = Server("Web Server") - web.validatesInput = False - web.encodesOutput = False + web.controls.validatesInput = False + web.controls.encodesOutput = False threat = threats["SC02"] self.assertTrue(threat.apply(web)) def test_AC07(self): web = Server("Web Server") - web.hasAccessControl = False + web.controls.hasAccessControl = False threat = threats["AC07"] self.assertTrue(threat.apply(web)) def test_INP15(self): web = Server("Web Server") web.protocol = "IMAP" - web.sanitizesInput = False + web.controls.sanitizesInput = False threat = threats["INP15"] self.assertTrue(threat.apply(web)) @@ -842,15 +842,15 @@ def test_HA04(self): def test_SC03(self): web = Server("Web Server") - web.validatesInput = False - web.sanitizesInput = False - web.hasAccessControl = False + web.controls.validatesInput = False + web.controls.sanitizesInput = False + web.controls.hasAccessControl = False threat = threats["SC03"] self.assertTrue(threat.apply(web)) def test_INP16(self): web = Server("Web Server") - web.validatesInput = False + web.controls.validatesInput = False threat = threats["INP16"] self.assertTrue(threat.apply(web)) @@ -883,34 +883,34 @@ def test_DO04(self): def test_DS04(self): web = Server("Web Server") - web.encodesOutput = False - web.validatesInput = False - web.sanitizesInput = False + web.controls.encodesOutput = False + web.controls.validatesInput = False + web.controls.sanitizesInput = False threat = threats["DS04"] self.assertTrue(threat.apply(web)) def test_SC04(self): web = Server("Web Server") - web.sanitizesInput = False - web.validatesInput = False - web.encodesOutput = False + web.controls.sanitizesInput = False + web.controls.validatesInput = False + web.controls.encodesOutput = False threat = threats["SC04"] self.assertTrue(threat.apply(web)) def test_CR05(self): web = Server("Web Server") db = Datastore("db") - web.usesEncryptionAlgorithm != "RSA" - web.usesEncryptionAlgorithm != "AES" - db.usesEncryptionAlgorithm != "RSA" - db.usesEncryptionAlgorithm != "AES" + web.controls.usesEncryptionAlgorithm != "RSA" + web.controls.usesEncryptionAlgorithm != "AES" + db.controls.usesEncryptionAlgorithm != "RSA" + db.controls.usesEncryptionAlgorithm != "AES" threat = threats["CR05"] self.assertTrue(threat.apply(web)) self.assertTrue(threat.apply(db)) def test_AC08(self): web = Server("Web Server") - web.hasAccessControl = False + web.controls.hasAccessControl = False threat = threats["AC08"] self.assertTrue(threat.apply(web)) @@ -964,13 +964,13 @@ def create_dataflow( def test_SC05(self): web = Server("Web Server") web.providesIntegrity = False - web.usesCodeSigning = False + web.controls.usesCodeSigning = False threat = threats["SC05"] self.assertTrue(threat.apply(web)) def test_INP17(self): web = Server("Web Server") - web.validatesContentType = False + web.controls.validatesContentType = False web.invokesScriptFilters = False threat = threats["INP17"] self.assertTrue(threat.apply(web)) @@ -979,21 +979,21 @@ def test_AA03(self): web = Server("Web Server") web.providesIntegrity = False web.authenticatesSource = False - web.usesStrongSessionIdentifiers = False + web.controls.usesStrongSessionIdentifiers = False threat = threats["AA03"] self.assertTrue(threat.apply(web)) def test_AC09(self): web = Server("Web Server") - web.hasAccessControl = False + web.controls.hasAccessControl = False web.authorizesSource = False threat = threats["AC09"] self.assertTrue(threat.apply(web)) def test_INP18(self): web = Server("Web Server") - web.sanitizesInput = False - web.encodesOutput = False + web.controls.sanitizesInput = False + web.controls.encodesOutput = False threat = threats["INP18"] self.assertTrue(threat.apply(web)) @@ -1016,7 +1016,7 @@ def test_AC10(self): web.authorizesSource = False user_to_web = Dataflow(user, web, "User enters comments (*)") user_to_web.protocol = "HTTPS" - user_to_web.isEncrypted = True + user_to_web.controls.isEncrypted = True user_to_web.tlsVersion = TLSVersion.SSLv3 web.inputs = [user_to_web] threat = threats["AC10"] @@ -1046,7 +1046,7 @@ def test_CR08(self): web.minTLSVersion = TLSVersion.TLSv11 user_to_web = Dataflow(user, web, "User enters comments (*)") user_to_web.protocol = "HTTPS" - user_to_web.isEncrypted = True + user_to_web.controls.isEncrypted = True user_to_web.tlsVersion = TLSVersion.SSLv3 threat = threats["CR08"] self.assertTrue(threat.apply(user_to_web)) @@ -1066,7 +1066,7 @@ def test_INP20(self): def test_AC11(self): web = Server("Web Server") - web.usesStrongSessionIdentifiers = False + web.controls.usesStrongSessionIdentifiers = False threat = threats["AC11"] self.assertTrue(threat.apply(web)) @@ -1086,16 +1086,16 @@ def test_INP22(self): def test_INP23(self): process1 = Process("Process") - process1.hasAccessControl = False - process1.sanitizesInput = False - process1.validatesInput = False + process1.controls.hasAccessControl = False + process1.controls.sanitizesInput = False + process1.controls.validatesInput = False threat = threats["INP23"] self.assertTrue(threat.apply(process1)) def test_DO05(self): web = Server("Web Server") - web.validatesInput = False - web.sanitizesInput = False + web.controls.validatesInput = False + web.controls.sanitizesInput = False web.usesXMLParser = True threat = threats["DO05"] self.assertTrue(threat.apply(web)) @@ -1103,32 +1103,32 @@ def test_DO05(self): def test_AC12(self): process1 = Process("Process") process1.hasAccessControl = False - process1.implementsPOLP = False + process1.controls.implementsPOLP = False threat = threats["AC12"] self.assertTrue(threat.apply(process1)) def test_AC13(self): process1 = Process("Process") process1.hasAccessControl = False - process1.implementsPOLP = False + process1.controls.implementsPOLP = False threat = threats["AC13"] self.assertTrue(threat.apply(process1)) def test_AC14(self): process1 = Process("Process") - process1.implementsPOLP = False + process1.controls.implementsPOLP = False process1.usesEnvironmentVariables = False - process1.validatesInput = False + process1.controls.validatesInput = False threat = threats["AC14"] self.assertTrue(threat.apply(process1)) def test_INP24(self): process1 = Process("Process") lambda1 = Lambda("lambda") - process1.checksInputBounds = False - process1.validatesInput = False - lambda1.checksInputBounds = False - lambda1.validatesInput = False + process1.controls.checksInputBounds = False + process1.controls.validatesInput = False + lambda1.controls.checksInputBounds = False + lambda1.controls.validatesInput = False threat = threats["INP24"] self.assertTrue(threat.apply(process1)) self.assertTrue(threat.apply(lambda1)) @@ -1136,10 +1136,10 @@ def test_INP24(self): def test_INP25(self): process1 = Process("Process") lambda1 = Lambda("lambda") - process1.validatesInput = False - process1.sanitizesInput = False - lambda1.validatesInput = False - lambda1.sanitizesInput = False + process1.controls.validatesInput = False + process1.controls.sanitizesInput = False + lambda1.controls.validatesInput = False + lambda1.controls.sanitizesInput = False threat = threats["INP25"] self.assertTrue(threat.apply(process1)) self.assertTrue(threat.apply(lambda1)) @@ -1147,30 +1147,30 @@ def test_INP25(self): def test_INP26(self): process1 = Process("Process") lambda1 = Lambda("lambda") - process1.validatesInput = False - process1.sanitizesInput = False - lambda1.validatesInput = False - lambda1.sanitizesInput = False + process1.controls.validatesInput = False + process1.controls.sanitizesInput = False + lambda1.controls.validatesInput = False + lambda1.controls.sanitizesInput = False threat = threats["INP26"] self.assertTrue(threat.apply(process1)) self.assertTrue(threat.apply(lambda1)) def test_INP27(self): process1 = Process("Process") - process1.validatesInput = False - process1.sanitizesInput = False + process1.controls.validatesInput = False + process1.controls.sanitizesInput = False threat = threats["INP27"] self.assertTrue(threat.apply(process1)) def test_INP28(self): web = Server("Web Server") process1 = Process("Process") - web.validatesInput = False - web.sanitizesInput = False - web.encodesOutput = False - process1.validatesInput = False - process1.sanitizesInput = False - process1.encodesOutput = False + web.controls.validatesInput = False + web.controls.sanitizesInput = False + web.controls.encodesOutput = False + process1.controls.validatesInput = False + process1.controls.sanitizesInput = False + process1.controls.encodesOutput = False threat = threats["INP28"] self.assertTrue(threat.apply(process1)) self.assertTrue(threat.apply(web)) @@ -1178,135 +1178,135 @@ def test_INP28(self): def test_INP29(self): web = Server("Web Server") process1 = Process("Process") - web.validatesInput = False - web.sanitizesInput = False - web.encodesOutput = False - process1.validatesInput = False - process1.sanitizesInput = False - process1.encodesOutput = False + web.controls.validatesInput = False + web.controls.sanitizesInput = False + web.controls.encodesOutput = False + process1.controls.validatesInput = False + process1.controls.sanitizesInput = False + process1.controls.encodesOutput = False threat = threats["INP29"] self.assertTrue(threat.apply(process1)) self.assertTrue(threat.apply(web)) def test_INP30(self): process1 = Process("Process") - process1.validatesInput = False - process1.sanitizesInput = False + process1.controls.validatesInput = False + process1.controls.sanitizesInput = False threat = threats["INP30"] self.assertTrue(threat.apply(process1)) def test_INP31(self): process1 = Process("Process") - process1.validatesInput = False - process1.sanitizesInput = False - process1.usesParameterizedInput = False + process1.controls.validatesInput = False + process1.controls.sanitizesInput = False + process1.controls.usesParameterizedInput = False threat = threats["INP31"] self.assertTrue(threat.apply(process1)) def test_INP32(self): process1 = Process("Process") - process1.validatesInput = False - process1.sanitizesInput = False - process1.encodesOutput = False + process1.controls.validatesInput = False + process1.controls.sanitizesInput = False + process1.controls.encodesOutput = False threat = threats["INP32"] self.assertTrue(threat.apply(process1)) def test_INP33(self): process1 = Process("Process") - process1.validatesInput = False - process1.sanitizesInput = False + process1.controls.validatesInput = False + process1.controls.sanitizesInput = False threat = threats["INP33"] self.assertTrue(threat.apply(process1)) def test_INP34(self): web = Server("web") - web.checksInputBounds = False + web.controls.checksInputBounds = False threat = threats["INP34"] self.assertTrue(threat.apply(web)) def test_INP35(self): process1 = Process("Process") - process1.validatesInput = False - process1.sanitizesInput = False + process1.controls.validatesInput = False + process1.controls.sanitizesInput = False threat = threats["INP35"] self.assertTrue(threat.apply(process1)) def test_DE04(self): data = Datastore("DB") - data.validatesInput = False - data.implementsPOLP = False + data.controls.validatesInput = False + data.controls.implementsPOLP = False threat = threats["DE04"] self.assertTrue(threat.apply(data)) def test_AC15(self): process1 = Process("Process") - process1.implementsPOLP = False + process1.controls.implementsPOLP = False threat = threats["AC15"] self.assertTrue(threat.apply(process1)) def test_INP36(self): web = Server("web") web.implementsStrictHTTPValidation = False - web.encodesHeaders = False + web.controls.encodesHeaders = False threat = threats["INP36"] self.assertTrue(threat.apply(web)) def test_INP37(self): web = Server("web") web.implementsStrictHTTPValidation = False - web.encodesHeaders = False + web.controls.encodesHeaders = False threat = threats["INP37"] self.assertTrue(threat.apply(web)) def test_INP38(self): process1 = Process("Process") process1.allowsClientSideScripting = True - process1.validatesInput = False - process1.sanitizesInput = False + process1.controls.validatesInput = False + process1.controls.sanitizesInput = False threat = threats["INP38"] self.assertTrue(threat.apply(process1)) def test_AC16(self): web = Server("web") - web.usesStrongSessionIdentifiers = False - web.encryptsCookies = False + web.controls.usesStrongSessionIdentifiers = False + web.controls.encryptsCookies = False threat = threats["AC16"] self.assertTrue(threat.apply(web)) def test_INP39(self): process1 = Process("Process") process1.allowsClientSideScripting = True - process1.validatesInput = False - process1.sanitizesInput = False + process1.controls.validatesInput = False + process1.controls.sanitizesInput = False threat = threats["INP39"] self.assertTrue(threat.apply(process1)) def test_INP40(self): process1 = Process("Process") process1.allowsClientSideScripting = True - process1.sanitizesInput = False - process1.validatesInput = False + process1.controls.sanitizesInput = False + process1.controls.validatesInput = False threat = threats["INP40"] self.assertTrue(threat.apply(process1)) def test_AC17(self): web = Server("web") - web.usesStrongSessionIdentifiers = False + web.controls.usesStrongSessionIdentifiers = False threat = threats["AC17"] self.assertTrue(threat.apply(web)) def test_AC18(self): process1 = Process("Process") - process1.usesStrongSessionIdentifiers = False - process1.encryptsCookies = False - process1.definesConnectionTimeout = False + process1.controls.usesStrongSessionIdentifiers = False + process1.controls.encryptsCookies = False + process1.controls.definesConnectionTimeout = False threat = threats["AC18"] self.assertTrue(threat.apply(process1)) def test_INP41(self): process1 = Process("Process") - process1.validatesInput = False - process1.sanitizesInput = False + process1.controls.validatesInput = False + process1.controls.sanitizesInput = False threat = threats["INP41"] self.assertTrue(threat.apply(process1)) @@ -1319,9 +1319,9 @@ def test_AC19(self): def test_AC20(self): process1 = Process("Process") - process1.definesConnectionTimeout = False - process1.usesMFA = False - process1.encryptsSessionData = False + process1.controlsdefinesConnectionTimeout = False + process1.controls.usesMFA = False + process1.controls.encryptsSessionData = False threat = threats["AC20"] self.assertTrue(threat.apply(process1)) @@ -1340,7 +1340,7 @@ def test_AC22(self): "password", isCredentials=True, credentialsLife=Lifetime.HARDCODED ) user_to_web.protocol = "HTTPS" - user_to_web.isEncrypted = True + user_to_web.controls.isEncrypted = True threat = threats["AC22"] self.assertTrue(threat.apply(user_to_web)) @@ -1349,6 +1349,6 @@ def test_DR01(self): db = Datastore("Database") insert = Dataflow(web, db, "Insert query") insert.data = Data("ssn", isPII=True, isStored=True) - insert.isEncrypted = False + insert.controls.isEncrypted = False threat = threats["DR01"] self.assertTrue(threat.apply(insert)) diff --git a/tm.py b/tm.py index 8c598640..81d6d43a 100755 --- a/tm.py +++ b/tm.py @@ -28,15 +28,15 @@ web = Server("Web Server") web.OS = "Ubuntu" -web.isHardened = True -web.sanitizesInput = False -web.encodesOutput = True -web.authorizesSource = False +web.controls.isHardened = True +web.controls.sanitizesInput = False +web.controls.encodesOutput = True +web.controls.authorizesSource = False web.sourceFiles = ["pytm/json.py", "docs/template.md"] db = Datastore("SQL Database") db.OS = "CentOS" -db.isHardened = False +db.controls.isHardened = False db.inBoundary = server_db db.isSQL = True db.inScope = True @@ -46,7 +46,7 @@ secretDb = Datastore("Real Identity Database") secretDb.OS = "CentOS" secretDb.sourceFiles = ["pytm/pytm.py"] -secretDb.isHardened = True +secretDb.controls.isHardened = True secretDb.inBoundary = server_db secretDb.isSQL = True secretDb.inScope = True @@ -54,7 +54,7 @@ secretDb.maxClassification = Classification.TOP_SECRET my_lambda = Lambda("AWS Lambda") -my_lambda.hasAccessControl = True +my_lambda.controls.hasAccessControl = True my_lambda.inBoundary = vpc my_lambda.levels = [1, 2] @@ -117,6 +117,6 @@ processedBy=[db, secretDb], ) - if __name__ == "__main__": tm.process() + From 953546257e8dacfff4e770c8d6a3d563855e275e Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Wed, 29 Sep 2021 13:26:46 -0400 Subject: [PATCH 28/86] Added output encoding for each Elements findings data --- pytm/pytm.py | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/pytm/pytm.py b/pytm/pytm.py index 9791b7a4..d17c4a7e 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -960,15 +960,21 @@ def report(self, template_path): 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, } @@ -1839,6 +1845,25 @@ def serialize(obj, nested=False): result[i.lstrip("_")] = value return result +def encode_element_threat_data(obj): + """Used to html encode threat data from a list of Elements""" + encoded_elements = [] + if (type(obj) is not list): + raise ValueError("expecting a list value, got a {}".format(type(value))) + + for o in obj: + c = copy.deepcopy(o) + for a in o._attr_values(): + if (a is not "findings"): + v = getattr(o, a) + c._safeset(a, v) + else: + encoded_findings = encode_threat_data(o.findings) + c._safeset("findings", encoded_findings) + + encoded_elements.append(c) + + return encoded_elements def encode_threat_data(obj): """Used to html encode threat data from a list of threats or findings""" From d9253958ca20e92a79c6630fdb5cfba1eb0f6e09 Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Wed, 29 Sep 2021 19:03:08 -0400 Subject: [PATCH 29/86] cleanup, removed extra report content from testing. --- docs/advanced_template.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/advanced_template.md b/docs/advanced_template.md index 2fd184b5..5a8892df 100644 --- a/docs/advanced_template.md +++ b/docs/advanced_template.md @@ -17,8 +17,6 @@ Name|From|To |Data|Protocol|Port {dataflows:repeat:|{{item.display_name:call:}}|{{item.source.name}}|{{item.sink.name}}|{{item.data}}|{{item.protocol}}|{{item.dstPort}}| } -{dataflows:repeat:{{item:call:getElementType}} -} ## Data Dictionary Name|Description|Classification|Carried|Processed From 1b18464c60f6eaa0220122ee326f5c32a20706d8 Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Wed, 29 Sep 2021 19:43:59 -0400 Subject: [PATCH 30/86] Updated object copy logic to allow tests to pass --- pytm/pytm.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pytm/pytm.py b/pytm/pytm.py index d17c4a7e..9eba5b7d 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -1854,13 +1854,14 @@ def encode_element_threat_data(obj): for o in obj: c = copy.deepcopy(o) for a in o._attr_values(): - if (a is not "findings"): - v = getattr(o, a) - c._safeset(a, v) - else: + if (a == "findings"): encoded_findings = encode_threat_data(o.findings) c._safeset("findings", encoded_findings) - + else: + v = getattr(o, a) + if (type(v) is not list or (type(v) is list and len(v) != 0)): + c._safeset(a, v) + encoded_elements.append(c) return encoded_elements From c7ecac007e238f9fb857527fb996c58a0953c5d1 Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Thu, 30 Sep 2021 01:24:41 -0400 Subject: [PATCH 31/86] Add enum for DatastoreType used in Datastore objects, removed isSQL, updated sample tm, and updated existing tests --- pytm/__init__.py | 2 ++ pytm/pytm.py | 29 ++++++++++++++++++++++++++++- tests/output.json | 6 +++--- tests/test_private_func.py | 8 +++++--- tm.py | 5 +++-- 5 files changed, 41 insertions(+), 9 deletions(-) diff --git a/pytm/__init__.py b/pytm/__init__.py index a6fbb492..e13eb82a 100644 --- a/pytm/__init__.py +++ b/pytm/__init__.py @@ -7,6 +7,7 @@ "Data", "Dataflow", "Datastore", + "DatastoreType", "Element", "ExternalEntity", "Finding", @@ -33,6 +34,7 @@ Data, Dataflow, Datastore, + DatastoreType, Element, ExternalEntity, Finding, diff --git a/pytm/pytm.py b/pytm/pytm.py index 9791b7a4..0723a455 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -177,6 +177,13 @@ def __set__(self, instance, value): super().__set__(instance, value) +class varDatastoreType(var): + def __set__(self, instance, value): + if not isinstance(value, DatastoreType): + raise ValueError("expecting a DatastoreType, got a {}".format(type(value))) + super().__set__(instance, value) + + class varTLSVersion(var): def __set__(self, instance, value): if not isinstance(value, TLSVersion): @@ -295,6 +302,17 @@ def label(self): return self.value.lower().replace("_", " ") +class DatastoreType(Enum): + UNKNOWN = "UNKNOWN" + FILE_SYSTEM = "FILE_SYSTEM" + SQL = "SQL" + LDAP = "LDAP" + AWS_S3 = "AWS_S3" + + def label(self): + return self.value.lower().replace("_", " ") + + class TLSVersion(OrderedEnum): NONE = 0 SSLv1 = 1 @@ -1499,7 +1517,6 @@ class Datastore(Asset): is any information relating to an identifiable person.""", ) storesSensitiveData = varBool(False) - isSQL = varBool(True) providesConfidentiality = varBool(False) providesIntegrity = varBool(False) isShared = varBool(False) @@ -1518,6 +1535,16 @@ class Datastore(Asset): that are necessary for its legitimate purpose.""", ) isEncryptedAtRest = varBool(False, doc="Stored data is encrypted at rest") + type = varDatastoreType( + DatastoreType.UNKNOWN, + doc="""The type of Datastore, values may be one of: +* UNKNOWN - unknown applicable +* FILE_SYSTEM - files on a file system +* SQL - A SQL Database +* LDAP - An LDAP Server +* AWS_S3 - An S3 Bucket within AWS""" + ) + def __init__(self, name, **kwargs): super().__init__(name, **kwargs) diff --git a/tests/output.json b/tests/output.json index 0adee11b..f487932c 100644 --- a/tests/output.json +++ b/tests/output.json @@ -236,7 +236,6 @@ "isEncryptedAtRest": false, "isHardened": false, "isResilient": false, - "isSQL": true, "isShared": false, "levels": [ 0 @@ -259,6 +258,7 @@ "storesLogData": false, "storesPII": false, "storesSensitiveData": false, + "type": "DatastoreType.UNKNOWN", "usesEncryptionAlgorithm": "", "usesEnvironmentVariables": false, "validatesInput": false @@ -551,7 +551,6 @@ "isEncryptedAtRest": false, "isHardened": false, "isResilient": false, - "isSQL": true, "isShared": false, "levels": [ 0 @@ -574,6 +573,7 @@ "storesLogData": false, "storesPII": false, "storesSensitiveData": false, + "type": "DatastoreType.UNKNOWN", "usesEncryptionAlgorithm": "", "usesEnvironmentVariables": false, "validatesInput": false @@ -801,4 +801,4 @@ "onDuplicates": "Action.NO_ACTION", "threatsExcluded": [], "threatsFile": "pytm/threatlib/threats.json" -} +} \ No newline at end of file diff --git a/tests/test_private_func.py b/tests/test_private_func.py index 98f08a0c..7ad33c4a 100644 --- a/tests/test_private_func.py +++ b/tests/test_private_func.py @@ -8,6 +8,7 @@ Data, Dataflow, Datastore, + DatastoreType, Process, Server, Threat, @@ -88,12 +89,12 @@ def test_defaults(self): sql_resp = Data("SQL resp") db = Datastore( "PostgreSQL", - isSQL=True, port=5432, protocol="PostgreSQL", isEncrypted=False, data=sql_resp, ) + db.type = DatastoreType.SQL worker = Process("Task queue worker") req_get_data = Data("HTTP GET") @@ -187,7 +188,8 @@ def test_defaults(self): user = Actor("User", inBoundary=internet) server = Server("Server") - db = Datastore("DB", inBoundary=cloud, isSQL=True) + db = Datastore("DB", inBoundary=cloud) + db.type = DatastoreType.SQL func = Datastore("Lambda function", inBoundary=cloud) request = Dataflow(user, server, "request") @@ -214,7 +216,7 @@ def test_defaults(self): {"target": func, "condition": "not any(target.inputs)"}, { "target": server, - "condition": "any(f.sink.oneOf(Datastore) and f.sink.isSQL " + "condition": "any(f.sink.oneOf(Datastore) and f.sink.type == DatastoreType.SQL " "for f in target.outputs)", }, ] diff --git a/tm.py b/tm.py index 8c598640..756a4f8b 100755 --- a/tm.py +++ b/tm.py @@ -10,6 +10,7 @@ Datastore, Lambda, Server, + DatastoreType, ) tm = TM("my test tm") @@ -38,7 +39,7 @@ db.OS = "CentOS" db.isHardened = False db.inBoundary = server_db -db.isSQL = True +db.type = DatastoreType.SQL db.inScope = True db.maxClassification = Classification.RESTRICTED db.levels = [2] @@ -48,7 +49,7 @@ secretDb.sourceFiles = ["pytm/pytm.py"] secretDb.isHardened = True secretDb.inBoundary = server_db -secretDb.isSQL = True +secretDb.type = DatastoreType.SQL secretDb.inScope = True secretDb.storesPII = True secretDb.maxClassification = Classification.TOP_SECRET From 73eae7c5d24fa59b24bc43ff92fdfb77cdd9b7f7 Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Fri, 1 Oct 2021 11:06:10 -0400 Subject: [PATCH 32/86] Updated README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 940c82a0..160fdc16 100644 --- a/README.md +++ b/README.md @@ -296,7 +296,7 @@ For the security practitioner, you may supply your own threats file by setting ` "details": "This attack pattern involves causing a buffer overflow through manipulation of environment variables. Once the attacker finds that they can modify an environment variable, they may try to overflow associated buffers. This attack leverages implicit trust often placed in environment variables.", "Likelihood Of Attack": "High", "severity": "High", - "condition": "target.usesEnvironmentVariables is True and target.sanitizesInput is False and target.checksInputBounds is False", + "condition": "target.usesEnvironmentVariables is True and target.controls.sanitizesInput is False and target.controls.checksInputBounds is False", "prerequisites": "The application uses environment variables.An environment variable exposed to the user is vulnerable to a buffer overflow.The vulnerable environment variable uses untrusted data.Tainted data used in the environment variables is not properly validated. For instance boundary checking is not done before copying the input data to a buffer.", "mitigations": "Do not expose environment variable to the user.Do not use untrusted data in your environment variables. Use a language or compiler that performs automatic bounds checking. There are tools such as Sharefuzz [R.10.3] which is an environment variable fuzzer for Unix that support loading a shared library. You can use Sharefuzz to determine if you are exposing an environment variable vulnerable to buffer overflow.", "example": "Attack Example: Buffer Overflow in $HOME A buffer overflow in sccw allows local users to gain root access via the $HOME environmental variable. Attack Example: Buffer Overflow in TERM A buffer overflow in the rlogin program involves its consumption of the TERM environmental variable.", @@ -318,7 +318,7 @@ to list findings in the final [report](#report). The logic lives in the `condition`, where members of `target` can be logically evaluated. Returning a true means the rule generates a finding, otherwise, it is not a finding. -Condition may compare attributes of `target` and also call one of these methods: +Condition may compare attributes of `target` and/or control attributes of the 'target.control' and also call one of these methods: * `target.oneOf(class, ...)` where `class` is one or more: Actor, Datastore, Server, Process, SetOfProcesses, ExternalEntity, Lambda or Dataflow, * `target.crosses(Boundary)`, From 0859b978c96d30ff7e064e211adafc031fdaf3b3 Mon Sep 17 00:00:00 2001 From: "codesee-architecture-diagrams[bot]" <86324825+codesee-architecture-diagrams[bot]@users.noreply.github.com> Date: Fri, 1 Oct 2021 16:01:01 +0000 Subject: [PATCH 33/86] Add CodeSee architecture diagram workflow to repository --- .github/workflows/codesee-arch-diagram.yml | 81 ++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 .github/workflows/codesee-arch-diagram.yml diff --git a/.github/workflows/codesee-arch-diagram.yml b/.github/workflows/codesee-arch-diagram.yml new file mode 100644 index 00000000..5b4e5814 --- /dev/null +++ b/.github/workflows/codesee-arch-diagram.yml @@ -0,0 +1,81 @@ +on: + push: + branches: + - master + pull_request_target: + types: [opened, synchronize, reopened] + +name: CodeSee Map + +jobs: + test_map_action: + runs-on: ubuntu-latest + continue-on-error: true + name: Run CodeSee Map Analysis + steps: + - name: checkout + id: checkout + uses: actions/checkout@v2 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + fetch-depth: 0 + + # codesee-detect-languages has an output with id languages. + - name: Detect Languages + id: detect-languages + uses: Codesee-io/codesee-detect-languages-action@latest + + - name: Configure JDK 16 + uses: actions/setup-java@v2 + if: ${{ fromJSON(steps.detect-languages.outputs.languages).java }} + with: + java-version: '16' + distribution: 'zulu' + + # CodeSee Maps Go support uses a static binary so there's no setup step required. + + - name: Configure Node.js 14 + uses: actions/setup-node@v2 + if: ${{ fromJSON(steps.detect-languages.outputs.languages).javascript }} + with: + node-version: '14' + + - name: Configure Python 3.x + uses: actions/setup-python@v2 + if: ${{ fromJSON(steps.detect-languages.outputs.languages).python }} + with: + python-version: '3.x' + architecture: 'x64' + + - name: Configure Ruby '3.x' + uses: ruby/setup-ruby@v1 + if: ${{ fromJSON(steps.detect-languages.outputs.languages).ruby }} + with: + ruby-version: '3.0' + + # CodeSee Maps Rust support uses a static binary so there's no setup step required. + + - name: Generate Map + id: generate-map + uses: Codesee-io/codesee-map-action@latest + with: + step: map + github_ref: ${{ github.ref }} + languages: ${{ steps.detect-languages.outputs.languages }} + + - name: Upload Map + id: upload-map + uses: Codesee-io/codesee-map-action@latest + with: + step: mapUpload + api_token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} + github_ref: ${{ github.ref }} + + - name: Insights + id: insights + uses: Codesee-io/codesee-map-action@latest + with: + step: insights + api_token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} + github_ref: ${{ github.ref }} From 73c4365b55fa5f9e52ebd4ead2a3836acd753fe0 Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Sat, 2 Oct 2021 01:02:22 -0400 Subject: [PATCH 34/86] Added a list of global assumptions about the design/model. --- pytm/pytm.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pytm/pytm.py b/pytm/pytm.py index 9791b7a4..977e3a8a 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -720,6 +720,11 @@ class TM: doc="""How to handle duplicate Dataflow with same properties, except name and notes""", ) + assumptions = varStrings( + [], + required=False, + doc="A list of assumptions about the design/model.", + ) def __init__(self, name, **kwargs): for key, value in kwargs.items(): From 6bbe1be83ca31b0224fa71b3c6989910a4b8d4d9 Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Sat, 2 Oct 2021 01:56:48 -0400 Subject: [PATCH 35/86] Added assumption in sample tm, added visibility in sample report template and updated documentation --- docs/pytm/index.html | 36 +++++++++++++++++++++++++++++++++--- docs/template.md | 13 +++++++++++++ tm.py | 3 +++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/docs/pytm/index.html b/docs/pytm/index.html index 30cb0a55..25d09c87 100644 --- a/docs/pytm/index.html +++ b/docs/pytm/index.html @@ -3,7 +3,7 @@ - + pytm API documentation @@ -2218,7 +2218,7 @@

    Instance variables

    ) def __str__(self): - return f"{self.target}: {self.description}\n{self.details}\n{self.severity}"
    + return f"'{self.target}': {self.description}\n{self.details}\n{self.severity}"

    Instance variables

    @@ -3548,6 +3548,11 @@

    Class variables

    doc="""How to handle duplicate Dataflow with same properties, except name and notes""", ) + assumptions = varStrings( + [], + required=False, + doc="A list of assumptions about the design/model.", + ) def __init__(self, name, **kwargs): for key, value in kwargs.items(): @@ -3603,6 +3608,7 @@

    Class variables

    finding_count += 1 f = Finding(e, id=str(finding_count), threat=t) + logger.debug(f"new finding: {f}") findings.append(f) elements[e].append(f) self.findings = findings @@ -3839,6 +3845,9 @@

    Class variables

    if result.describe is not None: _describe_classes(result.describe.split()) + if result.list_elements: + _list_elements() + if result.list is True: [print("{} - {}".format(t.id, t.description)) for t in TM._threats] @@ -3961,6 +3970,22 @@

    Static methods

    Instance variables

    +
    var assumptions
    +
    +

    A list of assumptions about the design/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 description

    Model description

    @@ -4160,6 +4185,9 @@

    Methods

    if result.describe is not None: _describe_classes(result.describe.split()) + if result.list_elements: + _list_elements() + if result.list is True: [print("{} - {}".format(t.id, t.description)) for t in TM._threats] @@ -4231,6 +4259,7 @@

    Methods

    finding_count += 1 f = Finding(e, id=str(finding_count), threat=t) + logger.debug(f"new finding: {f}") findings.append(f) elements[e].append(f) self.findings = findings @@ -4768,6 +4797,7 @@

    TLSVersion

    TM

      +
    • assumptions
    • description
    • findings
    • get_table
    • @@ -4805,7 +4835,7 @@

      Threat

      \ No newline at end of file diff --git a/docs/template.md b/docs/template.md index 55a7a03f..76a1a09c 100644 --- a/docs/template.md +++ b/docs/template.md @@ -7,6 +7,19 @@   +{tm.assumptions:if: + +|Assumptions| +|-----------| +{tm.assumptions:repeat:|{{item}}| +} + +  +  +  +} + + ## Dataflow Diagram - Level 0 DFD ![](sample.png) diff --git a/tm.py b/tm.py index 8c598640..2a207a05 100755 --- a/tm.py +++ b/tm.py @@ -16,6 +16,9 @@ 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.assumptions = [ +"Here you can document a list of assumptions about the system", +] internet = Boundary("Internet") server_db = Boundary("Server/DB") From 62b7e26110c36e07b54aefaefb24647f4615b75a Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Sat, 2 Oct 2021 02:43:03 -0400 Subject: [PATCH 36/86] Updated test data. --- tests/output.json | 3 ++- tests/output.md | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/output.json b/tests/output.json index 0adee11b..4008d507 100644 --- a/tests/output.json +++ b/tests/output.json @@ -264,6 +264,7 @@ "validatesInput": false } ], + "assumptions": [], "boundaries": [ { "description": "", @@ -801,4 +802,4 @@ "onDuplicates": "Action.NO_ACTION", "threatsExcluded": [], "threatsFile": "pytm/threatlib/threats.json" -} +} \ No newline at end of file diff --git a/tests/output.md b/tests/output.md index d8382499..e0199328 100644 --- a/tests/output.md +++ b/tests/output.md @@ -7,6 +7,9 @@ aaa   + + + ## Dataflow Diagram - Level 0 DFD ![](sample.png) From ab109973557fb69db9bd727f0a66647af7e89aee Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Sat, 2 Oct 2021 02:49:20 -0400 Subject: [PATCH 37/86] Updated report test to write the generated report file to disk, similar to existing logic for json. --- tests/test_pytmfunc.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_pytmfunc.py b/tests/test_pytmfunc.py index f0e3e90a..07bfed09 100644 --- a/tests/test_pytmfunc.py +++ b/tests/test_pytmfunc.py @@ -373,6 +373,9 @@ def test_report(self): self.assertTrue(tm.check()) output = tm.report("docs/template.md") + with open(os.path.join(dir_path, "output_current.md"), "w") as x: + x.write(output) + self.maxDiff = None self.assertEqual(output.strip(), expected.strip()) From 54543450297785d955b867c042225a653725c3cb Mon Sep 17 00:00:00 2001 From: Nick Ozmore Date: Sat, 30 Oct 2021 00:19:21 -0400 Subject: [PATCH 38/86] Cleanup commented code. --- pytm/pytm.py | 135 +-------------------------------------------------- 1 file changed, 1 insertion(+), 134 deletions(-) diff --git a/pytm/pytm.py b/pytm/pytm.py index 897c8dc6..631538bb 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -1134,7 +1134,6 @@ def get_table(self, db, klass): class Controls: """Controls implemented by/on and Element""" - #allowsClientSideScripting = varBool(False) #NOZ - positive attestation of negative behavior?. not a fan of this one. authenticatesDestination = varBool( False, doc="""Verifies the identity of the destination, @@ -1159,7 +1158,6 @@ class Controls: handlesCrashes = varBool(False) handlesInterruptions = varBool(False) handlesResourceConsumption = varBool(False) - #handlesResources = varBool(False) #NOZ What is this? hasAccessControl = varBool(False) implementsAuthenticationScheme = varBool(False) implementsCSRFToken = varBool(False) @@ -1192,7 +1190,6 @@ class Controls: providesIntegrity = varBool(False) sanitizesInput = varBool(False) tracksExecutionFlow = varBool(False) - #usesCache = varBool(False) usesCodeSigning = varBool(False) usesEncryptionAlgorithm = varString("") usesMFA = varBool( @@ -1205,8 +1202,7 @@ class Controls: ) usesParameterizedInput = varBool(False) usesSecureFunctions = varBool(False) - #usesSessionTokens = varBool(False) #NOZ - Was this intended as a control? - usesStrongSessionIdentifiers = varBool(False) #NOZ - Was this intended as a control? + usesStrongSessionIdentifiers = varBool(False) usesVPN = varBool(False) validatesContentType = varBool(False) validatesHeaders = varBool(False) @@ -1479,47 +1475,14 @@ class Asset(Element): """An asset with outgoing or incoming dataflows""" port = varInt(-1, doc="Default TCP port for incoming data flows") -# isEncrypted = varBool(False, doc="Requires incoming data flow to be encrypted") protocol = varString("", doc="Default network protocol for incoming data flows") data = varData([], doc="pytm.Data object(s) in incoming data flows") inputs = varElements([], doc="incoming Dataflows") outputs = varElements([], doc="outgoing Dataflows") onAWS = varBool(False) -# isHardened = varBool(False) -# implementsAuthenticationScheme = varBool(False) -# implementsNonce = varBool( -# False, -# doc="""Nonce is an arbitrary number -#that can be used just once in a cryptographic communication. -#It is often a random or pseudo-random number issued in an authentication protocol -#to ensure that old communications cannot be reused in replay attacks. -#They can also be useful as initialization vectors and in cryptographic -#hash functions.""", -# ) handlesResources = varBool(False) -# definesConnectionTimeout = varBool(False) -# authenticatesDestination = varBool( -# False, -# doc="""Verifies the identity of the destination, -#for example by verifying the authenticity of a digital certificate.""", -# ) -# checksDestinationRevocation = varBool( -# False, -# doc="""Correctly checks the revocation status -#of credentials used to authenticate the destination""", -# ) -# authenticatesSource = varBool(False) -# authorizesSource = varBool(False) -# hasAccessControl = varBool(False) -# validatesInput = varBool(False) -# sanitizesInput = varBool(False) -# checksInputBounds = varBool(False) -# encodesOutput = varBool(False) -# handlesResourceConsumption = varBool(False) -# authenticationScheme = varString("") usesEnvironmentVariables = varBool(False) OS = varString("") -# providesIntegrity = varBool(False) def __init__(self, name, **kwargs): super().__init__(name, **kwargs) @@ -1571,33 +1534,10 @@ def _shape(self): class Server(Asset): """An entity processing data""" - #providesConfidentiality = varBool(False) - #providesIntegrity = varBool(False) - #validatesHeaders = varBool(False) - #encodesHeaders = varBool(False) - #implementsCSRFToken = varBool(False) - #isResilient = varBool(False) usesSessionTokens = varBool(False) - #usesEncryptionAlgorithm = varString("") usesCache = varBool(False) usesVPN = varBool(False) - #usesCodeSigning = varBool(False) - #validatesContentType = varBool(False) - #invokesScriptFilters = varBool(False) - #usesStrongSessionIdentifiers = varBool(False) - #implementsServerSideValidation = varBool(False) usesXMLParser = varBool(False) - #disablesDTD = varBool(False) - #implementsStrictHTTPValidation = varBool(False) - #implementsPOLP = varBool( - # False, - # doc="""The principle of least privilege (PoLP), -#also known as the principle of minimal privilege or the principle of least authority, -#requires that in a particular abstraction layer of a computing environment, -#every module (such as a process, a user, or a program, depending on the subject) -#must be able to access only the information and resources -#that are necessary for its legitimate purpose.""", -# ) def __init__(self, name, **kwargs): super().__init__(name, **kwargs) @@ -1625,24 +1565,8 @@ class Datastore(Asset): ) storesSensitiveData = varBool(False) isSQL = varBool(True) -# providesConfidentiality = varBool(False) -# providesIntegrity = varBool(False) isShared = varBool(False) hasWriteAccess = varBool(False) -# handlesResourceConsumption = varBool(False) -# isResilient = varBool(False) -# handlesInterruptions = varBool(False) -# usesEncryptionAlgorithm = varString("") -# implementsPOLP = varBool( -# False, -# doc="""The principle of least privilege (PoLP), -#also known as the principle of minimal privilege or the principle of least authority, -#requires that in a particular abstraction layer of a computing environment, -#every module (such as a process, a user, or a program, depending on the subject) -#must be able to access only the information and resources -#that are necessary for its legitimate purpose.""", -# ) -# isEncryptedAtRest = varBool(False, doc="Stored data is encrypted at rest") def __init__(self, name, **kwargs): super().__init__(name, **kwargs) @@ -1687,19 +1611,7 @@ class Actor(Element): data = varData([], doc="pytm.Data object(s) in outgoing data flows") inputs = varElements([], doc="incoming Dataflows") outputs = varElements([], doc="outgoing Dataflows") -# authenticatesDestination = varBool( -# False, -# doc="""Verifies the identity of the destination, -#for example by verifying the authenticity of a digital certificate.""", -# ) -# checksDestinationRevocation = varBool( -# False, -# doc="""Correctly checks the revocation status -#of credentials used to authenticate the destination""", -# ) isAdmin = varBool(False) - # should not be settable, but accessible -# providesIntegrity = False def __init__(self, name, **kwargs): super().__init__(name, **kwargs) @@ -1711,41 +1623,10 @@ class Process(Asset): codeType = varString("Unmanaged") implementsCommunicationProtocol = varBool(False) -# providesConfidentiality = varBool(False) -# providesIntegrity = varBool(False) -# isResilient = varBool(False) tracksExecutionFlow = varBool(False) -# implementsCSRFToken = varBool(False) -# handlesResourceConsumption = varBool(False) -# handlesCrashes = varBool(False) -# handlesInterruptions = varBool(False) implementsAPI = varBool(False) -# usesSecureFunctions = varBool(False) environment = varString("") -# disablesiFrames = varBool(False) -# implementsPOLP = varBool( -# False, -# doc="""The principle of least privilege (PoLP), -#also known as the principle of minimal privilege or the principle of least authority, -#requires that in a particular abstraction layer of a computing environment, -#every module (such as a process, a user, or a program, depending on the subject) -#must be able to access only the information and resources -#that are necessary for its legitimate purpose.""", -# ) -# usesParameterizedInput = varBool(False) allowsClientSideScripting = varBool(False) -# usesStrongSessionIdentifiers = varBool(False) -# encryptsCookies = varBool(False) -# usesMFA = varBool( -# False, -# doc="""Multi-factor authentication is an authentication method -#in which a computer user is granted access only after successfully presenting two -#or more pieces of evidence (or factors) to an authentication mechanism: knowledge -#(something the user and only the user knows), possession (something the user -#and only the user has), and inherence (something the user and only the user is).""", -# ) -# encryptsSessionData = varBool(False) -# verifySessionIdentifiers = varBool(False) def __init__(self, name, **kwargs): super().__init__(name, **kwargs) @@ -1772,7 +1653,6 @@ class Dataflow(Element): responseTo = varElement(None, doc="Is a response to this data flow") srcPort = varInt(-1, doc="Source TCP port") dstPort = varInt(-1, doc="Destination TCP port") -# isEncrypted = varBool(False, doc="Is the data encrypted") tlsVersion = varTLSVersion( TLSVersion.NONE, required=True, @@ -1780,23 +1660,10 @@ class Dataflow(Element): ) protocol = varString("", doc="Protocol used in this data flow") data = varData([], doc="pytm.Data object(s) in incoming data flows") -# authenticatesDestination = varBool( -# False, -# doc="""Verifies the identity of the destination, -#for example by verifying the authenticity of a digital certificate.""", -# ) -# checksDestinationRevocation = varBool( -# False, -# doc="""Correctly checks the revocation status -#of credentials used to authenticate the destination""", -# ) -# authenticatedWith = varBool(False) order = varInt(-1, doc="Number of this data flow in the threat model") -# implementsAuthenticationScheme = varBool(False) implementsCommunicationProtocol = varBool(False) note = varString("") usesVPN = varBool(False) -# authorizesSource = varBool(False) usesSessionTokens = varBool(False) def __init__(self, source, sink, name, **kwargs): From a789c2c237283946597ba73e5aaa5f657a41a247 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Sun, 14 Nov 2021 05:14:34 +0000 Subject: [PATCH 39/86] fix: Dockerfile to reduce vulnerabilities --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 54d51bd9..1c095ff5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10.0rc1-alpine3.13 +FROM python:3-alpine3.13 WORKDIR /usr/src/app ENTRYPOINT ["sh"] From 33c395760ecfc7441f82d2c422d56af674a667b4 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Mon, 15 Nov 2021 06:11:39 +0000 Subject: [PATCH 40/86] fix: Dockerfile to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-ALPINE313-BUSYBOX-1920750 - https://snyk.io/vuln/SNYK-ALPINE313-BUSYBOX-1920751 - https://snyk.io/vuln/SNYK-ALPINE313-BUSYBOX-1920752 - https://snyk.io/vuln/SNYK-ALPINE313-BUSYBOX-1920759 - https://snyk.io/vuln/SNYK-ALPINE313-BUSYBOX-1920760 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 54d51bd9..1cf37a4a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10.0rc1-alpine3.13 +FROM python:3.10-alpine3.13 WORKDIR /usr/src/app ENTRYPOINT ["sh"] From c458cae91c5d16007abec7b081d3172187797380 Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Mon, 7 Feb 2022 15:38:15 +0100 Subject: [PATCH 41/86] Adding includeOrder property to Element and adds order to name when true --- .dccache | 1 + pytm/pytm.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .dccache diff --git a/.dccache b/.dccache new file mode 100644 index 00000000..8bd35b77 --- /dev/null +++ b/.dccache @@ -0,0 +1 @@ +{"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/setup.py":[1172,1642158280939.523,"ed23abe5acf72c844572cae218fe2005ab2045a7de53b83df6f207d3ff89eb0b"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tm.py":[3960,1642158280947.5244,"5ef8255031b9ef8a2d2d6517e92cd82cce81189673ec6afaba24a8f29b0e48b7"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/json.py":[3232,1642158280931.5227,"128047d952b09de0e8e31efaa751054dbd2d5fcb6d79bb73b0b56594443e4fef"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/pytm.py":[62996,1643904520252.11,"5d461c8dc13dbb52a5f3ef5fb072563f599451796adda0442bfbaab999f03027"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/report_util.py":[1337,1642158280931.5227,"af164d1c43a9b2688cb87bf5687d1954febf356024e434fdc0a15776e0961bba"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/template_engine.py":[3586,1642158280931.5227,"fe30652114e4570a2a3540137217794f80dcbfc102c5d467e5342c0b953ba3dc"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/__init__.py":[1234,1642158280931.5227,"2bb0a1dade8cf6f6ff1c197f152cda8cf0370e25e159ef331133613b902402d2"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/test_private_func.py":[9475,1642158280947.5244,"a69468e11fb41bc7f56eb7d96e502e523773f94bee62ce457a16ace9bdb1f8bc"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/test_pytmfunc.py":[49082,1643904266318.3647,"6356c8a31767ecf651738dde9bba8e7c859ad048c0fc6803ca3e2b613b045390"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/__init__.py":[0,1642158280939.523,"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/docs/pytm/index.html":[173903,1642158280923.5234,"105996e3243916849444da7a3df813ae162779005ffe994a2f55d84e503b7cce"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\setup.py":[1172,1642158280939.523,"ed23abe5acf72c844572cae218fe2005ab2045a7de53b83df6f207d3ff89eb0b"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\tm.py":[3960,1642158280947.5244,"5ef8255031b9ef8a2d2d6517e92cd82cce81189673ec6afaba24a8f29b0e48b7"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\pytm\\json.py":[3232,1642158280931.5227,"128047d952b09de0e8e31efaa751054dbd2d5fcb6d79bb73b0b56594443e4fef"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\pytm\\pytm.py":[62996,1643904520252.11,"5d461c8dc13dbb52a5f3ef5fb072563f599451796adda0442bfbaab999f03027"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\pytm\\report_util.py":[1337,1642158280931.5227,"af164d1c43a9b2688cb87bf5687d1954febf356024e434fdc0a15776e0961bba"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\pytm\\template_engine.py":[3586,1642158280931.5227,"fe30652114e4570a2a3540137217794f80dcbfc102c5d467e5342c0b953ba3dc"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\tests\\test_private_func.py":[9475,1642158280947.5244,"a69468e11fb41bc7f56eb7d96e502e523773f94bee62ce457a16ace9bdb1f8bc"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\pytm\\__init__.py":[1234,1642158280931.5227,"2bb0a1dade8cf6f6ff1c197f152cda8cf0370e25e159ef331133613b902402d2"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\tests\\test_pytmfunc.py":[49082,1643904266318.3647,"6356c8a31767ecf651738dde9bba8e7c859ad048c0fc6803ca3e2b613b045390"]} \ No newline at end of file diff --git a/pytm/pytm.py b/pytm/pytm.py index b18a197c..5128d732 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -1295,11 +1295,15 @@ class Element: doc="Location of the source code that describes this element relative to the directory of the model script.", ) controls = varControls(None) + includeOrder = varBool( + False, doc="If True and Order is set, the displayed name will be formatted as 'order:name'. If you make Order unique, this will give you a stable reference you can use for synchronization etc.") + order = varInt(-1, doc="Number of this element in the threat model") def __init__(self, name, **kwargs): for key, value in kwargs.items(): setattr(self, key, value) - self.name = name + if self.includeOrder is True: + self.name = "{}:{}".format(self.order, name) self.controls = Controls() self.uuid = uuid.UUID(int=random.getrandbits(128)) self._is_drawn = False @@ -1890,8 +1894,8 @@ def encode_element_threat_data(obj): v = getattr(o, a) if (type(v) is not list or (type(v) is list and len(v) != 0)): c._safeset(a, v) - - encoded_elements.append(c) + + encoded_elements.append(c) return encoded_elements From d416824bc91232c8a768f57c7761182c38e6a629 Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Mon, 7 Feb 2022 16:15:14 +0100 Subject: [PATCH 42/86] Adding uniqueId to Finding --- .dccache | 2 +- pytm/pytm.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.dccache b/.dccache index 8bd35b77..c03cf4e2 100644 --- a/.dccache +++ b/.dccache @@ -1 +1 @@ -{"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/setup.py":[1172,1642158280939.523,"ed23abe5acf72c844572cae218fe2005ab2045a7de53b83df6f207d3ff89eb0b"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tm.py":[3960,1642158280947.5244,"5ef8255031b9ef8a2d2d6517e92cd82cce81189673ec6afaba24a8f29b0e48b7"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/json.py":[3232,1642158280931.5227,"128047d952b09de0e8e31efaa751054dbd2d5fcb6d79bb73b0b56594443e4fef"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/pytm.py":[62996,1643904520252.11,"5d461c8dc13dbb52a5f3ef5fb072563f599451796adda0442bfbaab999f03027"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/report_util.py":[1337,1642158280931.5227,"af164d1c43a9b2688cb87bf5687d1954febf356024e434fdc0a15776e0961bba"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/template_engine.py":[3586,1642158280931.5227,"fe30652114e4570a2a3540137217794f80dcbfc102c5d467e5342c0b953ba3dc"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/__init__.py":[1234,1642158280931.5227,"2bb0a1dade8cf6f6ff1c197f152cda8cf0370e25e159ef331133613b902402d2"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/test_private_func.py":[9475,1642158280947.5244,"a69468e11fb41bc7f56eb7d96e502e523773f94bee62ce457a16ace9bdb1f8bc"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/test_pytmfunc.py":[49082,1643904266318.3647,"6356c8a31767ecf651738dde9bba8e7c859ad048c0fc6803ca3e2b613b045390"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/__init__.py":[0,1642158280939.523,"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/docs/pytm/index.html":[173903,1642158280923.5234,"105996e3243916849444da7a3df813ae162779005ffe994a2f55d84e503b7cce"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\setup.py":[1172,1642158280939.523,"ed23abe5acf72c844572cae218fe2005ab2045a7de53b83df6f207d3ff89eb0b"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\tm.py":[3960,1642158280947.5244,"5ef8255031b9ef8a2d2d6517e92cd82cce81189673ec6afaba24a8f29b0e48b7"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\pytm\\json.py":[3232,1642158280931.5227,"128047d952b09de0e8e31efaa751054dbd2d5fcb6d79bb73b0b56594443e4fef"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\pytm\\pytm.py":[62996,1643904520252.11,"5d461c8dc13dbb52a5f3ef5fb072563f599451796adda0442bfbaab999f03027"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\pytm\\report_util.py":[1337,1642158280931.5227,"af164d1c43a9b2688cb87bf5687d1954febf356024e434fdc0a15776e0961bba"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\pytm\\template_engine.py":[3586,1642158280931.5227,"fe30652114e4570a2a3540137217794f80dcbfc102c5d467e5342c0b953ba3dc"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\tests\\test_private_func.py":[9475,1642158280947.5244,"a69468e11fb41bc7f56eb7d96e502e523773f94bee62ce457a16ace9bdb1f8bc"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\pytm\\__init__.py":[1234,1642158280931.5227,"2bb0a1dade8cf6f6ff1c197f152cda8cf0370e25e159ef331133613b902402d2"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\tests\\test_pytmfunc.py":[49082,1643904266318.3647,"6356c8a31767ecf651738dde9bba8e7c859ad048c0fc6803ca3e2b613b045390"]} \ No newline at end of file +{"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/setup.py":[1172,1642158280939.523,"ed23abe5acf72c844572cae218fe2005ab2045a7de53b83df6f207d3ff89eb0b"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tm.py":[3960,1642158280947.5244,"5ef8255031b9ef8a2d2d6517e92cd82cce81189673ec6afaba24a8f29b0e48b7"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/json.py":[3232,1642158280931.5227,"128047d952b09de0e8e31efaa751054dbd2d5fcb6d79bb73b0b56594443e4fef"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/pytm.py":[63261,1644245760750.3005,"7e34d5318a9949db43bdeaddcbf796f1704cc9a883c4058dd6d841348a745dfc"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/report_util.py":[1337,1642158280931.5227,"af164d1c43a9b2688cb87bf5687d1954febf356024e434fdc0a15776e0961bba"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/template_engine.py":[3586,1642158280931.5227,"fe30652114e4570a2a3540137217794f80dcbfc102c5d467e5342c0b953ba3dc"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/__init__.py":[1234,1642158280931.5227,"2bb0a1dade8cf6f6ff1c197f152cda8cf0370e25e159ef331133613b902402d2"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/test_private_func.py":[9475,1642158280947.5244,"a69468e11fb41bc7f56eb7d96e502e523773f94bee62ce457a16ace9bdb1f8bc"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/test_pytmfunc.py":[49082,1643904266318.3647,"6356c8a31767ecf651738dde9bba8e7c859ad048c0fc6803ca3e2b613b045390"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/__init__.py":[0,1642158280939.523,"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/docs/pytm/index.html":[173903,1642158280923.5234,"105996e3243916849444da7a3df813ae162779005ffe994a2f55d84e503b7cce"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\pytm\\pytm.py":[63261,1644245760750.3005,"7e34d5318a9949db43bdeaddcbf796f1704cc9a883c4058dd6d841348a745dfc"]} \ No newline at end of file diff --git a/pytm/pytm.py b/pytm/pytm.py index 5128d732..7b134238 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -652,6 +652,8 @@ class Finding: """, ) cvss = varString("", required=False, doc="The CVSS score and/or vector") + uniqueId = varString( + "", doc="When order is present and includeOrder is true on the object, this will be formatted as findingId:order. E.g. if finding is INP01 and order is 123, the value becomes INP01:123.") def __init__( self, @@ -809,7 +811,12 @@ def resolve(self): continue finding_count += 1 - f = Finding(e, id=str(finding_count), threat=t) + if e.includeOrder is True and e.order != -1: + uniqueId="{}:{}".format(t.id,e.order) + else: + uniqueId=str(finding_count) + + f = Finding(e, id=str(finding_count), threat=t, uniqueId=uniqueId) logger.debug(f"new finding: {f}") findings.append(f) elements[e].append(f) From 1531a89ee12bc70580c6d823c020a18e16b32996 Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Mon, 7 Feb 2022 16:32:18 +0100 Subject: [PATCH 43/86] expose uniqueId to html rendering --- .dccache | 2 +- pytm/pytm.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.dccache b/.dccache index c03cf4e2..8761d5a2 100644 --- a/.dccache +++ b/.dccache @@ -1 +1 @@ -{"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/setup.py":[1172,1642158280939.523,"ed23abe5acf72c844572cae218fe2005ab2045a7de53b83df6f207d3ff89eb0b"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tm.py":[3960,1642158280947.5244,"5ef8255031b9ef8a2d2d6517e92cd82cce81189673ec6afaba24a8f29b0e48b7"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/json.py":[3232,1642158280931.5227,"128047d952b09de0e8e31efaa751054dbd2d5fcb6d79bb73b0b56594443e4fef"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/pytm.py":[63261,1644245760750.3005,"7e34d5318a9949db43bdeaddcbf796f1704cc9a883c4058dd6d841348a745dfc"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/report_util.py":[1337,1642158280931.5227,"af164d1c43a9b2688cb87bf5687d1954febf356024e434fdc0a15776e0961bba"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/template_engine.py":[3586,1642158280931.5227,"fe30652114e4570a2a3540137217794f80dcbfc102c5d467e5342c0b953ba3dc"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/__init__.py":[1234,1642158280931.5227,"2bb0a1dade8cf6f6ff1c197f152cda8cf0370e25e159ef331133613b902402d2"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/test_private_func.py":[9475,1642158280947.5244,"a69468e11fb41bc7f56eb7d96e502e523773f94bee62ce457a16ace9bdb1f8bc"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/test_pytmfunc.py":[49082,1643904266318.3647,"6356c8a31767ecf651738dde9bba8e7c859ad048c0fc6803ca3e2b613b045390"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/__init__.py":[0,1642158280939.523,"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/docs/pytm/index.html":[173903,1642158280923.5234,"105996e3243916849444da7a3df813ae162779005ffe994a2f55d84e503b7cce"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\pytm\\pytm.py":[63261,1644245760750.3005,"7e34d5318a9949db43bdeaddcbf796f1704cc9a883c4058dd6d841348a745dfc"]} \ No newline at end of file +{"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/setup.py":[1172,1642158280939.523,"ed23abe5acf72c844572cae218fe2005ab2045a7de53b83df6f207d3ff89eb0b"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tm.py":[3960,1642158280947.5244,"5ef8255031b9ef8a2d2d6517e92cd82cce81189673ec6afaba24a8f29b0e48b7"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/json.py":[3232,1642158280931.5227,"128047d952b09de0e8e31efaa751054dbd2d5fcb6d79bb73b0b56594443e4fef"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/pytm.py":[63427,1644247688953.1438,"20a25c63f7866ea2a9ba43ba86968cc66d7e11a63e3cb421c73ba7b7847557c4"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/report_util.py":[1337,1642158280931.5227,"af164d1c43a9b2688cb87bf5687d1954febf356024e434fdc0a15776e0961bba"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/template_engine.py":[3586,1642158280931.5227,"fe30652114e4570a2a3540137217794f80dcbfc102c5d467e5342c0b953ba3dc"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/__init__.py":[1234,1642158280931.5227,"2bb0a1dade8cf6f6ff1c197f152cda8cf0370e25e159ef331133613b902402d2"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/test_private_func.py":[9475,1642158280947.5244,"a69468e11fb41bc7f56eb7d96e502e523773f94bee62ce457a16ace9bdb1f8bc"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/test_pytmfunc.py":[49082,1644247688954.1438,"6356c8a31767ecf651738dde9bba8e7c859ad048c0fc6803ca3e2b613b045390"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/__init__.py":[0,1642158280939.523,"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/docs/pytm/index.html":[173903,1642158280923.5234,"105996e3243916849444da7a3df813ae162779005ffe994a2f55d84e503b7cce"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\pytm\\pytm.py":[63427,1644247688953.1438,"20a25c63f7866ea2a9ba43ba86968cc66d7e11a63e3cb421c73ba7b7847557c4"]} \ No newline at end of file diff --git a/pytm/pytm.py b/pytm/pytm.py index 7b134238..a84b5bcd 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -1920,6 +1920,7 @@ def encode_threat_data(obj): "threat_id", "references", "condition", + "uniqueId" ] if type(obj) is Finding or (len(obj) != 0 and type(obj[0]) is Finding): From bf51d1ff5fc5cf333cfd4e7d3c82035a1a054933 Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Mon, 7 Feb 2022 19:39:52 +0100 Subject: [PATCH 44/86] Let dataflow arrows use includeOrder syntax --- pytm/pytm.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pytm/pytm.py b/pytm/pytm.py index a84b5bcd..c1736183 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -1728,7 +1728,10 @@ def __init__(self, source, sink, name, **kwargs): def display_name(self): if self.order == -1: return self.name - return "({}) {}".format(self.order, self.name) + elif self.includeOrder is True: # order is already included in name + return self.name + else: + return "({}) {}".format(self.order, self.name) def _dfd_template(self): return """{source} -> {sink} [ From 429b82f7aececafa52e68569fde4493b34219d95 Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Sun, 13 Mar 2022 17:26:58 +0100 Subject: [PATCH 45/86] Ignore .dccache and not-in-git --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index f67268c8..4292367a 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,9 @@ tm/ /tests/1.txt /tests/0.txt /tests/.config.pytm + +# local files +not-in-git/ + +# Snyk cache +.dccache From f6c1cd124cdf7cdb97ea48674736772e57ed5575 Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Sun, 13 Mar 2022 17:27:59 +0100 Subject: [PATCH 46/86] Make datastore a rounded rectangle --- pytm/pytm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytm/pytm.py b/pytm/pytm.py index c1736183..4a39b0ea 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -1638,7 +1638,7 @@ def _dfd_template(self): """ def _shape(self): - return "none" + return "rectangle; style=rounded" def dfd(self, **kwargs): self._is_drawn = True From 12980700d63cd722f2fbc029c4f20236382c7331 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Wed, 16 Mar 2022 14:18:20 +0000 Subject: [PATCH 47/86] fix: Dockerfile to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-ALPINE313-EXPAT-2406625 - https://snyk.io/vuln/SNYK-ALPINE313-EXPAT-2406626 - https://snyk.io/vuln/SNYK-ALPINE313-EXPAT-2407739 - https://snyk.io/vuln/SNYK-ALPINE313-EXPAT-2407752 - https://snyk.io/vuln/SNYK-ALPINE313-EXPAT-2407757 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 54fe2dd9..0130d4d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM python:3-alpine3.13 +FROM python:3.11.0a5-slim-bullseye WORKDIR /usr/src/app From 790bfbe7ddec4159fdfa5fed7bb41112f71ee51b Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Wed, 23 Mar 2022 14:25:16 +0100 Subject: [PATCH 48/86] Remove not-in-git again --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 4292367a..997e21cb 100644 --- a/.gitignore +++ b/.gitignore @@ -131,8 +131,5 @@ tm/ /tests/0.txt /tests/.config.pytm -# local files -not-in-git/ - # Snyk cache .dccache From 42b64c0e5fa73cbd9b8b95fe3c44af77d3fb9fe1 Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Mon, 28 Mar 2022 14:53:14 +0200 Subject: [PATCH 49/86] Delete .dccache --- .dccache | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .dccache diff --git a/.dccache b/.dccache deleted file mode 100644 index 8761d5a2..00000000 --- a/.dccache +++ /dev/null @@ -1 +0,0 @@ -{"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/setup.py":[1172,1642158280939.523,"ed23abe5acf72c844572cae218fe2005ab2045a7de53b83df6f207d3ff89eb0b"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tm.py":[3960,1642158280947.5244,"5ef8255031b9ef8a2d2d6517e92cd82cce81189673ec6afaba24a8f29b0e48b7"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/json.py":[3232,1642158280931.5227,"128047d952b09de0e8e31efaa751054dbd2d5fcb6d79bb73b0b56594443e4fef"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/pytm.py":[63427,1644247688953.1438,"20a25c63f7866ea2a9ba43ba86968cc66d7e11a63e3cb421c73ba7b7847557c4"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/report_util.py":[1337,1642158280931.5227,"af164d1c43a9b2688cb87bf5687d1954febf356024e434fdc0a15776e0961bba"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/template_engine.py":[3586,1642158280931.5227,"fe30652114e4570a2a3540137217794f80dcbfc102c5d467e5342c0b953ba3dc"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/pytm/__init__.py":[1234,1642158280931.5227,"2bb0a1dade8cf6f6ff1c197f152cda8cf0370e25e159ef331133613b902402d2"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/test_private_func.py":[9475,1642158280947.5244,"a69468e11fb41bc7f56eb7d96e502e523773f94bee62ce457a16ace9bdb1f8bc"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/test_pytmfunc.py":[49082,1644247688954.1438,"6356c8a31767ecf651738dde9bba8e7c859ad048c0fc6803ca3e2b613b045390"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/tests/__init__.py":[0,1642158280939.523,"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],"c:/Users/dkperoes/dev/gitwork/per-oestergaard/pytm/docs/pytm/index.html":[173903,1642158280923.5234,"105996e3243916849444da7a3df813ae162779005ffe994a2f55d84e503b7cce"],"c:\\Users\\dkperoes\\dev\\gitwork\\per-oestergaard\\pytm\\pytm\\pytm.py":[63427,1644247688953.1438,"20a25c63f7866ea2a9ba43ba86968cc66d7e11a63e3cb421c73ba7b7847557c4"]} \ No newline at end of file From 5da9f1017bb102cb7e782d2e353d3db6cf5d0c55 Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Mon, 28 Mar 2022 14:54:31 +0200 Subject: [PATCH 50/86] Reset shape to none --- pytm/pytm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytm/pytm.py b/pytm/pytm.py index 4a39b0ea..c1736183 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -1638,7 +1638,7 @@ def _dfd_template(self): """ def _shape(self): - return "rectangle; style=rounded" + return "none" def dfd(self, **kwargs): self._is_drawn = True From d2be2601cb19d5e6fba07d274986e0d9d16e997f Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Mon, 28 Mar 2022 15:11:51 +0200 Subject: [PATCH 51/86] Add not -1 to help text --- pytm/pytm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytm/pytm.py b/pytm/pytm.py index c1736183..9d0e5875 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -1303,7 +1303,7 @@ class Element: ) controls = varControls(None) includeOrder = varBool( - False, doc="If True and Order is set, the displayed name will be formatted as 'order:name'. If you make Order unique, this will give you a stable reference you can use for synchronization etc.") + False, doc="If True and Order is set (not -1), the displayed name will be formatted as 'order:name'. If you make Order unique, this will give you a stable reference you can use for synchronization etc.") order = varInt(-1, doc="Number of this element in the threat model") def __init__(self, name, **kwargs): From 400255a5e6fb5b58203e05f4387889528fcc40d0 Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Mon, 28 Mar 2022 16:38:35 +0200 Subject: [PATCH 52/86] Adding test cases --- tests/test_pytmfunc.py | 35 +++++++++++++++++++++++++++++++++++ x | 3 +++ 2 files changed, 38 insertions(+) create mode 100644 x diff --git a/tests/test_pytmfunc.py b/tests/test_pytmfunc.py index 029aefbd..5ee10fde 100644 --- a/tests/test_pytmfunc.py +++ b/tests/test_pytmfunc.py @@ -186,6 +186,41 @@ def test_dfd_duplicates_raise(self): with self.assertRaisesRegex(ValueError, e): tm.check() + def test_order_in_finding(self): + random.seed(0) + + TM.reset() + order=1234 + threat_name = "INP03" + formatted_name="{0}:{1}".format(threat_name,order) + + tm = TM("my test tm", description="aaa") + web = Server("Web") + web.includeOrder= True + web.order=order + + tm.resolve() + + self.assertIn(formatted_name, [t.uniqueId for t in tm.findings]) + + def test_order_not_in_finding_no_includeorder(self): + random.seed(0) + + TM.reset() + order=1234 + threat_name = "INP03" + formatted_name="{0}:{1}".format(threat_name,order) + + tm = TM("my test tm", description="aaa") + web = Server("Web") + web.includeOrder= False + web.order=order + + tm.resolve() + + self.assertIn(("1","INP03"), [(t.uniqueId,t.threat_id) for t in tm.findings]) + self.assertNotIn((formatted_name,"INP03"), [(t.uniqueId,t.threat_id) for t in tm.findings]) + def test_exclude_threats_ignore(self): random.seed(0) diff --git a/x b/x new file mode 100644 index 00000000..04b6bfa1 --- /dev/null +++ b/x @@ -0,0 +1,3 @@ +cd /pwd +pip install --force-reinstall pydal > /dev/null +python -m unittest -f -k order \ No newline at end of file From 13598a203052bff531a9f7299cc682ea1125bb58 Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Mon, 28 Mar 2022 16:39:17 +0200 Subject: [PATCH 53/86] Remove test file x --- x | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 x diff --git a/x b/x deleted file mode 100644 index 04b6bfa1..00000000 --- a/x +++ /dev/null @@ -1,3 +0,0 @@ -cd /pwd -pip install --force-reinstall pydal > /dev/null -python -m unittest -f -k order \ No newline at end of file From ecf74f77ac20b53488fb71f3d5e94fe063e5a5b5 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Wed, 30 Mar 2022 14:39:28 +0000 Subject: [PATCH 54/86] fix: Dockerfile to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-DEBIAN11-GLIBC-2340908 - https://snyk.io/vuln/SNYK-DEBIAN11-GLIBC-2340922 - https://snyk.io/vuln/SNYK-DEBIAN11-OPENSSL-2388380 - https://snyk.io/vuln/SNYK-DEBIAN11-OPENSSL-2426309 - https://snyk.io/vuln/SNYK-DEBIAN11-OPENSSL-2426309 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0130d4d7..7622a28c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM python:3.11.0a5-slim-bullseye +FROM python:3.11-rc-slim WORKDIR /usr/src/app From d64a736b9b0a069f78f590b4c5171df71c09be3c Mon Sep 17 00:00:00 2001 From: Izar Tarandach Date: Thu, 28 Apr 2022 09:38:42 -0400 Subject: [PATCH 55/86] Revert "Adding uniqueId and includeOrder" --- .gitignore | 3 --- pytm/pytm.py | 25 +++++-------------------- tests/test_pytmfunc.py | 35 ----------------------------------- 3 files changed, 5 insertions(+), 58 deletions(-) diff --git a/.gitignore b/.gitignore index 997e21cb..f67268c8 100644 --- a/.gitignore +++ b/.gitignore @@ -130,6 +130,3 @@ tm/ /tests/1.txt /tests/0.txt /tests/.config.pytm - -# Snyk cache -.dccache diff --git a/pytm/pytm.py b/pytm/pytm.py index 9d0e5875..b18a197c 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -652,8 +652,6 @@ class Finding: """, ) cvss = varString("", required=False, doc="The CVSS score and/or vector") - uniqueId = varString( - "", doc="When order is present and includeOrder is true on the object, this will be formatted as findingId:order. E.g. if finding is INP01 and order is 123, the value becomes INP01:123.") def __init__( self, @@ -811,12 +809,7 @@ def resolve(self): continue finding_count += 1 - if e.includeOrder is True and e.order != -1: - uniqueId="{}:{}".format(t.id,e.order) - else: - uniqueId=str(finding_count) - - f = Finding(e, id=str(finding_count), threat=t, uniqueId=uniqueId) + f = Finding(e, id=str(finding_count), threat=t) logger.debug(f"new finding: {f}") findings.append(f) elements[e].append(f) @@ -1302,15 +1295,11 @@ class Element: doc="Location of the source code that describes this element relative to the directory of the model script.", ) controls = varControls(None) - includeOrder = varBool( - False, doc="If True and Order is set (not -1), the displayed name will be formatted as 'order:name'. If you make Order unique, this will give you a stable reference you can use for synchronization etc.") - order = varInt(-1, doc="Number of this element in the threat model") def __init__(self, name, **kwargs): for key, value in kwargs.items(): setattr(self, key, value) - if self.includeOrder is True: - self.name = "{}:{}".format(self.order, name) + self.name = name self.controls = Controls() self.uuid = uuid.UUID(int=random.getrandbits(128)) self._is_drawn = False @@ -1728,10 +1717,7 @@ def __init__(self, source, sink, name, **kwargs): def display_name(self): if self.order == -1: return self.name - elif self.includeOrder is True: # order is already included in name - return self.name - else: - return "({}) {}".format(self.order, self.name) + return "({}) {}".format(self.order, self.name) def _dfd_template(self): return """{source} -> {sink} [ @@ -1904,8 +1890,8 @@ def encode_element_threat_data(obj): v = getattr(o, a) if (type(v) is not list or (type(v) is list and len(v) != 0)): c._safeset(a, v) - - encoded_elements.append(c) + + encoded_elements.append(c) return encoded_elements @@ -1923,7 +1909,6 @@ def encode_threat_data(obj): "threat_id", "references", "condition", - "uniqueId" ] if type(obj) is Finding or (len(obj) != 0 and type(obj[0]) is Finding): diff --git a/tests/test_pytmfunc.py b/tests/test_pytmfunc.py index 5ee10fde..029aefbd 100644 --- a/tests/test_pytmfunc.py +++ b/tests/test_pytmfunc.py @@ -186,41 +186,6 @@ def test_dfd_duplicates_raise(self): with self.assertRaisesRegex(ValueError, e): tm.check() - def test_order_in_finding(self): - random.seed(0) - - TM.reset() - order=1234 - threat_name = "INP03" - formatted_name="{0}:{1}".format(threat_name,order) - - tm = TM("my test tm", description="aaa") - web = Server("Web") - web.includeOrder= True - web.order=order - - tm.resolve() - - self.assertIn(formatted_name, [t.uniqueId for t in tm.findings]) - - def test_order_not_in_finding_no_includeorder(self): - random.seed(0) - - TM.reset() - order=1234 - threat_name = "INP03" - formatted_name="{0}:{1}".format(threat_name,order) - - tm = TM("my test tm", description="aaa") - web = Server("Web") - web.includeOrder= False - web.order=order - - tm.resolve() - - self.assertIn(("1","INP03"), [(t.uniqueId,t.threat_id) for t in tm.findings]) - self.assertNotIn((formatted_name,"INP03"), [(t.uniqueId,t.threat_id) for t in tm.findings]) - def test_exclude_threats_ignore(self): random.seed(0) From 809ecefe822b51cacc3363a98748fedeead4024b Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Mon, 2 May 2022 11:15:24 +0200 Subject: [PATCH 56/86] Create output files from tests in tempdir --- tests/test_pytmfunc.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/test_pytmfunc.py b/tests/test_pytmfunc.py index 029aefbd..f0f00981 100644 --- a/tests/test_pytmfunc.py +++ b/tests/test_pytmfunc.py @@ -3,6 +3,7 @@ import random import re import unittest +import tempfile from contextlib import redirect_stdout from pytm import ( @@ -34,6 +35,7 @@ ) as threat_file: threats = {t["SID"]: Threat(**t) for t in json.load(threat_file)} +output_path=tempfile.gettempdir() class TestTM(unittest.TestCase): def test_seq(self): @@ -325,7 +327,7 @@ def test_json_dumps(self): self.assertTrue(tm.check()) output = json.dumps(tm, default=to_serializable, sort_keys=True, indent=4) - with open(os.path.join(dir_path, "output_current.json"), "w") as x: + with open(os.path.join(output_path, "output_current.json"), "w") as x: x.write(output) self.maxDiff = None @@ -395,10 +397,10 @@ def test_report(self): self.assertTrue(tm.check()) output = tm.report("docs/basic_template.md") - with open(os.path.join(dir_path, "output_current.md"), "w") as x: + with open(os.path.join(output_path, "output_current.md"), "w") as x: x.write(output) - with open(os.path.join(dir_path, "output_current.md"), "w") as x: + with open(os.path.join(output_path, "output_current.md"), "w") as x: x.write(output) self.maxDiff = None @@ -433,7 +435,7 @@ def test_multilevel_dfd(self): self.assertTrue(tm.check()) output = tm.dfd(levels={0}) - with open(os.path.join(dir_path, "0.txt"), "w") as x: + with open(os.path.join(output_path, "0.txt"), "w") as x: x.write(output) self.assertEqual(output, level_0) @@ -452,7 +454,7 @@ def test_multilevel_dfd(self): self.assertTrue(tm.check()) output = tm.dfd(levels={1}) - with open(os.path.join(dir_path, "1.txt"), "w") as x: + with open(os.path.join(output_path, "1.txt"), "w") as x: x.write(output) self.maxDiff = None self.assertEqual(output, level_1) From 858fde601a45ea8eeb30486954d432050caf716a Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Mon, 2 May 2022 11:16:10 +0200 Subject: [PATCH 57/86] As test output files are in tempdir, we do not have to ignore them --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index f67268c8..dfbba4c8 100644 --- a/.gitignore +++ b/.gitignore @@ -125,8 +125,4 @@ tm_example.dot plantuml.jar tm/ /sqldump -/tests/output_current.json -/tests/output_current.md -/tests/1.txt -/tests/0.txt /tests/.config.pytm From fa8c0bbf6cc42b7dc76aefdc378c2d441cdf36b2 Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Mon, 2 May 2022 11:39:23 +0200 Subject: [PATCH 58/86] Adding scripts to make testing easier --- tests/run-unittests.ps1 | 10 ++++++++++ tests/run-unittests.sh | 6 ++++++ x.sh | 7 +++++++ 3 files changed, 23 insertions(+) create mode 100644 tests/run-unittests.ps1 create mode 100644 tests/run-unittests.sh create mode 100644 x.sh diff --git a/tests/run-unittests.ps1 b/tests/run-unittests.ps1 new file mode 100644 index 00000000..8e6f8c2c --- /dev/null +++ b/tests/run-unittests.ps1 @@ -0,0 +1,10 @@ +[CmdletBinding()] +param ( + [ValidateSet('always', 'never')] + [string] $pull = 'always' +) + +# Run all tests using docker and a read-only file system so the docker image cannot impact the local files. + +$rootFolder = Split-Path $PSScriptRoot +docker run --pull $pull --rm -v "${rootFolder}:/pwd:ro" python bash /pwd/tests/run-unittests.sh \ No newline at end of file diff --git a/tests/run-unittests.sh b/tests/run-unittests.sh new file mode 100644 index 00000000..c98f02fe --- /dev/null +++ b/tests/run-unittests.sh @@ -0,0 +1,6 @@ +# Script to prepare the environment and run the test. Is invoked by run-unittests.ps1 + +cd /pwd && \ +pip install -r requirements-dev.txt && \ +pip install -r requirements.txt && \ +python3 -m unittest -v tests/test_pytmfunc.py \ No newline at end of file diff --git a/x.sh b/x.sh new file mode 100644 index 00000000..504b5c00 --- /dev/null +++ b/x.sh @@ -0,0 +1,7 @@ +cd /pwd +ls -l +pip install -r requirements-dev.txt +pip install -r requirements.txt +pwd +python3 -m unittest -v tests/test_pytmfunc.py +ls \ No newline at end of file From 5b1874a9d92f758b61cea2c6ae75073ec0295cd2 Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Mon, 2 May 2022 11:48:22 +0200 Subject: [PATCH 59/86] x.sh snug in. Removing it --- x.sh | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 x.sh diff --git a/x.sh b/x.sh deleted file mode 100644 index 504b5c00..00000000 --- a/x.sh +++ /dev/null @@ -1,7 +0,0 @@ -cd /pwd -ls -l -pip install -r requirements-dev.txt -pip install -r requirements.txt -pwd -python3 -m unittest -v tests/test_pytmfunc.py -ls \ No newline at end of file From 3e0ec9d44da85fc194f9c060bd2402f3332a560f Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Mon, 9 May 2022 11:48:36 +0200 Subject: [PATCH 60/86] Include all tests (test_*.py) --- tests/run-unittests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/run-unittests.sh b/tests/run-unittests.sh index c98f02fe..451663f6 100644 --- a/tests/run-unittests.sh +++ b/tests/run-unittests.sh @@ -3,4 +3,4 @@ cd /pwd && \ pip install -r requirements-dev.txt && \ pip install -r requirements.txt && \ -python3 -m unittest -v tests/test_pytmfunc.py \ No newline at end of file +python3 -m unittest -v tests/test_*.py \ No newline at end of file From 1ef2a205e4c12ff59dd0a4ca38d4d3d8a860745b Mon Sep 17 00:00:00 2001 From: Per Oestergaard Date: Mon, 9 May 2022 13:06:04 +0200 Subject: [PATCH 61/86] Empty commit to re-run codeql From 51bd6a7b11b44da51a540295c04711dae6506156 Mon Sep 17 00:00:00 2001 From: Al S Date: Mon, 30 May 2022 17:19:35 -0400 Subject: [PATCH 62/86] Correct base image to use Python's Alpine image As it stands, the current image is slim, which is Debian-based. This minor correction will use the `-alpine` flavor of official Python docker images so the apk commands work. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7622a28c..aa3047c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM python:3.11-rc-slim +FROM python:3.11-rc-alpine WORKDIR /usr/src/app From c8a9758be05fb9523352ffd0f56066e8206eb26b Mon Sep 17 00:00:00 2001 From: colesmj <39390458+colesmj@users.noreply.github.com> Date: Fri, 22 Jul 2022 17:14:31 -0400 Subject: [PATCH 63/86] Update LICENSE Fixed a spelling error. --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index af3342a2..2cb4bd89 100644 --- a/LICENSE +++ b/LICENSE @@ -22,7 +22,7 @@ SOFTWARE. ======================================================== -PyTM uses material from CAPEC on its threat catalog. CAPEC has its own license, reproduced bellow: +PyTM uses material from CAPEC on its threat catalog. CAPEC has its own license, reproduced below: LICENSE The MITRE Corporation (MITRE) hereby grants you a non-exclusive, royalty-free license to use Common Attack Pattern Enumeration and Classification (CAPEC™) for research, development, and commercial purposes. Any copy you make for such purposes is authorized provided that you reproduce MITRE’s copyright designation and this license in any such copy. From 589e17d24f2e0a51f3d1eed7cdcc64816ff2fd83 Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 28 Jul 2022 07:31:51 -0400 Subject: [PATCH 64/86] Adding in the Controls and DatastoreType classes to documentation --- docs/pytm/index.html | 1008 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1007 insertions(+), 1 deletion(-) diff --git a/docs/pytm/index.html b/docs/pytm/index.html index 25d09c87..90fd0a2e 100644 --- a/docs/pytm/index.html +++ b/docs/pytm/index.html @@ -31,10 +31,12 @@

      Package pytm

      "Actor", "Boundary", "Classification", + "Controls", "TLSVersion", "Data", "Dataflow", "Datastore", + "DatastoreType", "Element", "ExternalEntity", "Finding", @@ -58,9 +60,11 @@

      Package pytm

      Actor, Boundary, Classification, + Controls, Data, Dataflow, Datastore, + DatastoreType, Element, ExternalEntity, Finding, @@ -498,6 +502,822 @@

      Class variables

    +
    +class Controls +
    +
    +

    Controls implemented by/on and Element

    +
    + +Expand source code + +
    class Controls:
    +"""Controls implemented by/on and Element"""
    +
    +authenticatesDestination = varBool(
    +    False,
    +    doc="""Verifies the identity of the destination,
    +for example by verifying the authenticity of a digital certificate.""",
    +)
    +authenticatesSource = varBool(False)
    +authenticationScheme = varString("")
    +authorizesSource = varBool(False)
    +checksDestinationRevocation = varBool(
    +    False,
    +    doc="""Correctly checks the revocation status
    +of credentials used to authenticate the destination""",
    +)
    +checksInputBounds = varBool(False)
    +definesConnectionTimeout = varBool(False)
    +disablesDTD = varBool(False)
    +disablesiFrames = varBool(False)
    +encodesHeaders = varBool(False)
    +encodesOutput = varBool(False)
    +encryptsCookies = varBool(False)
    +encryptsSessionData = varBool(False)
    +handlesCrashes = varBool(False)
    +handlesInterruptions = varBool(False)
    +handlesResourceConsumption = varBool(False)
    +hasAccessControl = varBool(False)
    +implementsAuthenticationScheme = varBool(False)
    +implementsCSRFToken = varBool(False)
    +implementsNonce = varBool(
    +    False,
    +    doc="""Nonce is an arbitrary number
    +that can be used just once in a cryptographic communication.
    +It is often a random or pseudo-random number issued in an authentication protocol
    +to ensure that old communications cannot be reused in replay attacks.
    +They can also be useful as initialization vectors and in cryptographic
    +hash functions.""",
    +)
    +implementsPOLP = varBool(
    +    False,
    +    doc="""The principle of least privilege (PoLP),
    +also known as the principle of minimal privilege or the principle of least authority,
    +requires that in a particular abstraction layer of a computing environment,
    +every module (such as a process, a user, or a program, depending on the subject)
    +must be able to access only the information and resources
    +that are necessary for its legitimate purpose.""",
    +)
    +implementsServerSideValidation = varBool(False)
    +implementsStrictHTTPValidation = varBool(False)
    +invokesScriptFilters = varBool(False)
    +isEncrypted = varBool(False, doc="Requires incoming data flow to be encrypted")
    +isEncryptedAtRest = varBool(False, doc="Stored data is encrypted at rest")
    +isHardened = varBool(False)
    +isResilient = varBool(False)
    +providesConfidentiality = varBool(False)
    +providesIntegrity = varBool(False)
    +sanitizesInput = varBool(False)
    +tracksExecutionFlow = varBool(False)
    +usesCodeSigning = varBool(False)
    +usesEncryptionAlgorithm = varString("")
    +usesMFA = varBool(
    +    False,
    +    doc="""Multi-factor authentication is an authentication method
    +in which a computer user is granted access only after successfully presenting two
    +or more pieces of evidence (or factors) to an authentication mechanism: knowledge
    +(something the user and only the user knows), possession (something the user
    +and only the user has), and inherence (something the user and only the user is).""",
    +)
    +usesParameterizedInput = varBool(False)
    +usesSecureFunctions = varBool(False)
    +usesStrongSessionIdentifiers = varBool(False)
    +usesVPN = varBool(False)
    +validatesContentType = varBool(False)
    +validatesHeaders = varBool(False)
    +validatesInput = varBool(False)
    +verifySessionIdentifiers = varBool(False)
    +
    +def _attr_values(self):
    +    klass = self.__class__
    +    result = {}
    +    for i in dir(klass):
    +        if i.startswith("_") or callable(getattr(klass, i)):
    +            continue
    +        attr = getattr(klass, i, {})
    +        if isinstance(attr, var):
    +            value = attr.data.get(self, attr.default)
    +        else:
    +            value = getattr(self, i)
    +        result[i] = value
    +    return result
    +
    +
    +def _safeset(self, attr, value):
    +    try:
    +        setattr(self, attr, value)
    +    except ValueError:
    +        pass
    +
    +

    Instance variables

    +
    +
    var authenticatesDestination
    +
    +

    Verifies the identity of the destination, +for example by verifying the authenticity of a digital certificate.

    +
    + +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 authenticatesSource
    +
    +
    +
    + +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 authenticationScheme
    +
    +
    +
    + +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 authorizesSource
    +
    +
    +
    + +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 checksDestinationRevocation
    +
    +

    Correctly checks the revocation status +of credentials used to authenticate the destination

    +
    + +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 checksInputBounds
    +
    +
    +
    + +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 definesConnectionTimeout
    +
    +
    +
    + +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 disablesDTD
    +
    +
    +
    + +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 disablesiFrames
    +
    +
    +
    + +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 encodesHeaders
    +
    +
    +
    + +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 encodesOutput
    +
    +
    +
    + +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 encryptsCookies
    +
    +
    +
    + +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 encryptsSessionData
    +
    +
    +
    + +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 handlesCrashes
    +
    +
    +
    + +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 handlesInterruptions
    +
    +
    +
    + +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 handlesResourceConsumption
    +
    +
    +
    + +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 hasAccessControl
    +
    +
    +
    + +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 implementsAuthenticationScheme
    +
    +
    +
    + +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 implementsCSRFToken
    +
    +
    +
    + +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 implementsNonce
    +
    +

    Nonce is an arbitrary number +that can be used just once in a cryptographic communication. +It is often a random or pseudo-random number issued in an authentication protocol +to ensure that old communications cannot be reused in replay attacks. +They can also be useful as initialization vectors and in cryptographic +hash functions.

    +
    + +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 implementsPOLP
    +
    +

    The principle of least privilege (PoLP), +also known as the principle of minimal privilege or the principle of least authority, +requires that in a particular abstraction layer of a computing environment, +every module (such as a process, a user, or a program, depending on the subject) +must be able to access only the information and resources +that are necessary for its legitimate purpose.

    +
    + +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 implementsServerSideValidation
    +
    +
    +
    + +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 implementsStrictHTTPValidation
    +
    +
    +
    + +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 invokesScriptFilters
    +
    +
    +
    + +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 isEncrypted
    +
    +

    Requires incoming data flow to be encrypted

    +
    + +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 isEncryptedAtRest
    +
    +

    Stored data is encrypted at rest

    +
    + +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 isHardened
    +
    +
    +
    + +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 isResilient
    +
    +
    +
    + +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 providesConfidentiality
    +
    +
    +
    + +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 providesIntegrity
    +
    +
    +
    + +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 sanitizesInput
    +
    +
    +
    + +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 tracksExecutionFlow
    +
    +
    +
    + +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 usesCodeSigning
    +
    +
    +
    + +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 usesEncryptionAlgorithm
    +
    +
    +
    + +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 usesMFA
    +
    +

    Multi-factor authentication is an authentication method +in which a computer user is granted access only after successfully presenting two +or more pieces of evidence (or factors) to an authentication mechanism: knowledge +(something the user and only the user knows), possession (something the user +and only the user has), and inherence (something the user and only the user is).

    +
    + +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 usesParameterizedInput
    +
    +
    +
    + +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 usesSecureFunctions
    +
    +
    +
    + +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 usesStrongSessionIdentifiers
    +
    +
    +
    + +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 usesVPN
    +
    +
    +
    + +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 validatesContentType
    +
    +
    +
    + +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 validatesHeaders
    +
    +
    +
    + +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 validatesInput
    +
    +
    +
    + +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 verifySessionIdentifiers
    +
    +
    +
    + +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)
    +
    +
    +
    +
    class Data (name, **kwargs) @@ -1306,6 +2126,15 @@

    Methods

    that are necessary for its legitimate purpose.""", ) isEncryptedAtRest = varBool(False, doc="Stored data is encrypted at rest") + type = varDatastoreType( + DatastoreType.UNKNOWN, + doc="""The type of Datastore, values may be one of: +* UNKNOWN - unknown applicable +* FILE_SYSTEM - files on a file system +* SQL - A SQL Database +* LDAP - An LDAP Server +* AWS_S3 - An S3 Bucket within AWS""" + ) def __init__(self, name, **kwargs): super().__init__(name, **kwargs) @@ -1594,6 +2423,94 @@

    Instance variables

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

    The +type of Datastore, values may be one of: +* UNKNOWN - unknown applicable +* FILE_SYSTEM - files on a file system +* SQL - A SQL Database +* LDAP - An LDAP Server +* AWS_S3 - An S3 Bucket within AWS

    +
    + +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)
    +
    +
    + + + + +
    +class DatastoreType +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
    +
    +

    An enumeration.

    +
    + +Expand source code + +
    class DatastoreType(Enum):
    +    UNKNOWN = "UNKNOWN"
    +    FILE_SYSTEM = "FILE_SYSTEM"
    +    SQL = "SQL"
    +    LDAP = "LDAP"
    +    AWS_S3 = "AWS_S3"
    +
    +    def label(self):
    +        return self.value.lower().replace("_", " ")
    +
    +

    Ancestors

    +
      +
    • enum.Enum
    • +
    +

    Class variables

    +
    +
    var AWS_S3
    +
    +
    +
    +
    var FILE_SYSTEM
    +
    +
    +
    +
    var LDAP
    +
    +
    +
    +
    var SQL
    +
    +
    +
    +
    var UNKNOWN
    +
    +
    +
    +
    +

    Methods

    +
    +
    +def label(self) +
    +
    +
    +
    + +Expand source code + +
    def label(self):
    +    return self.value.lower().replace("_", " ")
    +
    +
    @@ -1635,11 +2552,13 @@

    Instance variables

    required=False, doc="Location of the source code that describes this element relative to the directory of the model script.", ) + controls = varControls(None) def __init__(self, name, **kwargs): for key, value in kwargs.items(): setattr(self, key, value) self.name = name + self.controls = Controls() self.uuid = uuid.UUID(int=random.getrandbits(128)) self._is_drawn = False TM._elements.append(self) @@ -1784,6 +2703,22 @@

    Subclasses

Instance variables

+
var controls
+
+
+
+ +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 description
@@ -3531,7 +4466,7 @@

Class variables

_data = [] _threatsExcluded = [] _sf = None - _duplicate_ignored_attrs = "name", "note", "order", "response", "responseTo" + _duplicate_ignored_attrs = "name", "note", "order", "response", "responseTo" "controls" name = varString("", required=True, doc="Model name") description = varString("", required=True, doc="Model description") threatsFile = varString( @@ -3673,6 +4608,16 @@

Class variables

right._is_drawn = True continue + left_controls_attrs = left.controls._attr_values() + right_controls_attrs = right.controls._attr_values() + #for a in self._duplicate_ignored_attrs: + # del left_controls_attrs[a], right_controls_attrs[a] + if left_controls_attrs != right_controls_attrs: + continue + if self.onDuplicates == Action.IGNORE: + right._is_drawn = True + continue + raise ValueError( "Duplicate Dataflow found between {} and {}: " "{} is same as {}".format( @@ -4596,6 +5541,54 @@

Classificat
  • +

    Controls

    + +
  • +
  • Data

    +
  • +
  • +

    DatastoreType

    +
  • Element