Skip to content

Commit

Permalink
Merge pull request #104 from nineinchnick/data-prop
Browse files Browse the repository at this point in the history
upgrade data attr to an object
  • Loading branch information
izar authored Sep 16, 2020
2 parents 0bcec71 + cff863b commit f4b48f0
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 30 deletions.
9 changes: 9 additions & 0 deletions docs/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,18 @@ 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

 
 

Expand Down
4 changes: 2 additions & 2 deletions pytm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__all__ = ['Element', 'Server', 'ExternalEntity', 'Datastore', 'Actor', 'Process', 'SetOfProcesses', 'Dataflow', 'Boundary', 'TM', 'Action', 'Lambda', 'Threat']
__all__ = ['Element', 'Server', 'ExternalEntity', 'Datastore', 'Actor', 'Process', 'SetOfProcesses', 'Dataflow', 'Boundary', 'TM', 'Action', 'Lambda', 'Threat', 'Classification', 'Data']

from .pytm import Element, Server, ExternalEntity, Dataflow, Datastore, Actor, Process, SetOfProcesses, Boundary, TM, Action, Lambda, Threat
from .pytm import Element, Server, ExternalEntity, Dataflow, Datastore, Actor, Process, SetOfProcesses, Boundary, TM, Action, Lambda, Threat, Classification, Data

