Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature to define findings manually in the model, similar to over… #180

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
78 changes: 64 additions & 14 deletions docs/pytm/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<meta name="generator" content="pdoc 0.9.2" />
<meta name="generator" content="pdoc 0.10.0" />
<title>pytm API documentation</title>
<meta name="description" content="" />
<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/sanitize.min.css" integrity="sha256-PK9q560IAAa6WVRRh76LtCaI8pjTJ2z11v0miyNNjrs=" crossorigin>
Expand Down Expand Up @@ -2134,17 +2134,17 @@ <h3>Instance variables</h3>
&#34;&#34;&#34;Represents a Finding - the element in question
and a description of the finding&#34;&#34;&#34;

element = varElement(None, required=True, doc=&#34;Element this finding applies to&#34;)
element = varElement(None, required=False, doc=&#34;Element this finding applies to&#34;)
target = varString(&#34;&#34;, doc=&#34;Name of the element this finding applies to&#34;)
description = varString(&#34;&#34;, required=True, doc=&#34;Threat description&#34;)
details = varString(&#34;&#34;, required=True, doc=&#34;Threat details&#34;)
severity = varString(&#34;&#34;, required=True, doc=&#34;Threat severity&#34;)
mitigations = varString(&#34;&#34;, required=True, doc=&#34;Threat mitigations&#34;)
example = varString(&#34;&#34;, required=True, doc=&#34;Threat example&#34;)
severity = varString(&#34;&#34;, required=False, doc=&#34;Threat severity&#34;)
mitigations = varString(&#34;&#34;, required=False, doc=&#34;Threat mitigations&#34;)
example = varString(&#34;&#34;, required=False, doc=&#34;Threat example&#34;)
id = varString(&#34;&#34;, required=True, doc=&#34;Finding ID&#34;)
threat_id = varString(&#34;&#34;, required=True, doc=&#34;Threat ID&#34;)
references = varString(&#34;&#34;, required=True, doc=&#34;Threat references&#34;)
condition = varString(&#34;&#34;, required=True, doc=&#34;Threat condition&#34;)
threat_id = varString(&#34;&#34;, required=False, doc=&#34;Threat ID&#34;)
references = varString(&#34;&#34;, required=False, doc=&#34;Threat references&#34;)
condition = varString(&#34;&#34;, required=False, doc=&#34;Threat condition&#34;)
response = varString(
&#34;&#34;,
required=False,
Expand All @@ -2157,6 +2157,7 @@ <h3>Instance variables</h3>
&#34;&#34;&#34;,
)
cvss = varString(&#34;&#34;, required=False, doc=&#34;The CVSS score and/or vector&#34;)
source = varString(&#34;manual&#34;, required=False, doc=&#34;The source of the Finding.&#34;)

def __init__(
self,
Expand Down Expand Up @@ -2218,7 +2219,7 @@ <h3>Instance variables</h3>
)

def __str__(self):
return f&#34;{self.target}: {self.description}\n{self.details}\n{self.severity}&#34;</code></pre>
return f&#34;&#39;{self.target}&#39;: {self.description}\n{self.details}\n{self.severity}&#34;</code></pre>
</details>
<h3>Instance variables</h3>
<dl>
Expand Down Expand Up @@ -2403,6 +2404,22 @@ <h3>Instance variables</h3>
return self.data.get(instance, self.default)</code></pre>
</details>
</dd>
<dt id="pytm.Finding.source"><code class="name">var <span class="ident">source</span></code></dt>
<dd>
<div class="desc"><p>The source of the Finding.</p></div>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">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)</code></pre>
</details>
</dd>
<dt id="pytm.Finding.target"><code class="name">var <span class="ident">target</span></code></dt>
<dd>
<div class="desc"><p>Name of the element this finding applies to</p></div>
Expand Down Expand Up @@ -3587,6 +3604,16 @@ <h3>Class variables</h3>
if not e.inScope:
continue

if (len(e.findings) &gt; 0):

for f in e.findings:
finding_count += 1
f._safeset(&#34;id&#34;, str(finding_count))
f._safeset(&#34;element&#34;, e)
f._safeset(&#34;target&#34;, e.name)
findings.append(f)
elements[e].append(f)

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
Expand All @@ -3602,12 +3629,15 @@ <h3>Class variables</h3>
continue

finding_count += 1
f = Finding(e, id=str(finding_count), threat=t)
f = Finding(e, id=str(finding_count), threat=t, source=&#34;pytm&#34;)
logger.debug(f&#34;new finding: {f}&#34;)
findings.append(f)
elements[e].append(f)

self.findings = findings
for e, findings in elements.items():
e.findings = findings
e._safeset(&#34;findings&#34;, findings)
#e.findings = findings

def check(self):
if self.description is None:
Expand Down Expand Up @@ -3839,6 +3869,9 @@ <h3>Class variables</h3>
if result.describe is not None:
_describe_classes(result.describe.split())

if result.list_elements:
_list_elements()

if result.list is True:
[print(&#34;{} - {}&#34;.format(t.id, t.description)) for t in TM._threats]

Expand Down Expand Up @@ -4160,6 +4193,9 @@ <h3>Methods</h3>
if result.describe is not None:
_describe_classes(result.describe.split())

if result.list_elements:
_list_elements()

if result.list is True:
[print(&#34;{} - {}&#34;.format(t.id, t.description)) for t in TM._threats]

Expand Down Expand Up @@ -4215,6 +4251,16 @@ <h3>Methods</h3>
if not e.inScope:
continue

if (len(e.findings) &gt; 0):

for f in e.findings:
finding_count += 1
f._safeset(&#34;id&#34;, str(finding_count))
f._safeset(&#34;element&#34;, e)
f._safeset(&#34;target&#34;, e.name)
findings.append(f)
elements[e].append(f)

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
Expand All @@ -4230,12 +4276,15 @@ <h3>Methods</h3>
continue

finding_count += 1
f = Finding(e, id=str(finding_count), threat=t)
f = Finding(e, id=str(finding_count), threat=t, source=&#34;pytm&#34;)
logger.debug(f&#34;new finding: {f}&#34;)
findings.append(f)
elements[e].append(f)

self.findings = findings
for e, findings in elements.items():
e.findings = findings</code></pre>
e._safeset(&#34;findings&#34;, findings)
#e.findings = findings</code></pre>
</details>
</dd>
<dt id="pytm.TM.sqlDump"><code class="name flex">
Expand Down Expand Up @@ -4673,6 +4722,7 @@ <h4><code><a title="pytm.Finding" href="#pytm.Finding">Finding</a></code></h4>
<li><code><a title="pytm.Finding.references" href="#pytm.Finding.references">references</a></code></li>
<li><code><a title="pytm.Finding.response" href="#pytm.Finding.response">response</a></code></li>
<li><code><a title="pytm.Finding.severity" href="#pytm.Finding.severity">severity</a></code></li>
<li><code><a title="pytm.Finding.source" href="#pytm.Finding.source">source</a></code></li>
<li><code><a title="pytm.Finding.target" href="#pytm.Finding.target">target</a></code></li>
<li><code><a title="pytm.Finding.threat_id" href="#pytm.Finding.threat_id">threat_id</a></code></li>
</ul>
Expand Down Expand Up @@ -4805,7 +4855,7 @@ <h4><code><a title="pytm.Threat" href="#pytm.Threat">Threat</a></code></h4>
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc"><cite>pdoc</cite> 0.9.2</a>.</p>
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.10.0</a>.</p>
</footer>
</body>
</html>
3 changes: 3 additions & 0 deletions docs/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ Name|Description|Classification
<p>{{item.mitigations}}</p>
<h6>References</h6>
<p>{{item.references}}</p>
<h6>Source</h6>
<p>{{item.source}}</p>
&nbsp;
&nbsp;
&emsp;
</details>
}|

99 changes: 68 additions & 31 deletions pytm/pytm.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,15 @@ def __set__(self, instance, value):
# called when x.d = val
# instance = x
# value = val

if instance in self.data:
raise ValueError(
"cannot overwrite {}.{} value with {}, already set to {}".format(
instance, self.__class__.__name__, value, self.data[instance]
if (not isinstance(instance, Finding)):
nozmore marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError(
"cannot overwrite {}.{} value with {}, already set to {}".format(
instance, self.__class__.__name__, value, self.data[instance]
)
)
)

self.data[instance] = value
if self.onSet is not None:
self.onSet(instance, value)
Expand Down Expand Up @@ -603,17 +606,17 @@ class Finding:
"""Represents a Finding - the element in question
and a description of the finding"""

element = varElement(None, required=True, doc="Element this finding applies to")
element = varElement(None, required=False, doc="Element this finding applies to")
target = varString("", doc="Name of the element this finding applies to")
description = varString("", required=True, doc="Threat description")
details = varString("", required=True, doc="Threat details")
severity = varString("", required=True, doc="Threat severity")
mitigations = varString("", required=True, doc="Threat mitigations")
example = varString("", required=True, doc="Threat example")
severity = varString("", required=False, doc="Threat severity")
mitigations = varString("", required=False, doc="Threat mitigations")
example = varString("", required=False, doc="Threat example")
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")
threat_id = varString("", required=False, doc="Threat ID")
references = varString("", required=False, doc="Threat references")
condition = varString("", required=False, doc="Threat condition")
response = varString(
"",
required=False,
Expand All @@ -626,6 +629,7 @@ class Finding:
""",
)
cvss = varString("", required=False, doc="The CVSS score and/or vector")
source = varString("manual", required=False, doc="The source of the Finding.")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name source on its own is too vague. Can the doc mention that it's automatically populated with pytm for findings matched from the threat library? It'll be the second possible value next to the default value, and it would help users to figure out the meaning of this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I'll add some better text. I could remove the logic around source altogether and we just add it to every element in the threatlib.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine to have that logic, it's better to have this field only in Finding than both in here and in Threat


def __init__(
self,
Expand All @@ -635,10 +639,12 @@ def __init__(
if args:
element = args[0]
else:
element = kwargs.pop("element", Element("invalid"))
element = kwargs.pop("element", None)

if (element):
self.target = element.name
self.element = element

self.target = element.name
self.element = element
attrs = [
"description",
"details",
Expand All @@ -656,25 +662,30 @@ def __init__(
kwargs[a] = getattr(threat, a)

threat_id = kwargs.get("threat_id", None)
for f in element.overrides:
if f.threat_id != threat_id:
continue
for i in dir(f.__class__):
attr = getattr(f.__class__, i)
if (
i in ("element", "target")
or i.startswith("_")
or callable(attr)
or not isinstance(attr, var)
):

if (element):
for f in element.overrides:
if f.threat_id != threat_id:
continue
if f in attr.data:
kwargs[i] = attr.data[f]
break

for i in dir(f.__class__):
attr = getattr(f.__class__, i)
if (
i in ("element", "target")
or i.startswith("_")
or callable(attr)
or not isinstance(attr, var)
):
continue
if f in attr.data:
kwargs[i] = attr.data[f]
break

for k, v in kwargs.items():
setattr(self, k, v)

TM._findings.append(self)


def _safeset(self, attr, value):
try:
setattr(self, attr, value)
Expand Down Expand Up @@ -704,6 +715,7 @@ class TM:
_threatsExcluded = []
_sf = None
_duplicate_ignored_attrs = "name", "note", "order", "response", "responseTo"
_findings = []
name = varString("", required=True, doc="Model name")
description = varString("", required=True, doc="Model description")
threatsFile = varString(
Expand Down Expand Up @@ -739,6 +751,7 @@ def reset(cls):
cls._threats = []
cls._boundaries = []
cls._data = []
cls._findings = []

def _init_threats(self):
TM._threats = []
Expand All @@ -755,10 +768,30 @@ def resolve(self):
finding_count = 0
findings = []
elements = defaultdict(list)

#Manually added findings with element as arg to Finding object
for f in TM._findings:
if (f.element):
finding_count += 1
f._safeset("id", str(finding_count))
findings.append(f)
elements[f.element].append(f)

for e in TM._elements:
if not e.inScope:
continue

#Manually added findings, added to an element's finding attribute
if (len(e.findings) > 0):

for f in e.findings:
finding_count += 1
f._safeset("id", str(finding_count))
f._safeset("element", e)
f._safeset("target", e.name)
findings.append(f)
elements[e].append(f)

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
Expand All @@ -769,18 +802,21 @@ def resolve(self):
except AttributeError:
pass

#Findings added by pytm using threatlib
for t in TM._threats:
if not t.apply(e) and t.id not in override_ids:
continue

finding_count += 1
f = Finding(e, id=str(finding_count), threat=t)
f = Finding(e, id=str(finding_count), threat=t, source="pytm")
logger.debug(f"new finding: {f}")
findings.append(f)
elements[e].append(f)

self.findings = findings

for e, findings in elements.items():
e.findings = findings
e._safeset("findings", findings)

def check(self):
if self.description is None:
Expand Down Expand Up @@ -1854,6 +1890,7 @@ def encode_threat_data(obj):
"threat_id",
"references",
"condition",
"source",
]

if type(obj) is Finding or (len(obj) != 0 and type(obj[0]) is Finding):
Expand Down
2 changes: 1 addition & 1 deletion tests/output.json
Original file line number Diff line number Diff line change
Expand Up @@ -801,4 +801,4 @@
"onDuplicates": "Action.NO_ACTION",
"threatsExcluded": [],
"threatsFile": "pytm/threatlib/threats.json"
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't remove end of line at end of file. Configure your text editor not to do that automatically.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replaced the json using the produced json during the tests. I'm editing in vi so nothing is removed automatically on my end. If the space is important we could look to add it to the generated file.

Loading