Skip to content

Multi threat libs #263

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

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,4 @@ plantuml.jar
tm/
/sqldump
/tests/.config.pytm
.aider*
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,9 @@ make
All available arguments:

```text
usage: tm.py [-h] [--sqldump SQLDUMP] [--debug] [--dfd] [--report REPORT]
[--exclude EXCLUDE] [--seq] [--list] [--describe DESCRIBE]
[--list-elements] [--json JSON] [--levels LEVELS [LEVELS ...]]
[--stale_days STALE_DAYS]
usage: tm.py [-h] [--sqldump SQLDUMP] [--debug] [--dfd] [--report REPORT] [--exclude EXCLUDE] [--seq] [--list] [--colormap]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are you adding --colormap in this PR also?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it should be there already, it is an old one. It appears on the currently checked code on the master branch.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

oh this is the readme! let me fix that

[--describe DESCRIBE] [--list-elements] [--json JSON] [--levels LEVELS [LEVELS ...]] [--stale_days STALE_DAYS]
[--threat-files THREAT_FILES [THREAT_FILES ...]]

optional arguments:
-h, --help show this help message and exit
Expand All @@ -87,10 +86,18 @@ optional arguments:
checks if the delta between the TM script and the code
described by it is bigger than the specified value in
days
--threat-files THREAT_FILES [THREAT_FILES ...]
Files containing libraries of threats.
```

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.

The *THREAT_FILES* argument can list proprietary threat files. The keyword 'default' stands for the pytm library when it is wanted together with the proprietary files:

* nothing in the command line: uses the default library
* --threat-files foo.json : uses the threats in foo.json only
* --threat-files foo.json default : uses the threats in foo.json and the default ones

Currently available elements are: TM, Element, Server, ExternalEntity, Datastore, Actor, Process, SetOfProcesses, Dataflow, Boundary and Lambda.

The available properties of an element can be listed by using `--describe` followed by the name of an element:
Expand Down
49 changes: 35 additions & 14 deletions pytm/pytm.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def __init__(self, e, context):


logger = logging.getLogger(__name__)
_defaultThreatsFile = os.path.dirname(__file__) + "/threatlib/threats.json"


class var(object):
Expand Down Expand Up @@ -787,11 +788,7 @@ class TM:
)
name = varString("", required=True, doc="Model name")
description = varString("", required=True, doc="Model description")
threatsFile = varString(
os.path.dirname(__file__) + "/threatlib/threats.json",
onSet=lambda i, v: i._init_threats(),
doc="JSON file with custom threats",
)
threatsFile = varStrings([_defaultThreatsFile])
isOrdered = varBool(False, doc="Automatically order all Dataflows")
mergeResponses = varBool(False, doc="Merge response edges in DFDs")
ignoreUnused = varBool(False, doc="Ignore elements not used in any Dataflow")
Expand Down Expand Up @@ -838,16 +835,19 @@ def _init_threats(self):
self._add_threats()

def _add_threats(self):
try:
with open(self.threatsFile, "r", encoding="utf8") as threat_file:
threats_json = json.load(threat_file)
except (FileNotFoundError, PermissionError, IsADirectoryError) as e:
raise UIError(
e, f"while trying to open the the threat file ({self.threatsFile})."

for tf in self.threatsFile:
try:
with open(tf, "r", encoding="utf8") as threat_file:
threats_json = json.load(threat_file)
except (FileNotFoundError, PermissionError, IsADirectoryError) as e:
raise UIError(
e, f"while trying to open the the threat file ({tf})."
)
active_threats = (threat for threat in threats_json if "DEPRECATED" not in threat)
for threat in active_threats:
TM._threats.append(Threat(**threat))

active_threats = (threat for threat in threats_json if "DEPRECATED" not in threat)
for threat in active_threats:
TM._threats.append(Threat(**threat))

def resolve(self):
finding_count = 0
Expand Down Expand Up @@ -1129,6 +1129,21 @@ def _process(self):
result = get_args()
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")

# delaying loading of threats to accomodate multiple threat files in the
# command line
if result.threat_files:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do extra work on line 791 to load the default when you can just do it here? You will always reach this line at least once, correct?

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 don't remember the details but there is a timing issue there. Something around things being used before loaded.

# start by removing the default
del self.threatsFile[0]
if "default" in result.threat_files:
index = result.threat_files.index("default")
result.threat_files[index] = _defaultThreatsFile
for x in result.threat_files:
self.threatsFile.append(x)
else:
# it is just the default file, so no need to do anything
pass
self._init_threats()

if result.debug:
logger.setLevel(logging.DEBUG)

Expand Down Expand Up @@ -1178,6 +1193,7 @@ def _process(self):
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])
Expand Down Expand Up @@ -2158,6 +2174,11 @@ def get_args():
help="""checks if the delta between the TM script and the code described by it is bigger than the specified value in days""",
type=int,
)
_parser.add_argument(
"--threat-files",
nargs="+",
help="Files containing libraries of threats."
)

_args = _parser.parse_args()
return _args
Binary file removed sample.png
Binary file not shown.
19 changes: 19 additions & 0 deletions tests/1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[
{
"SID": "FOO",
"target": [
"Lambda",
"Process"
],
"description": "FOOOOOOOOOOOOOO",
"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.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.",
"references": "https://capec.mitre.org/data/definitions/10.html, CVE-1999-0906, CVE-1999-0046, http://cwe.mitre.org/data/definitions/120.html, http://cwe.mitre.org/data/definitions/119.html, http://cwe.mitre.org/data/definitions/680.html"
}
]

