diff --git a/.gitignore b/.gitignore index dfbba4c..680bcf5 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,4 @@ plantuml.jar tm/ /sqldump /tests/.config.pytm +.aider* diff --git a/README.md b/README.md index 5fbcdbf..191c1c7 100644 --- a/README.md +++ b/README.md @@ -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] + [--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 @@ -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: diff --git a/pytm/pytm.py b/pytm/pytm.py index a711687..77fa5e4 100644 --- a/pytm/pytm.py +++ b/pytm/pytm.py @@ -51,6 +51,7 @@ def __init__(self, e, context): logger = logging.getLogger(__name__) +_defaultThreatsFile = os.path.dirname(__file__) + "/threatlib/threats.json" class var(object): @@ -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") @@ -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 @@ -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: + # 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) @@ -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]) @@ -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 diff --git a/sample.png b/sample.png deleted file mode 100644 index dc7d815..0000000 Binary files a/sample.png and /dev/null differ diff --git a/tests/1.json b/tests/1.json new file mode 100644 index 0000000..2123e62 --- /dev/null +++ b/tests/1.json @@ -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" + } +] + diff --git a/tests/2.json b/tests/2.json new file mode 100644 index 0000000..1d4cd78 --- /dev/null +++ b/tests/2.json @@ -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" + } +] + diff --git a/tests/output.json b/tests/output.json index 942519f..8989b5c 100644 --- a/tests/output.json +++ b/tests/output.json @@ -1405,5 +1405,5 @@ "name": "my test tm", "onDuplicates": "Action.NO_ACTION", "threatsExcluded": [], - "threatsFile": "pytm/threatlib/threats.json" + "threatsFile": "{'pytm/threatlib/threats.json'}" } diff --git a/tests/test_private_func.py b/tests/test_private_func.py index 4ce0816..39d0180 100644 --- a/tests/test_private_func.py +++ b/tests/test_private_func.py @@ -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) diff --git a/tests/test_pytmfunc.py b/tests/test_pytmfunc.py index f94d042..dd0cbfa 100644 --- a/tests/test_pytmfunc.py +++ b/tests/test_pytmfunc.py @@ -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] + + 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__))