191 changes: 174 additions & 17 deletions pytm/pytm.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ def __set__(self, instance, value):
# value = val
if instance in self.data:
raise ValueError(
"cannot overwrite {} value with {}, already set to {}".format(
self.__class__.__name__, value, self.data[instance]
"cannot overwrite {}.{} value with {}, already set to {}".format(
instance, self.__class__.__name__, value, self.data[instance]
)
)
self.data[instance] = value
Expand Down Expand Up @@ -109,7 +109,7 @@ def __set__(self, instance, value):
if not isinstance(e, Element):
raise ValueError(
"expecting a list of Elements, item number {} is a {}".format(
i, type(value)
i, type(e)
)
)
super().__set__(instance, list(value))
Expand All @@ -122,7 +122,7 @@ def __set__(self, instance, value):
if not isinstance(e, Finding):
raise ValueError(
"expecting a list of Findings, item number {} is a {}".format(
i, type(value)
i, type(e)
)
)
super().__set__(instance, list(value))
Expand All @@ -136,12 +136,91 @@ def __set__(self, instance, value):
super().__set__(instance, value)


class varClassification(var):

def __set__(self, instance, value):
if not isinstance(value, Classification):
raise ValueError("expecting a Classification, got a {}".format(type(value)))
super().__set__(instance, value)


class varData(var):

def __set__(self, instance, value):
if isinstance(value, str):
value = [Data(value)]
if not isinstance(value, Iterable):
value = [value]
for i, e in enumerate(value):
if not isinstance(e, Data):
raise ValueError(
"expecting a list of Data, item number {} is a {}".format(
i, type(e)
)
)
super().__set__(instance, DataSet(value))


class DataSet(set):
def __contains__(self, item):
if isinstance(item, str):
return item in [d.name for d in self]
if isinstance(item, Data):
return super().__contains__(item)
return NotImplemented

def __eq__(self, other):
if isinstance(other, set):
return super().__eq__(other)
if isinstance(other, str):
return other in self
return NotImplemented

def __ne__(self, other):
if isinstance(other, set):
return super().__ne__(other)
if isinstance(other, str):
return other not in self
return NotImplemented


class Action(Enum):
NO_ACTION = 'NO_ACTION'
RESTRICT = 'RESTRICT'
IGNORE = 'IGNORE'


class OrderedEnum(Enum):
def __ge__(self, other):
if self.__class__ is other.__class__:
return self.value >= other.value
return NotImplemented

def __gt__(self, other):
if self.__class__ is other.__class__:
return self.value > other.value
return NotImplemented

def __le__(self, other):
if self.__class__ is other.__class__:
return self.value <= other.value
return NotImplemented

def __lt__(self, other):
if self.__class__ is other.__class__:
return self.value < other.value
return NotImplemented


class Classification(OrderedEnum):
UNKNOWN = 0
PUBLIC = 1
RESTRICTED = 2
SENSITIVE = 3
SECRET = 4
TOP_SECRET = 5


def _sort(flows, addOrder=False):
ordered = sorted(flows, key=lambda flow: flow.order)
if not addOrder:
Expand Down Expand Up @@ -203,11 +282,30 @@ def _match_responses(flows):
return flows


def _apply_defaults(flows):
def _apply_defaults(flows, data):
inputs = defaultdict(list)
outputs = defaultdict(list)
carriers = defaultdict(set)
processors = defaultdict(set)

for d in data:
for e in d.carriedBy:
try:
setattr(e, "data", d)
except ValueError:
e.data.add(d)

for e in flows:
e._safeset("data", e.source.data)
if e.source.data:
try:
setattr(e, "data", e.source.data.copy())
except ValueError:
e.data.update(e.source.data)

for d in e.data:
carriers[d].add(e)
processors[d].add(e.source)
processors[d].add(e.sink)

if e.isResponse:
e._safeset("protocol", e.source.protocol)
Expand Down Expand Up @@ -235,6 +333,21 @@ def _apply_defaults(flows):
except (AttributeError, ValueError):
pass

for d, flows in carriers.items():
try:
setattr(d, "carriedBy", list(flows))
except ValueError:
for e in flows:
if e not in d.carriedBy:
d.carriedBy.append(e)
for d, elements in processors.items():
try:
setattr(d, "processedBy", list(elements))
except ValueError:
for e in elements:
if e not in d.processedBy:
d.processedBy.append(e)


def _describe_classes(classes):
for name in classes:
Expand Down Expand Up @@ -370,6 +483,7 @@ class TM():
_BagOfElements = []
_BagOfThreats = []
_BagOfBoundaries = []
_BagOfData = []
_threatsExcluded = []
_sf = None
_duplicate_ignored_attrs = "name", "note", "order", "response", "responseTo"
Expand Down Expand Up @@ -400,6 +514,7 @@ def reset(cls):
cls._BagOfElements = []
cls._BagOfThreats = []
cls._BagOfBoundaries = []
cls._BagOfData = []

def _init_threats(self):
TM._BagOfThreats = []
Expand Down Expand Up @@ -443,7 +558,7 @@ def check(self):
a brief description of the system being modeled.""")
TM._BagOfFlows = _match_responses(_sort(TM._BagOfFlows, self.isOrdered))
self._check_duplicates(TM._BagOfFlows)
_apply_defaults(TM._BagOfFlows)
_apply_defaults(TM._BagOfFlows, TM._BagOfData)
if self.ignoreUnused:
TM._BagOfElements, TM._BagOfBoundaries = _get_elements_and_boundaries(
TM._BagOfFlows
Expand Down Expand Up @@ -566,11 +681,12 @@ def report(self, *args, **kwargs):

data = {
"tm": self,
"dataflows": TM._BagOfFlows,
"threats": TM._BagOfThreats,
"dataflows": self._BagOfFlows,
"threats": self._BagOfThreats,
"findings": self.findings,
"elements": TM._BagOfElements,
"boundaries": TM._BagOfBoundaries,
"elements": self._BagOfElements,
"boundaries": self._BagOfBoundaries,
"data": self._BagOfData,
}
return self._sf.format(template, **data)

Expand Down Expand Up @@ -604,6 +720,11 @@ class Element():
inScope = varBool(True, doc="Is the element in scope of the threat model")
onAWS = varBool(False)
isHardened = varBool(False)
maxClassification = varClassification(
Classification.UNKNOWN,
required=False,
doc="Maximum data classification this element can handle.",
)
implementsAuthenticationScheme = varBool(False)
implementsNonce = varBool(False, doc="""Nonce is an arbitrary number
that can be used just once in a cryptographic communication.
Expand Down Expand Up @@ -753,12 +874,40 @@ def _attr_values(self):
return result


class Data():
"""Represents a single piece of data that traverses the system"""

name = varString("", required=True)
description = varString("")
classification = varClassification(
Classification.PUBLIC,
required=True,
doc="Level of classification for this piece of data",
)
carriedBy = varElements([], doc="Dataflows that carries this piece of data")
processedBy = varElements([], doc="Elements that store/process this piece of data")

def __init__(self, name, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
self.name = name
TM._BagOfData.append(self)

def __repr__(self):
return "<{0}.{1}({2}) at {3}>".format(
self.__module__, type(self).__name__, self.name, hex(id(self))
)

def __str__(self):
return "{0}({1})".format(type(self).__name__, self.name)


class Lambda(Element):
"""A lambda function running in a Function-as-a-Service (FaaS) environment"""

port = varInt(-1, doc="Default TCP port for outgoing data flows")
protocol = varString("", doc="Default network protocol for outgoing data flows")
data = varString("", doc="Default type of data in outgoing data flows")
data = varData([], doc="Default type of data in outgoing data flows")
onAWS = varBool(True)
authenticatesSource = varBool(False)
hasAccessControl = varBool(False)
Expand Down Expand Up @@ -814,7 +963,7 @@ class Server(Element):
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 = varString("", doc="Default type of data in incoming data flows")
data = varData([], doc="Default type of data in incoming data flows")
inputs = varElements([], doc="incoming Dataflows")
outputs = varElements([], doc="outgoing Dataflows")
providesConfidentiality = varBool(False)
Expand Down Expand Up @@ -872,7 +1021,7 @@ class Datastore(Element):
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 = varString("", doc="Default type of data in incoming data flows")
data = varData([], doc="Default type of data in incoming data flows")
inputs = varElements([], doc="incoming Dataflows")
outputs = varElements([], doc="outgoing Dataflows")
onRDS = varBool(False)
Expand Down Expand Up @@ -926,7 +1075,7 @@ class Actor(Element):

port = varInt(-1, doc="Default TCP port for outgoing data flows")
protocol = varString("", doc="Default network protocol for outgoing data flows")
data = varString("", doc="Default type of data in outgoing data flows")
data = varData([], doc="Default type of data in outgoing data flows")
inputs = varElements([], doc="incoming Dataflows")
outputs = varElements([], doc="outgoing Dataflows")

Expand All @@ -940,7 +1089,7 @@ class Process(Element):
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 = varString("", doc="Default type of data in incoming data flows")
data = varData([], doc="Default type of data in incoming data flows")
inputs = varElements([], doc="incoming Dataflows")
outputs = varElements([], doc="outgoing Dataflows")
codeType = varString("Unmanaged")
Expand Down Expand Up @@ -1012,7 +1161,7 @@ class Dataflow(Element):
dstPort = varInt(-1, doc="Destination TCP port")
isEncrypted = varBool(False, doc="Is the data encrypted")
protocol = varString("", doc="Protocol used in this data flow")
data = varString("", "Type of data carried in this data flow")
data = varData([], "Type of data carried in this data flow")
authenticatedWith = varBool(False)
order = varInt(-1, doc="Number of this data flow in the threat model")
implementsCommunicationProtocol = varBool(False)
Expand Down Expand Up @@ -1062,6 +1211,14 @@ def dfd(self, mergeResponses=False, **kwargs):
color=self._color(),
)

def hasDataLeaks(self):
return any(
d.classification > self.source.maxClassification
or d.classification > self.sink.maxClassification
or d.classification > self.maxClassification
for d in self.data
)


class Boundary(Element):
"""Trust boundary"""
Expand Down
17 changes: 15 additions & 2 deletions pytm/threatlib/threats.json
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,19 @@
"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"
},
{
"SID": "DS06",
"target":["Dataflow"],
"description": "Data Leak",
"details": "An attacker can access data in transit or at rest that is not sufficiently protected. If an attacker can decrypt a stored password, it might be used to authenticate against different services.",
"Likelihood Of Attack": "High",
"severity": "Very High",
"prerequisites": "",
"condition": "target.hasDataLeaks",
"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": "An application, which connects to a database without TLS, performs a database query in which it compares the password to a stored hash, instead of fetching the hash and comparing it locally.",
"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"
}
]
]

Loading

0 comments on commit f4b48f0

Please sign in to comment.