19 changes: 19 additions & 0 deletions tests/2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[
{
"SID": "BAR",
"target": [
"Lambda",
"Process"
],
"description": "FOOOOOOOOOOOOOO",
"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.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.",
"references": "https://capec.mitre.org/data/definitions/10.html, CVE-1999-0906, CVE-1999-0046, http://cwe.mitre.org/data/definitions/120.html, http://cwe.mitre.org/data/definitions/119.html, http://cwe.mitre.org/data/definitions/680.html"
}
]

2 changes: 1 addition & 1 deletion tests/output.json
Original file line number Diff line number Diff line change
Expand Up @@ -1405,5 +1405,5 @@
"name": "my test tm",
"onDuplicates": "Action.NO_ACTION",
"threatsExcluded": [],
"threatsFile": "pytm/threatlib/threats.json"
"threatsFile": "{'pytm/threatlib/threats.json'}"
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is the output a string and not a list of strings?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

that's what varStrings with only one value put out...

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this a bug?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

in the immortal words of just about everybody..."works for me"!

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok I had a quick look and the issue is that the default to_serializable is used.

pytm/pytm/pytm.py

Lines 2015 to 2018 in 9dc0f1f

@singledispatch
def to_serializable(val):
"""Used by default."""
return str(val)

This is just the same as

json.dumps(str(set(["./a.json"])))

The issue is created by

pytm/pytm/pytm.py

Line 2023 in 9dc0f1f

return serialize(obj, nested=True)

Because of this check for not nested in serialize().

pytm/pytm/pytm.py

Line 2064 in 9dc0f1f

not nested

pytm/pytm/pytm.py

Lines 2035 to 2070 in 9dc0f1f

def serialize(obj, nested=False):
"""Used if *obj* is an instance of TM, Element, Threat or Finding."""
klass = obj.__class__
result = {}
if isinstance(obj, (Actor, Asset)):
result["__class__"] = klass.__name__
for i in dir(obj):
if (
i.startswith("__")
or callable(getattr(klass, i, {}))
or (
isinstance(obj, TM)
and i in ("_sf", "_duplicate_ignored_attrs", "_threats")
)
or (isinstance(obj, Element) and i in ("_is_drawn", "uuid"))
or (isinstance(obj, Finding) and i == "element")
):
continue
value = getattr(obj, i)
if isinstance(obj, TM) and i == "_elements":
value = [e for e in value if isinstance(e, (Actor, Asset))]
if value is not None:
if isinstance(value, (Element, Data)):
value = value.name
elif isinstance(obj, Threat) and i == "target":
value = [v.__name__ for v in value]
elif i in ("levels", "sourceFiles", "assumptions"):
value = list(value)
elif (
not nested
and not isinstance(value, str)
and isinstance(value, Iterable)
):
value = [v.id if isinstance(v, Finding) else v.name for v in value]
result[i.lstrip("_")] = value
return result

This whole serialize function is a bit overloaded with special handling of member variables and might require a rewrite.
All the checks seem to be for specific classes which are all handle in the same function.
Also why is there the check for nested?
This is basically equivalent to checking if the class is TM.

A quick fix would be something like if instance(obj, TM) and i == "threatsFile" but oh boy that is not nice.

Should this be a new issue?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sounds like something to be addressed. @nineinchnick , you there?

Copy link
Collaborator

Choose a reason for hiding this comment

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

There appears to be only 1 check for nested. If nested is true, the behavior seems to be potentially undefined (if the code reaches the not nested elif).

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes. I started to rewrite the code see #268, but it is difficult to understand what the intention here was.
It seems that the default is result[i.lstrip("_")] = value and the cases above only change the value of value.
So setting nested = True means that value will not be touched, except it is a Element, Data, or Threat. Or the name of the member is in ("levels", "sourceFiles", "assumptions").

The last one seems to be a fix for a specific class.

}
4 changes: 4 additions & 0 deletions tests/test_private_func.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,15 @@ def test_kwargs(self):
def test_load_threats(self):
tm = TM("TM")
self.assertNotEqual(len(TM._threats), 0)

''' commenting this bit off since it is now valid to change threatsFile,
but looking for confirmation from Jan that it is ok to do so
with self.assertRaises(UIError):
tm.threatsFile = "threats.json"

with self.assertRaises(UIError):
TM("TM", threatsFile="threats.json")
'''

def test_responses(self):
tm = TM("my test tm", description="aa", isOrdered=True)
Expand Down
17 changes: 17 additions & 0 deletions tests/test_pytmfunc.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,23 @@ def test_json_loads(self):
[f.name for f in tm._flows], ["Request", "Insert", "Select", "Response"]
)

def test_threat_files(self):
dir_path = os.path.dirname(os.path.realpath(__file__))
foo_file = f"{dir_path}/1.json"
bar_file = f"{dir_path}/2.json"
threat_files = [os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+ "/pytm/threatlib/threats.json", foo_file, bar_file]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you have a test for when the default is not included in threatsfile?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hm. No. Gotta do one, you're right.


TM.reset()
tm = TM("testing multiple threat library files",
description="aaa",
threatsFile=threat_files)
ctr = 0
for t in TM._threats:
if t.id == "FOO" or t.id == "BAR":
ctr += 1
self.assertTrue(ctr == 2)

def test_report(self):
random.seed(0)
dir_path = os.path.dirname(os.path.realpath(__file__))
Expand Down