diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d48f8ded..87e9bf6a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,5 +25,4 @@ jobs: # Runs a set of commands using the runners shell - name: Run pytm tests run: | - cd tests - python3 ./test_pytmfunc.py + python3 -m unittest diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..ae817261 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,55 @@ +# 2.0.0 + +## Breaking changes + +- Removed `HandlesResources` attribute from the `Process` class, which duplicates `handlesResources` +- Change default `Dataflow.dstPort` attribute value from `10000` to `-1` + +## New features + +- Move authenticateDestination to base Element [#88](https://github.com/izar/pytm/pull/88) +- Assign inputs and outputs to all elements [#89](https://github.com/izar/pytm/pull/89) +- Allow detecting and/or hiding duplicate dataflows by setting `TM.onDuplicates` [#100](https://github.com/izar/pytm/pull/100) +- Ignore unused elements if `TM.ignoreUnused` is True [#84](https://github.com/izar/pytm/pull/84) +- Assign findings to elements [#86](https://github.com/izar/pytm/pull/86) +- Add description to class attributes [#91](https://github.com/izar/pytm/pull/91) +- New Element methods to be used in threat conditions [#82](https://github.com/izar/pytm/pull/82) +- Provide a Docker image and allow running make targets in a container [#87](https://github.com/izar/pytm/pull/87) +- Dataflow inherits source and/or sink attribute values [#79](https://github.com/izar/pytm/pull/79) +- Merge edges in DFD when `TM.mergeResponses` is True; allow marking `Dataflow` as responses [#76](https://github.com/izar/pytm/pull/76) +- Automatic ordering of dataflows when `TM.isOrdered` is True [#66](https://github.com/izar/pytm/pull/66) +- Loading a custom threats file by setting `TM.threatsFile` [#68](https://github.com/izar/pytm/pull/68) +- Setting properties on init [#67](https://github.com/izar/pytm/pull/67) +- Wrap long labels in DFDs [#65](https://github.com/izar/pytm/pull/65) + +## Bug fixes + +- Ensure all items have correct color, based on scope [#93](https://github.com/izar/pytm/pull/93) +- Add missing server isResilient property [#63](https://github.com/izar/pytm/issues/63) +- Advanced templates in repeat blocks [#81](https://github.com/izar/pytm/pull/81) +- Produce stable diagrams [#79](https://github.com/izar/pytm/pull/79) +- Allow overriding classes [#64](https://github.com/izar/pytm/pull/64) + +# 1.0.0 + +## New features + +- New threats [#61](https://github.com/izar/pytm/pull/61) + +## Bug fixes + +- UnicodeDecodeError: 'charmap' codec can't decode byte 0x9d [#57](https://github.com/izar/pytm/pull/57) +- `_uniq_name` missing 1 required positional argument [#60](https://github.com/izar/pytm/pull/60) +- Render objects with duplicate names [#45](https://github.com/izar/pytm/issues/45) + +# 0.8.1 + +## Bug fixes + +- Draw nested boundaries [#54](https://github.com/izar/pytm/pull/54), [#55](https://github.com/izar/pytm/pull/55) + +# 0.8.0 + +## New features + +- Draw nested boundaries [#52](https://github.com/izar/pytm/pull/52) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dc4abf70..50fb8392 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ Sometimes you will find the root cause yourself in the process. Try to give your issue a title that is succinct and specific. The devs will rename issues as needed to keep track of them. -To execute the test suite, from the root of the repo run `python3 -m unittest -v`. +To execute the test suite, from the root of the repo run `make test`. To control what tests to run, use `python3 -m unittest -v tests/`. ## PyTM-users diff --git a/Makefile b/Makefile index a7b0749e..1de848c4 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,10 @@ build: setup.py python3 setup.py sdist bdist_wheel twine upload $(DEPLOYURL) dist/* +.PHONY: test +test: + python3 -m unittest + .PHONY: describe describe: ./tm.py --describe "TM Element Boundary ExternalEntity Actor Lambda Server Process SetOfProcesses Datastore Dataflow" diff --git a/setup.py b/setup.py index 1ffb8f16..eeb46003 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name='pytm', - version='0.7', + version='1.1', packages=['pytm'], summary='A Python-based framework for threat modeling.', long_description=open('README.md').read(), @@ -13,15 +13,16 @@ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", - "Development Status :: 3 - Alpha", - "Environment :: Console", - "Intended Audience :: Developers", - "Topic :: Security", + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Topic :: Security", + "Natural Language :: English", ], python_requires='>=3', package_data={ - 'pytm': ['images/lambda.png', 'threatlib/threats.json'] - }, + 'pytm': ['images/lambda.png', 'threatlib/threats.json'], + }, exclude_package_data={'': ['report.html']}, - include_package_data=True + include_package_data=True, ) diff --git a/tests/test_private_func.py b/tests/test_private_func.py index 968a638f..6b8c0d71 100644 --- a/tests/test_private_func.py +++ b/tests/test_private_func.py @@ -1,12 +1,11 @@ -import sys -sys.path.append("..") -import unittest import random +import unittest -from pytm.pytm import Actor, Boundary, Dataflow, Datastore, Process, Server, TM, Threat +from pytm.pytm import TM, Actor, Boundary, Dataflow, Datastore, Process, Server, Threat class TestUniqueNames(unittest.TestCase): + def test_duplicate_boundary_names_have_different_unique_names(self): random.seed(0) object_1 = Boundary("foo") @@ -21,6 +20,7 @@ def test_duplicate_boundary_names_have_different_unique_names(self): class TestAttributes(unittest.TestCase): + def test_write_once(self): user = Actor("User") with self.assertRaises(ValueError): diff --git a/tests/test_pytmfunc.py b/tests/test_pytmfunc.py index be14ee8a..4cd4544b 100644 --- a/tests/test_pytmfunc.py +++ b/tests/test_pytmfunc.py @@ -1,18 +1,26 @@ -import sys -sys.path.append("..") - import json import os import random import re +import sys import unittest from contextlib import contextmanager -from os.path import dirname from io import StringIO +from os.path import dirname -from pytm import (TM, Action, Actor, Boundary, Dataflow, Datastore, ExternalEntity, - Lambda, Process, Server, Threat) - +from pytm import ( + TM, + Action, + Actor, + Boundary, + Dataflow, + Datastore, + ExternalEntity, + Lambda, + Process, + Server, + Threat, +) with open(os.path.abspath(os.path.join(dirname(__file__), '..')) + "/pytm/threatlib/threats.json", "r") as threat_file: threats = {t["SID"]: Threat(**t) for t in json.load(threat_file)} @@ -208,8 +216,8 @@ def test_INP01(self): lambda1.usesEnvironmentVariables = True lambda1.sanitizesInput = False lambda1.checksInputBounds = False - process1.usesEnvironmentVariables = True - process1.sanitizesInput = False + process1.usesEnvironmentVariables = True + process1.sanitizesInput = False process1.checksInputBounds = False threat = threats["INP01"] self.assertTrue(threat.apply(lambda1)) @@ -319,7 +327,7 @@ def test_DS01(self): def test_DE01(self): user = Actor("User") - web = Server("Web Server") + web = Server("Web Server") user_to_web = Dataflow(user, web, "User enters comments (*)") user_to_web.protocol = 'HTTP' user_to_web.isEncrypted = False @@ -445,19 +453,19 @@ def test_INP08(self): self.assertTrue(threat.apply(web)) def test_INP09(self): - web = Server("Web Server") + web = Server("Web Server") web.validatesInput = False threat = threats["INP09"] self.assertTrue(threat.apply(web)) def test_INP10(self): - web = Server("Web Server") + web = Server("Web Server") web.validatesInput = False threat = threats["INP10"] self.assertTrue(threat.apply(web)) def test_INP11(self): - web = Server("Web Server") + web = Server("Web Server") web.validatesInput = False web.sanitizesInput = False threat = threats["INP11"] @@ -476,18 +484,18 @@ def test_INP12(self): def test_AC04(self): user = Actor("User") - web = Server("Web Server") + web = Server("Web Server") user_to_web = Dataflow(user, web, "User enters comments (*)") - user_to_web.data = 'XML' + user_to_web.data = 'XML' user_to_web.authorizesSource = False threat = threats["AC04"] self.assertTrue(threat.apply(user_to_web)) def test_DO03(self): user = Actor("User") - web = Server("Web Server") + web = Server("Web Server") user_to_web = Dataflow(user, web, "User enters comments (*)") - user_to_web.data = 'XML' + user_to_web.data = 'XML' threat = threats["DO03"] self.assertTrue(threat.apply(user_to_web)) @@ -525,7 +533,7 @@ def test_INP14(self): def test_DE03(self): user = Actor("User") - web = Server("Web Server") + web = Server("Web Server") user_to_web = Dataflow(user, web, "User enters comments (*)") user_to_web.protocol = 'HTTP' user_to_web.isEncrypted = False @@ -638,7 +646,7 @@ def test_CR04(self): def test_DO04(self): user = Actor("User") - web = Server("Web Server") + web = Server("Web Server") user_to_web = Dataflow(user, web, "User enters comments (*)") user_to_web.data = 'XML' user_to_web.handlesResources = False @@ -722,7 +730,7 @@ def test_INP18(self): def test_CR06(self): user = Actor("User") - web = Server("Web Server") + web = Server("Web Server") user_to_web = Dataflow(user, web, "User enters comments (*)") user_to_web.protocol = 'HTTP' user_to_web.usesVPN = False @@ -741,7 +749,7 @@ def test_AC10(self): def test_CR07(self): user = Actor("User") - web = Server("Web Server") + web = Server("Web Server") user_to_web = Dataflow(user, web, "User enters comments (*)") user_to_web.protocol = 'HTTP' user_to_web.data = 'XML' @@ -758,7 +766,7 @@ def test_AA04(self): def test_CR08(self): user = Actor("User") - web = Server("Web Server") + web = Server("Web Server") user_to_web = Dataflow(user, web, "User enters comments (*)") user_to_web.protocol = 'HTTP' user_to_web.usesLatestTLSversion = False @@ -1045,7 +1053,3 @@ def test_AC21(self): process1.verifySessionIdentifiers = False threat = threats["AC21"] self.assertTrue(threat.apply(process1)) - - -if __name__ == '__main__': - unittest.main()