├── tests ├── __init__.py ├── run-unittests.sh ├── run-unittests.ps1 ├── seq_unused.plantuml ├── seq.plantuml ├── output.md ├── input.json ├── dfd_level1.txt ├── test_sql_dump.py ├── dfd_level0.txt ├── dfd.dot ├── dfd_colormap.dot ├── test_private_func.py └── output.json ├── requirements.txt ├── .github ├── CODEOWNERS └── workflows │ ├── codesee-arch-diagram.yml │ ├── main.yml │ ├── codeql-analysis.yml │ └── scorecard.yml ├── SUMMARY.md ├── requirements-dev.txt ├── sample.png ├── MANIFEST.in ├── docs ├── sample.png ├── threats.jq ├── sample.tm ├── basic_template.md ├── Stylesheet.css ├── reveal.md ├── advanced_template.md └── pytm │ └── report_util.html ├── .gitbook └── assets │ ├── dfd.png │ ├── seq.png │ └── sample.png ├── pytm ├── images │ ├── lambda.png │ ├── datastore.png │ ├── datastore_black.png │ ├── datastore_gold.png │ ├── datastore_darkgreen.png │ └── datastore_firebrick3.png ├── TODO.txt ├── report_util.py ├── __init__.py ├── flows.py ├── json.py └── template_engine.py ├── CONTRIBUTORS.md ├── SECURITY.md ├── devbox.json ├── pyproject.toml ├── ROADMAP.md ├── Dockerfile ├── setup.py ├── Makefile ├── .gitignore ├── LICENSE ├── CONTRIBUTING.md ├── tm.py ├── CHANGELOG.md ├── README.md └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pydal>=20200714.1 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @colesmj 2 | * @izar 3 | 4 | 5 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [pytm](README.md) 4 | 5 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | pdoc3 3 | black 4 | -------------------------------------------------------------------------------- /sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/pytm/HEAD/sample.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include images/lambda.png 2 | include threatlib/threats.json 3 | -------------------------------------------------------------------------------- /docs/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/pytm/HEAD/docs/sample.png -------------------------------------------------------------------------------- /.gitbook/assets/dfd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/pytm/HEAD/.gitbook/assets/dfd.png -------------------------------------------------------------------------------- /.gitbook/assets/seq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/pytm/HEAD/.gitbook/assets/seq.png -------------------------------------------------------------------------------- /pytm/images/lambda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/pytm/HEAD/pytm/images/lambda.png -------------------------------------------------------------------------------- /.gitbook/assets/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/pytm/HEAD/.gitbook/assets/sample.png -------------------------------------------------------------------------------- /pytm/images/datastore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/pytm/HEAD/pytm/images/datastore.png -------------------------------------------------------------------------------- /pytm/images/datastore_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/pytm/HEAD/pytm/images/datastore_black.png -------------------------------------------------------------------------------- /pytm/images/datastore_gold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/pytm/HEAD/pytm/images/datastore_gold.png -------------------------------------------------------------------------------- /pytm/images/datastore_darkgreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/pytm/HEAD/pytm/images/datastore_darkgreen.png -------------------------------------------------------------------------------- /pytm/images/datastore_firebrick3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/pytm/HEAD/pytm/images/datastore_firebrick3.png -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # PyTM Main Contributors 2 | 3 | * Was, Jan 4 | * Avhad, Pooja 5 | * Coles, Matthew 6 | * Ozmore, Nick 7 | * Shambhuni, Rohit 8 | * Tarandach, Izar 9 | 10 | Join us! 11 | -------------------------------------------------------------------------------- /tests/run-unittests.sh: -------------------------------------------------------------------------------- 1 | # Script to prepare the environment and run the test. Is invoked by run-unittests.ps1 2 | 3 | cd /pwd && \ 4 | pip install -r requirements-dev.txt && \ 5 | pip install -r requirements.txt && \ 6 | python3 -m unittest -v tests/test_*.py -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Reporting a Vulnerability 2 | Please report (suspected) security vulnerabilities as a project issue. You will receive a response from us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days. 3 | 4 | 5 | -------------------------------------------------------------------------------- /pytm/TODO.txt: -------------------------------------------------------------------------------- 1 | TODO 2 | ==== 3 | 4 | * mitigations - create a mitigation class where Mitigation matches Finding for Element 5 | * add threats and verify that eval of bool expression matches all cases. if not, create a variation where a function can be provided instead 6 | * documentation with Sphinx preparing for Read The Docs (?) 7 | -------------------------------------------------------------------------------- /tests/run-unittests.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [ValidateSet('always', 'never')] 4 | [string] $pull = 'always' 5 | ) 6 | 7 | # Run all tests using docker and a read-only file system so the docker image cannot impact the local files. 8 | 9 | $rootFolder = Split-Path $PSScriptRoot 10 | docker run --pull $pull --rm -v "${rootFolder}:/pwd:ro" python bash /pwd/tests/run-unittests.sh -------------------------------------------------------------------------------- /devbox.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.14.2/.schema/devbox.schema.json", 3 | "packages": [ 4 | "pandoc@latest", 5 | "graphviz@latest", 6 | "openjdk@latest", 7 | "python@3.11.13", 8 | "poetry@latest" 9 | ], 10 | "shell": { 11 | "init_hook": ["poetry install"], 12 | "scripts": { 13 | "test": "poetry run pytest" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/threats.jq: -------------------------------------------------------------------------------- 1 | ## \(.SID) \(.description) 2 | 3 | \(.details) 4 | 5 |
6 |
Severity
7 |
\(.severity)
8 | 9 |
Prerequisites
10 |
\(.prerequisites)
11 | 12 |
Example
13 |
\(.example)
14 | 15 |
Mitigations
16 |
\(.mitigations)
17 | 18 |
References
19 |
\(.references)
20 | 21 |
Condition
22 |
\(.condition)
23 |
24 | \n\n 25 | -------------------------------------------------------------------------------- /docs/sample.tm: -------------------------------------------------------------------------------- 1 | /* threats = 2 | Finding: Dataflow not authenticated on web and db with score 8.6 3 | */ 4 | diagram { 5 | boundary Web_Side { 6 | title = "Web Side" 7 | function web_server { 8 | title = "web server" 9 | } 10 | } 11 | boundary DB_side { 12 | title = "DB side" 13 | database database_server { 14 | title = "database server" 15 | } 16 | } 17 | web_server -> database_server { 18 | operation = "web and db" 19 | data = "HTTP" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pytm" 3 | version = "1.3.1" 4 | description = "A Pythonic framework for threat modeling" 5 | authors = ["pytm Team"] 6 | license = "MIT License" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.9 || ^3.10 || ^3.11" 10 | pydal = "~20200714.1" 11 | legacy-cgi = { version = "^2.0", markers = "python_version >= '3.13'" } 12 | 13 | [tool.poetry.group.dev.dependencies] 14 | pytest = "^8.3.5" 15 | black = "^25.9.0" 16 | pdoc3 = "^0.11.6" 17 | 18 | [build-system] 19 | requires = ["poetry-core>=1.0.0"] 20 | build-backend = "poetry.core.masonry.api" 21 | -------------------------------------------------------------------------------- /tests/seq_unused.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | actor actor_User_579e9aae81 as "User" 3 | database datastore_SQLDatabase_d2006ce1bb as "SQL Database" 4 | entity server_WebServer_f2eb7a3ff7 as "Web Server" 5 | 6 | actor_User_579e9aae81 -> server_WebServer_f2eb7a3ff7: User enters comments (*) 7 | note left 8 | bbb 9 | end note 10 | server_WebServer_f2eb7a3ff7 -> datastore_SQLDatabase_d2006ce1bb: Insert query with comments 11 | note left 12 | ccc 13 | end note 14 | datastore_SQLDatabase_d2006ce1bb -> server_WebServer_f2eb7a3ff7: Retrieve comments 15 | server_WebServer_f2eb7a3ff7 -> actor_User_579e9aae81: Show comments (*) 16 | @enduml 17 | -------------------------------------------------------------------------------- /.github/workflows/codesee-arch-diagram.yml: -------------------------------------------------------------------------------- 1 | # This workflow was added by CodeSee. Learn more at https://codesee.io/ 2 | # This is v2.0 of this workflow file 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request_target: 8 | types: [opened, synchronize, reopened] 9 | 10 | name: CodeSee 11 | 12 | permissions: read-all 13 | 14 | jobs: 15 | codesee: 16 | runs-on: ubuntu-latest 17 | continue-on-error: true 18 | name: Analyze the repo with CodeSee 19 | steps: 20 | - uses: Codesee-io/codesee-action@v2 21 | with: 22 | codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} 23 | -------------------------------------------------------------------------------- /tests/seq.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | actor actor_User_579e9aae81 as "User" 3 | entity server_WebServer_f2eb7a3ff7 as "Web Server" 4 | database datastore_SQLDatabase_d2006ce1bb as "SQL Database" 5 | 6 | actor_User_579e9aae81 -> server_WebServer_f2eb7a3ff7: (1) User enters comments (*) 7 | note left 8 | bbb 9 | end note 10 | server_WebServer_f2eb7a3ff7 -> datastore_SQLDatabase_d2006ce1bb: (2) Insert query with comments 11 | note left 12 | ccc 13 | end note 14 | datastore_SQLDatabase_d2006ce1bb -> server_WebServer_f2eb7a3ff7: (3) Retrieve comments 15 | server_WebServer_f2eb7a3ff7 -> actor_User_579e9aae81: (4) Show comments (*) 16 | @enduml 17 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # To the end of 2021 2 | 3 | * add more threat rules 4 | * add debugging capability to threat rules 5 | * merge/close existing PRs 6 | 7 | # 1H2022 8 | 9 | * add more rules 10 | * move to a more complete rule evaluation engine 11 | * export/import other popular TM tools data formats 12 | * lower barrier of entry by adding a new, natural way of describing systems 13 | 14 | # 2H2022 15 | 16 | * total world domination via threat modeling 17 | 18 | # 2H2025 19 | 20 | * world domination was not reached - but we got a nice slice of it 21 | * integration of TM-BOM format 22 | * additional diagramming formats 23 | * incorporation of AI capabilities 24 | * prioritization of findings 25 | * creation of threat scenarios 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/output.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## System Description 4 |   5 | 6 | aaa 7 | 8 |   9 | 10 | 11 | 12 | 13 | ## Dataflow Diagram - Level 0 DFD 14 | 15 | ![](sample.png) 16 | 17 |   18 | 19 | ## Dataflows 20 |   21 | 22 | Name|From|To |Data|Protocol|Port 23 | |:----:|:----:|:---:|:----:|:--------:|:----:| 24 | |User enters comments (*)|User|Web Server|auth cookie||-1| 25 | |Insert query with comments|Web Server|SQL Database|[]||-1| 26 | |Call func|Web Server|Lambda func|[]||-1| 27 | |Retrieve comments|SQL Database|Web Server|[]||-1| 28 | |Show comments (*)|Web Server|User|[]||-1| 29 | |Query for tasks|Task queue worker|SQL Database|[]||-1| 30 | 31 | 32 | ## Data Dictionary 33 |   34 | 35 | Name|Description|Classification 36 | |:----:|:--------:|:----:| 37 | |auth cookie|auth cookie description|PUBLIC| 38 | 39 | 40 |   41 | 42 | ## Potential Threats 43 | 44 |   45 |   46 | 47 | || 48 | -------------------------------------------------------------------------------- /tests/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my test tm", 3 | "description": "aaa", 4 | "isOrdered": true, 5 | "onDuplicates": "IGNORE", 6 | "boundaries": [ 7 | { 8 | "name": "Internet" 9 | }, 10 | { 11 | "name": "Server/DB" 12 | } 13 | ], 14 | "elements": [ 15 | { 16 | "__class__": "Actor", 17 | "name": "User", 18 | "inBoundary": "Internet" 19 | }, 20 | { 21 | "__class__": "Server", 22 | "name": "Web Server" 23 | }, 24 | { 25 | "__class__": "Datastore", 26 | "name": "SQL Database", 27 | "inBoundary": "Server/DB" 28 | } 29 | ], 30 | "flows": [ 31 | { 32 | "name": "Request", 33 | "source": "User", 34 | "sink": "Web Server", 35 | "note": "bbb" 36 | }, 37 | { 38 | "name": "Insert", 39 | "source": "Web Server", 40 | "sink": "SQL Database", 41 | "note": "ccc" 42 | }, 43 | { 44 | "name": "Select", 45 | "source": "SQL Database", 46 | "sink": "Web Server" 47 | }, 48 | { 49 | "name": "Response", 50 | "source": "Web Server", 51 | "sink": "User" 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM python:3.12-rc-alpine 3 | 4 | 5 | WORKDIR /usr/src/app 6 | ENTRYPOINT ["sh"] 7 | 8 | ENV PLANTUML_VER 1.2021.7 9 | ENV PLANTUML_PATH /usr/local/lib/plantuml.jar 10 | ENV PANDOC_VER 2.14.0.1 11 | 12 | RUN apk add --no-cache graphviz openjdk11-jre fontconfig make curl ttf-liberation ttf-linux-libertine ttf-dejavu \ 13 | && apk add --no-cache --virtual .build-deps gcc musl-dev \ 14 | && rm -rf /var/cache/apk/* \ 15 | && curl -LO https://master.dl.sourceforge.net/project/plantuml/$PLANTUML_VER/plantuml.$PLANTUML_VER.jar \ 16 | && mv plantuml.$PLANTUML_VER.jar $PLANTUML_PATH \ 17 | && curl -LO https://github.com/jgm/pandoc/releases/download/$PANDOC_VER/pandoc-$PANDOC_VER-linux-amd64.tar.gz \ 18 | && tar xvzf pandoc-$PANDOC_VER-linux-amd64.tar.gz --strip-components 1 -C /usr/local/ 19 | 20 | ENV _JAVA_OPTIONS -Duser.home=/tmp -Dawt.useSystemAAFontSettings=gasp 21 | RUN printf '@startuml\n@enduml' | java -Djava.awt.headless=true -jar $PLANTUML_PATH -tpng -pipe >/dev/null 22 | 23 | COPY requirements.txt requirements-dev.txt ./ 24 | RUN pip install --no-cache-dir -r requirements-dev.txt \ 25 | && apk del .build-deps 26 | 27 | COPY pytm ./pytm 28 | COPY docs ./docs 29 | COPY *.py Makefile ./ 30 | -------------------------------------------------------------------------------- /tests/dfd_level1.txt: -------------------------------------------------------------------------------- 1 | digraph tm { 2 | graph [ 3 | fontname = Arial; 4 | fontsize = 14; 5 | ] 6 | node [ 7 | fontname = Arial; 8 | fontsize = 14; 9 | rankdir = lr; 10 | ] 11 | edge [ 12 | shape = none; 13 | arrowtail = onormal; 14 | fontname = Arial; 15 | fontsize = 12; 16 | ] 17 | labelloc = "t"; 18 | fontsize = 20; 19 | nodesep = 1; 20 | 21 | subgraph cluster_boundary_Internet_acf3059e70 { 22 | graph [ 23 | fontsize = 10; 24 | fontcolor = black; 25 | style = dashed; 26 | color = firebrick2; 27 | label = <Internet>; 28 | ] 29 | 30 | actor_User_579e9aae81 [ 31 | shape = square; 32 | color = black; 33 | fontcolor = black; 34 | label = "User"; 35 | margin = 0.02; 36 | ] 37 | 38 | } 39 | 40 | subgraph cluster_boundary_ServerDB_88f2d9c06f { 41 | graph [ 42 | fontsize = 10; 43 | fontcolor = black; 44 | style = dashed; 45 | color = firebrick2; 46 | label = <Server/DB>; 47 | ] 48 | 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: build+test 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | permissions: 14 | contents: read 15 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 16 | jobs: 17 | # This workflow contains a single job called "build" 18 | build: 19 | # The type of runner that the job will run on 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | matrix: 24 | python-version: ["3.9", "3.10", "3.11"] 25 | 26 | # Steps represent a sequence of tasks that will be executed as part of the job 27 | steps: 28 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 29 | - uses: actions/checkout@v4 30 | - name: Set up Python ${{ matrix.python-version }} 31 | uses: actions/setup-python@v6 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | - name: Install Poetry 35 | run: pip install poetry 36 | - name: Install dependencies 37 | run: poetry install --with dev 38 | - name: Run tests 39 | run: poetry run pytest 40 | -------------------------------------------------------------------------------- /docs/basic_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## System Description 4 |   5 | 6 | {tm.description} 7 | 8 |   9 | 10 | {tm.assumptions:if: 11 | 12 | |Assumptions| 13 | |-----------| 14 | {tm.assumptions:repeat:|{{item}}| 15 | } 16 | 17 |   18 |   19 |   20 | } 21 | 22 | 23 | ## Dataflow Diagram - Level 0 DFD 24 | 25 | ![](sample.png) 26 | 27 |   28 | 29 | ## Dataflows 30 |   31 | 32 | Name|From|To |Data|Protocol|Port 33 | |:----:|:----:|:---:|:----:|:--------:|:----:| 34 | {dataflows:repeat:|{{item.name}}|{{item.source.name}}|{{item.sink.name}}|{{item.data}}|{{item.protocol}}|{{item.dstPort}}| 35 | } 36 | 37 | ## Data Dictionary 38 |   39 | 40 | Name|Description|Classification 41 | |:----:|:--------:|:----:| 42 | {data:repeat:|{{item.name}}|{{item.description}}|{{item.classification.name}}| 43 | } 44 | 45 |   46 | 47 | ## Potential Threats 48 | 49 |   50 |   51 | 52 | |{findings:repeat: 53 |
54 | {{item.threat_id}} -- {{item.description}} 55 |
Targeted Element
56 |

{{item.target}}

57 |
Severity
58 |

{{item.severity}}

59 |
Example Instances
60 |

{{item.example}}

61 |
Mitigations
62 |

{{item.mitigations}}

63 |
References
64 |

{{item.references}}

65 |   66 |   67 |   68 |
69 | }| 70 | -------------------------------------------------------------------------------- /pytm/report_util.py: -------------------------------------------------------------------------------- 1 | 2 | class ReportUtils: 3 | @staticmethod 4 | def getParentName(element): 5 | from pytm import Boundary 6 | if (isinstance(element, Boundary)): 7 | parent = element.inBoundary 8 | if (parent is not None): 9 | return parent.name 10 | else: 11 | return str("") 12 | else: 13 | return "ERROR: getParentName method is not valid for " + element.__class__.__name__ 14 | 15 | 16 | @staticmethod 17 | def getNamesOfParents(element): 18 | from pytm import Boundary 19 | if (isinstance(element, Boundary)): 20 | parents = [p.name for p in element.parents()] 21 | return parents 22 | else: 23 | return "ERROR: getNamesOfParents method is not valid for " + element.__class__.__name__ 24 | 25 | @staticmethod 26 | def getFindingCount(element): 27 | from pytm import Element 28 | if (isinstance(element, Element)): 29 | return str(len(list(element.findings))) 30 | else: 31 | return "ERROR: getFindingCount method is not valid for " + element.__class__.__name__ 32 | 33 | @staticmethod 34 | def getElementType(element): 35 | from pytm import Element 36 | if (isinstance(element, Element)): 37 | return str(element.__class__.__name__) 38 | else: 39 | return "ERROR: getElementType method is not valid for " + element.__class__.__name__ 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8") as f: 4 | long_description = f.read() 5 | 6 | setuptools.setup( 7 | name="pytm", 8 | version="1.3.1", 9 | packages=["pytm"], 10 | description="A Python-based framework for threat modeling.", 11 | long_description=long_description, 12 | long_description_content_type="text/markdown", 13 | author="pytm team", 14 | author_email="please_use_github_issues@nowhere.com", 15 | license="MIT License", 16 | url="https://github.com/izar/pytm", 17 | classifiers=[ 18 | "Programming Language :: Python :: 3", 19 | "License :: OSI Approved :: MIT License", 20 | "Operating System :: OS Independent", 21 | "Development Status :: 5 - Production/Stable", 22 | "Environment :: Console", 23 | "Intended Audience :: Developers", 24 | "Topic :: Security", 25 | "Natural Language :: English", 26 | ], 27 | python_requires=">=3", 28 | install_requires=["pydal>=20200714.1"], 29 | package_data={ 30 | "pytm": [ 31 | "images/datastore.png", 32 | "images/lambda.png", 33 | "images/datastore_black.png", 34 | "images/datastore_darkgreen.png", 35 | "images/datastore_firebrick3.png", 36 | "images/datastore_gold.png", 37 | "threatlib/threats.json", 38 | ], 39 | }, 40 | exclude_package_data={"": ["report.html"]}, 41 | include_package_data=True, 42 | ) 43 | -------------------------------------------------------------------------------- /pytm/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "Action", 3 | "Actor", 4 | "Assumption", 5 | "Boundary", 6 | "Classification", 7 | "TLSVersion", 8 | "Data", 9 | "Dataflow", 10 | "Datastore", 11 | "DatastoreType", 12 | "Element", 13 | "ExternalEntity", 14 | "Finding", 15 | "Lambda", 16 | "Lifetime", 17 | "load", 18 | "loads", 19 | "Process", 20 | "Server", 21 | "SetOfProcesses", 22 | "Threat", 23 | "TM", 24 | ] 25 | 26 | import sys 27 | 28 | from .json import load, loads 29 | from .pytm import ( 30 | TM, 31 | Action, 32 | Actor, 33 | Assumption, 34 | Boundary, 35 | Classification, 36 | Data, 37 | Dataflow, 38 | Datastore, 39 | DatastoreType, 40 | Element, 41 | ExternalEntity, 42 | Finding, 43 | Lambda, 44 | Lifetime, 45 | Process, 46 | Server, 47 | SetOfProcesses, 48 | Threat, 49 | TLSVersion, 50 | var, 51 | ) 52 | 53 | 54 | def pdoc_overrides(): 55 | result = {"pytm": False, "json": False, "template_engine": False} 56 | mod = sys.modules[__name__] 57 | for name, klass in mod.__dict__.items(): 58 | if not isinstance(klass, type): 59 | continue 60 | for i in dir(klass): 61 | if i in ("check", "dfd", "seq"): 62 | result[f"{name}.{i}"] = False 63 | attr = getattr(klass, i, {}) 64 | if isinstance(attr, var) and attr.doc != "": 65 | result[f"{name}.{i}"] = attr.doc 66 | return result 67 | 68 | 69 | __pdoc__ = pdoc_overrides() 70 | -------------------------------------------------------------------------------- /docs/Stylesheet.css: -------------------------------------------------------------------------------- 1 | * { margin: 0; padding: 0; } 2 | html { 3 | padding: 0; 4 | font: normal 15px/1.25 Source Sans Pro, sans-serif; 5 | color: #000; 6 | hyphens: auto; 7 | word-wrap: break-word; 8 | background: #fff; 9 | margin-left: 1rem; 10 | } 11 | 12 | body > :first-child { 13 | margin-top: 1; 14 | } 15 | 16 | h1,h2,h3,h4,h5,h6 { 17 | line-height: 1; 18 | margin: 1rem; 19 | margin-top: 1.5rem; 20 | text-rendering: optimizeLegibility; 21 | } 22 | 23 | h1 { font-size: 2.15rem; } 24 | h2 { font-size: 2rem; } 25 | h3 { font-size: 1.65rem; } 26 | h4 { font-size: 1.25rem; } 27 | h5 { font-size: 1.1rem; } 28 | h6 { font-size: 1rem; } 29 | 30 | h2 em, h3 em{ 31 | color:grey; 32 | } 33 | 34 | /* @end */ 35 | 36 | p { 37 | margin-top: .75rem; 38 | margin-left: 1rem; 39 | } 40 | 41 | hr { 42 | margin: .75rem 0; 43 | opacity: .5; 44 | } 45 | table { 46 | margin: .75rem 0 0 1rem; 47 | padding: 0; 48 | width: 50%; 49 | text-align: left; 50 | white-space: nowrap; 51 | border-collapse: collapse; 52 | } 53 | table tr { 54 | margin: 0; 55 | padding: 0; 56 | width: auto; 57 | text-align: left; 58 | border-top: 1px solid #ccc; 59 | background-color: #fff; 60 | } 61 | table tr:nth-child(2n) { 62 | background-color: #f8f8f8; 63 | } 64 | 65 | table tr th { 66 | margin: 0; 67 | padding: .35em .75em; 68 | font-weight: bold; 69 | text-align: left; 70 | border: 1px solid #ccc; 71 | } 72 | 73 | table tr td { 74 | margin: 0; 75 | padding: .35em .75em; 76 | text-align: left; 77 | border: 1px solid #ccc; 78 | } 79 | 80 | details { 81 | margin-left: 2rem 82 | } 83 | /* @end */ 84 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MKFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) 2 | CWD := $(patsubst %/,%,$(dir $(MKFILE_PATH))) 3 | DOCKER_IMG := pytm 4 | 5 | ifeq ($(USE_DOCKER),true) 6 | SHELL=docker 7 | .SHELLFLAGS=run -u $$(id -u) -v $(CWD):/usr/src/app --rm $(DOCKER_IMG):latest -c 8 | endif 9 | ifndef PLANTUML_PATH 10 | export PLANTUML_PATH = ./plantuml.jar 11 | endif 12 | 13 | MODEL?=tm 14 | 15 | libs := $(wildcard pytm/*.py) $(wildcard pytm/threatlib/*.json) $(wildcard pytm/images/*) 16 | 17 | all: clean docs/pytm/index.html $(MODEL) 18 | 19 | safe_filename: 20 | ifeq ($(suffix $(MODEL)), .py) 21 | @echo "I think you mean MODEL=$(patsubst .py,,$(MODEL))" 22 | exit 1 23 | endif 24 | 25 | 26 | docs/pytm/index.html: $(wildcard pytm/*.py) 27 | PYTHONPATH=. pdoc --html --force --output-dir docs pytm 28 | 29 | docs/threats.md: $(wildcard pytm/threatlib/*.json) 30 | printf "# Threat database\n" > $@ 31 | jq -r ".[] | \"$$(cat docs/threats.jq)\"" $< >> $@ 32 | 33 | clean: safe_filename 34 | rm -rf dist/* build/* $(MODEL) 35 | 36 | $(MODEL): safe_filename 37 | mkdir -p $(MODEL) 38 | $(MAKE) MODEL=$(MODEL) report 39 | 40 | $(MODEL)/dfd.png: $(MODEL).py $(libs) 41 | ./$< --dfd | dot -Tpng -o $@ 42 | 43 | $(MODEL)/seq.png: $(MODEL).py $(libs) 44 | ./$< --seq | java -Djava.awt.headless=true -jar $$PLANTUML_PATH -tpng -pipe > $@ 45 | 46 | $(MODEL)/report.html: $(MODEL).py $(libs) docs/basic_template.md docs/Stylesheet.css 47 | ./$< --report docs/basic_template.md | pandoc -f markdown -t html > $@ 48 | 49 | dfd: $(MODEL)/dfd.png 50 | 51 | seq: $(MODEL)/seq.png 52 | 53 | report: $(MODEL)/report.html seq dfd 54 | 55 | .PHONY: test 56 | test: 57 | @python3 -m unittest 58 | 59 | .PHONY: describe 60 | describe: 61 | ./tm.py --describe "TM Element Boundary ExternalEntity Actor Lambda Server Process SetOfProcesses Datastore Dataflow" 62 | 63 | .PHONY: image 64 | image: 65 | docker build -t $(DOCKER_IMG) . 66 | 67 | .PHONY: docs 68 | docs: docs/pytm/index.html docs/threats.md 69 | 70 | .PHONY: fmt 71 | fmt: 72 | black $(wildcard pytm/*.py) $(wildcard tests/*.py) $(wildcard *.py) 73 | -------------------------------------------------------------------------------- /pytm/flows.py: -------------------------------------------------------------------------------- 1 | from pytm import Dataflow as DF 2 | from pytm import Element 3 | 4 | 5 | def req_reply(src: Element, dest: Element, req_name: str, reply_name=None) -> (DF, DF): 6 | ''' 7 | This function creates two datflows where one dataflow is a request 8 | and the second dataflow is the corresponding reply to the newly created request. 9 | 10 | Args: 11 | req_name: name of the request dataflow 12 | reply_name: name of the reply datadlow 13 | if not set the name will be "Reply to " 14 | 15 | Usage: 16 | query_titles, reply_titles = req_reply(api, database, 'Query book titles') 17 | 18 | view_authors, reply_authors = req_reply(api, database, 19 | req_name='Query authors view', 20 | reply_name='Authors, with top titles') 21 | 22 | Returns: 23 | a tuple of two dataflows, where the first is the request and the second is the reply. 24 | 25 | ''' 26 | if not reply_name: 27 | reply_name = f'Reply to {req_name}' 28 | req = DF(src, dest, req_name) 29 | reply = DF(dest, src, name=reply_name) 30 | reply.responseTo = req 31 | return req, reply 32 | 33 | 34 | def reply(req: DF, **kwargs) -> DF: 35 | ''' 36 | This function takes a dataflow as an argument and returns a new dataflow, which is a response to the given dataflow. 37 | 38 | Args: 39 | req: a dataflow for which a reply should be generated 40 | kwargs: key word arguments for the newly created reply 41 | Usage: 42 | client_query = Dataflow(client, api, "Get authors page") 43 | api_query = Dataflow(api, database, 'Get authors') 44 | api_reply = reply(api_query) 45 | client_reply = reply(client_query) 46 | Returns: 47 | a Dataflow which is a reply to the given datadlow req 48 | ''' 49 | if 'name' not in kwargs: 50 | name = f'Reply to {req.name}' 51 | else: 52 | name = kwargs['name'] 53 | del kwargs['name'] 54 | reply = DF(req.sink, req.source, name, **kwargs) 55 | reply.responseTo = req 56 | return req, reply 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pyre/ 2 | bin/ 3 | include/ 4 | pip-selfcheck.json 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | .vscode/settings.json 111 | 112 | pytm.code-workspace 113 | tm.df 114 | tm.png 115 | pytm-workspace.code-workspace 116 | .gitignore 117 | tm_example.dot 118 | .work/ 119 | 120 | #IntelliJ/PyCharm 121 | .idea 122 | *.iml 123 | 124 | #Others 125 | plantuml.jar 126 | tm/ 127 | /sqldump 128 | /tests/.config.pytm 129 | 130 | # devbox 131 | devbox.lock 132 | 133 | # zed 134 | .zed 135 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Main Project Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | ======================================================== 24 | 25 | PyTM uses material from CAPEC on its threat catalog. CAPEC has its own license, reproduced below: 26 | 27 | LICENSE 28 | The MITRE Corporation (MITRE) hereby grants you a non-exclusive, royalty-free license to use Common Attack Pattern Enumeration and Classification (CAPEC™) for research, development, and commercial purposes. Any copy you make for such purposes is authorized provided that you reproduce MITRE’s copyright designation and this license in any such copy. 29 | 30 | DISCLAIMERS 31 | ALL DOCUMENTS AND THE INFORMATION CONTAINED THEREIN ARE PROVIDED ON AN "AS IS" BASIS AND THE CONTRIBUTOR, THE ORGANIZATION HE/SHE REPRESENTS OR IS SPONSORED BY (IF ANY), THE MITRE CORPORATION, ITS BOARD OF TRUSTEES, OFFICERS, AGENTS, AND EMPLOYEES, DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION THEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. 32 | ========================================================= 33 | 34 | -------------------------------------------------------------------------------- /tests/test_sql_dump.py: -------------------------------------------------------------------------------- 1 | import random 2 | import sqlite3 3 | from pathlib import Path 4 | 5 | import pytest 6 | 7 | from pytm import Boundary, Server, Threat, TM 8 | 9 | 10 | @pytest.fixture 11 | def sample_tm(): 12 | TM.reset() 13 | random.seed(0) 14 | tm = TM("sql dump tm", description="desc") 15 | 16 | internet = Boundary("Internet") 17 | server_db = Boundary("Server/DB", inBoundary=internet) 18 | Server("Web Server", inBoundary=server_db) 19 | 20 | TM._threats = [ 21 | Threat( 22 | SID="SRV001", 23 | description="Server threat", 24 | severity="High", 25 | target="Server", 26 | ) 27 | ] 28 | 29 | tm.resolve() 30 | assert tm.findings, "Expected at least one finding for sqlDump tests" 31 | return tm 32 | 33 | 34 | def _open_connection(tmp_path: Path) -> sqlite3.Connection: 35 | db_path = tmp_path / "sqldump" / "test.db" 36 | return sqlite3.connect(db_path) 37 | 38 | 39 | def test_sql_dump_creates_serialized_columns(sample_tm, tmp_path, monkeypatch): 40 | monkeypatch.chdir(tmp_path) 41 | 42 | sample_tm.sqlDump("test.db") 43 | 44 | with _open_connection(tmp_path) as conn: 45 | column_names = { 46 | column_info[1].lower() 47 | for column_info in conn.execute("PRAGMA table_info(Boundary)") 48 | } 49 | 50 | assert {"name", "inscope", "inboundary"}.issubset(column_names) 51 | 52 | 53 | def test_sql_dump_persists_element_and_finding_data(sample_tm, tmp_path, monkeypatch): 54 | monkeypatch.chdir(tmp_path) 55 | 56 | sample_tm.sqlDump("test.db") 57 | 58 | with _open_connection(tmp_path) as conn: 59 | boundary_rows = conn.execute( 60 | "SELECT name, inBoundary FROM Boundary ORDER BY id" 61 | ).fetchall() 62 | server_rows = conn.execute( 63 | "SELECT name, inBoundary FROM Server ORDER BY id" 64 | ).fetchall() 65 | finding_rows = conn.execute( 66 | "SELECT threat_id FROM Finding ORDER BY id" 67 | ).fetchall() 68 | 69 | assert ("Internet", None) in boundary_rows 70 | assert ("Server/DB", "Internet") in boundary_rows 71 | assert ("Web Server", "Server/DB") in server_rows 72 | assert [row[0] for row in finding_rows] == ["SRV001"] -------------------------------------------------------------------------------- /tests/dfd_level0.txt: -------------------------------------------------------------------------------- 1 | digraph tm { 2 | graph [ 3 | fontname = Arial; 4 | fontsize = 14; 5 | ] 6 | node [ 7 | fontname = Arial; 8 | fontsize = 14; 9 | rankdir = lr; 10 | ] 11 | edge [ 12 | shape = none; 13 | arrowtail = onormal; 14 | fontname = Arial; 15 | fontsize = 12; 16 | ] 17 | labelloc = "t"; 18 | fontsize = 20; 19 | nodesep = 1; 20 | 21 | subgraph cluster_boundary_Internet_acf3059e70 { 22 | graph [ 23 | fontsize = 10; 24 | fontcolor = black; 25 | style = dashed; 26 | color = firebrick2; 27 | label = <Internet>; 28 | ] 29 | 30 | actor_User_579e9aae81 [ 31 | shape = square; 32 | color = black; 33 | fontcolor = black; 34 | label = "User"; 35 | margin = 0.02; 36 | ] 37 | 38 | } 39 | 40 | subgraph cluster_boundary_ServerDB_88f2d9c06f { 41 | graph [ 42 | fontsize = 10; 43 | fontcolor = black; 44 | style = dashed; 45 | color = firebrick2; 46 | label = <Server/DB>; 47 | ] 48 | 49 | datastore_SQLDatabase_d2006ce1bb [ 50 | shape = none; 51 | fixedsize = shape; 52 | image = "INSTALL_PATH/pytm/images/datastore_black.png"; 53 | imagescale = true; 54 | color = black; 55 | fontcolor = black; 56 | xlabel = "SQL Database"; 57 | label = ""; 58 | ] 59 | 60 | } 61 | 62 | server_WebServer_f2eb7a3ff7 [ 63 | shape = circle; 64 | color = black; 65 | fontcolor = black; 66 | label = "Web Server"; 67 | margin = 0.02; 68 | ] 69 | 70 | actor_User_579e9aae81 -> server_WebServer_f2eb7a3ff7 [ 71 | color = black; 72 | fontcolor = black; 73 | dir = forward; 74 | label = "User enters\ncomments (*)"; 75 | ] 76 | 77 | server_WebServer_f2eb7a3ff7 -> datastore_SQLDatabase_d2006ce1bb [ 78 | color = black; 79 | fontcolor = black; 80 | dir = forward; 81 | label = "Insert query with\ncomments"; 82 | ] 83 | 84 | datastore_SQLDatabase_d2006ce1bb -> server_WebServer_f2eb7a3ff7 [ 85 | color = black; 86 | fontcolor = black; 87 | dir = forward; 88 | label = "Retrieve comments"; 89 | ] 90 | 91 | server_WebServer_f2eb7a3ff7 -> actor_User_579e9aae81 [ 92 | color = black; 93 | fontcolor = black; 94 | dir = forward; 95 | label = "Show comments (*)"; 96 | ] 97 | 98 | } 99 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 15 * * 3' 16 | 17 | permissions: # added using https://github.com/step-security/secure-workflows 18 | contents: read 19 | 20 | jobs: 21 | analyze: 22 | permissions: 23 | actions: read # for github/codeql-action/init to get workflow details 24 | contents: read # for actions/checkout to fetch code 25 | security-events: write # for github/codeql-action/autobuild to send a status report 26 | name: Analyze 27 | runs-on: ubuntu-latest 28 | 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | # Override automatic language detection by changing the below list 33 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 34 | language: ['python'] 35 | # Learn more... 36 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 37 | 38 | steps: 39 | - name: Checkout repository 40 | uses: actions/checkout@v2 41 | with: 42 | # We must fetch at least the immediate parents so that if this is 43 | # a pull request then we can checkout the head. 44 | fetch-depth: 2 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@v1 49 | with: 50 | languages: ${{ matrix.language }} 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v1 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 https://git.io/JvXDl 63 | 64 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 65 | # and modify them (or add more) to build your code if your project 66 | # uses a compiled language 67 | 68 | #- run: | 69 | # make bootstrap 70 | # make release 71 | 72 | - name: Perform CodeQL Analysis 73 | uses: github/codeql-action/analyze@v1 74 | 75 | 76 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '18 20 * * 3' 14 | push: 15 | branches: [ "master" ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | # Uncomment the permissions below if installing in a private repository. 30 | # contents: read 31 | # actions: read 32 | 33 | steps: 34 | - name: "Checkout code" 35 | uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 36 | with: 37 | persist-credentials: false 38 | 39 | - name: "Run analysis" 40 | uses: ossf/scorecard-action@v2.3.1 41 | with: 42 | results_file: results.sarif 43 | results_format: sarif 44 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 45 | # - you want to enable the Branch-Protection check on a *public* repository, or 46 | # - you are installing Scorecard on a *private* repository 47 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 48 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 49 | 50 | # Public repositories: 51 | # - Publish results to OpenSSF REST API for easy access by consumers 52 | # - Allows the repository to include the Scorecard badge. 53 | # - See https://github.com/ossf/scorecard-action#publishing-results. 54 | # For private repositories: 55 | # - `publish_results` will always be set to `false`, regardless 56 | # of the value entered here. 57 | publish_results: true 58 | 59 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 60 | # format to the repository Actions tab. 61 | - name: "Upload artifact" 62 | uses: actions/upload-artifact@v4 63 | with: 64 | name: SARIF file 65 | path: results.sarif 66 | retention-days: 5 67 | 68 | # Upload the results to GitHub's code scanning dashboard. 69 | - name: "Upload to code-scanning" 70 | uses: github/codeql-action/upload-sarif@807578363a7869ca324a79039e6db9c843e0e100 # v2.1.27 71 | with: 72 | sarif_file: results.sarif 73 | -------------------------------------------------------------------------------- /pytm/json.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | from .pytm import ( 5 | TM, 6 | Boundary, 7 | Element, 8 | Dataflow, 9 | Server, 10 | ExternalEntity, 11 | Datastore, 12 | Actor, 13 | Process, 14 | SetOfProcesses, 15 | Action, 16 | Lambda, 17 | Controls, 18 | ) 19 | 20 | 21 | def loads(s): 22 | """Load a TM object from a JSON string *s*.""" 23 | result = json.loads(s, object_hook=decode) 24 | if not isinstance(result, TM): 25 | raise ValueError("Failed to decode JSON input as TM") 26 | return result 27 | 28 | 29 | def load(fp): 30 | """Load a TM object from an open file containing JSON.""" 31 | result = json.load(fp, object_hook=decode) 32 | if not isinstance(result, TM): 33 | raise ValueError("Failed to decode JSON input as TM") 34 | return result 35 | 36 | 37 | def decode(data): 38 | if "elements" not in data and "flows" not in data and "boundaries" not in data: 39 | return data 40 | 41 | boundaries = decode_boundaries(data.pop("boundaries", [])) 42 | elements = decode_elements(data.pop("elements", []), boundaries) 43 | decode_flows(data.pop("flows", []), elements) 44 | 45 | if "name" not in data: 46 | raise ValueError("name property missing for threat model") 47 | if "onDuplicates" in data: 48 | data["onDuplicates"] = Action(data["onDuplicates"]) 49 | return TM(data.pop("name"), **data) 50 | 51 | 52 | def decode_boundaries(flat): 53 | boundaries = {} 54 | refs = {} 55 | for i, e in enumerate(flat): 56 | name = e.pop("name", None) 57 | if name is None: 58 | raise ValueError(f"name property missing in boundary {i}") 59 | if "inBoundary" in e: 60 | refs[name] = e.pop("inBoundary") 61 | e = Boundary(name, **e) 62 | boundaries[name] = e 63 | 64 | # do a second pass to resolve self-references 65 | for b in boundaries.values(): 66 | if b.name not in refs: 67 | continue 68 | b.inBoundary = boundaries[refs[b.name]] 69 | 70 | return boundaries 71 | 72 | 73 | def decode_elements(flat, boundaries): 74 | elements = {} 75 | for i, e in enumerate(flat): 76 | klass = getattr(sys.modules[__name__], e.pop("__class__", "Asset")) 77 | name = e.pop("name", None) 78 | if name is None: 79 | raise ValueError(f"name property missing in element {i}") 80 | if "inBoundary" in e: 81 | if e["inBoundary"] not in boundaries: 82 | raise ValueError( 83 | f"element {name} references invalid boundary {e['inBoundary']}" 84 | ) 85 | e["inBoundary"] = boundaries[e["inBoundary"]] 86 | e = klass(name, **e) 87 | elements[name] = e 88 | 89 | return elements 90 | 91 | 92 | def decode_flows(flat, elements): 93 | for i, e in enumerate(flat): 94 | name = e.pop("name", None) 95 | if name is None: 96 | raise ValueError(f"name property missing in dataflow {i}") 97 | if "source" not in e: 98 | raise ValueError(f"dataflow {name} is missing source property") 99 | if e["source"] not in elements: 100 | raise ValueError(f"dataflow {name} references invalid source {e['source']}") 101 | source = elements[e.pop("source")] 102 | if "sink" not in e: 103 | raise ValueError(f"dataflow {name} is missing sink property") 104 | if e["sink"] not in elements: 105 | raise ValueError(f"dataflow {name} references invalid sink {e['sink']}") 106 | sink = elements[e.pop("sink")] 107 | Dataflow(source, sink, name, **e) 108 | -------------------------------------------------------------------------------- /pytm/template_engine.py: -------------------------------------------------------------------------------- 1 | # shamelessly lifted from https://makina-corpus.com/blog/metier/2016/the-worlds-simplest-python-template-engine 2 | # but modified to include support to call methods which return lists, to call external utility methods, use 3 | # if operator with methods and added a not operator. 4 | 5 | import string 6 | 7 | 8 | class SuperFormatter(string.Formatter): 9 | """World's simplest Template engine.""" 10 | 11 | def format_field(self, value, spec): 12 | 13 | spec_parts = spec.split(":") 14 | if spec.startswith("repeat"): 15 | # Example usage, format, count of spec_parts, exampple format 16 | # object:repeat:template 2 {item.findings:repeat:{{item.id}}, } 17 | 18 | template = spec.partition(":")[-1] 19 | if type(value) is dict: 20 | value = value.items() 21 | return "".join([self.format(template, item=item) for item in value]) 22 | 23 | elif spec.startswith("call:") and hasattr(value, "__call__"): 24 | # Example usage, format, exampple format 25 | # methood:call {item.display_name:call:} 26 | # methood:call:template {item.parents:call:{{item.name}}, } 27 | result = value() 28 | 29 | if type(result) is list: 30 | template = spec.partition(":")[-1] 31 | return "".join([self.format(template, item=item) for item in result]) 32 | 33 | return result 34 | 35 | elif spec.startswith("call:"): 36 | # Example usage, format, exampple format 37 | # object:call:method_name {item:call:getFindingCount} 38 | # object:call:method_name:template {item:call:getNamesOfParents: 39 | # {{item}} 40 | # } 41 | 42 | method_name = spec_parts[1] 43 | 44 | result = self.call_util_method(method_name, value) 45 | 46 | if type(result) is list: 47 | template = spec.partition(":")[-1] 48 | template = template.partition(":")[-1] 49 | return "".join([self.format(template, item=item) for item in result]) 50 | 51 | return result 52 | 53 | elif (spec.startswith("if") or spec.startswith("not")): 54 | # Example usage, format, exampple format 55 | # object.bool:if:template {item.inScope:if:Is in scope} 56 | # object:if:template {item.findings:if:Has Findings} 57 | # object.method:if:template {item.parents:if:Has Parents} 58 | # 59 | # object.bool:not:template {item.inScope:not:Is not in scope} 60 | # object:not:template {item.findings:not:Has No Findings} 61 | # object.method:not:template {item.parents:not:Has No Parents} 62 | 63 | template = spec.partition(":")[-1] 64 | if (hasattr(value, "__call__")): 65 | result = value() 66 | else: 67 | result = value 68 | 69 | if (spec.startswith("if")): 70 | return (result and template or "") 71 | else: 72 | return (not result and template or "") 73 | 74 | else: 75 | return super(SuperFormatter, self).format_field(value, spec) 76 | 77 | def call_util_method(self, method_name, object): 78 | module_name = "pytm.report_util" 79 | klass_name = "ReportUtils" 80 | module = __import__(module_name, fromlist=['ReportUtils']) 81 | klass = getattr(module, klass_name) 82 | method = getattr(klass, method_name) 83 | 84 | result = method(object) 85 | return result 86 | -------------------------------------------------------------------------------- /docs/reveal.md: -------------------------------------------------------------------------------- 1 | # {tm.name} 2 | 3 | --- 4 | 5 | ## System Description 6 | 7 | {tm.description} 8 | 9 | --- 10 | 11 | ## Dataflow Diagram 12 | 13 | ![](sample.png) 14 | 15 | --- 16 | 17 | ## Dataflows 18 | 19 | ---- 20 | 21 | {dataflows:repeat: 22 | 23 | - **name** : {{item.display_name:call:}} 24 | - **from** : {{item.source.name}} 25 | - **to** : {{item.sink.name}}:{{item.dstPort}} 26 | - **data** : {{item.data}} 27 | - **protocol** : {{item.protocol}} 28 | 29 | ---- 30 | } 31 | 32 | --- 33 | 34 | ## Data Dictionary 35 | 36 | ---- 37 | 38 | {data:repeat: 39 | 40 | - **name** : {{item.name}} 41 | - **description** : {{item.description}} 42 | - **classification** : {{item.classification.name}} 43 | - **carried by** : {{item.carriedBy:repeat:{{{{item.name}}}}
}} 44 | - **processed by** : {{item.processedBy:repeat:{{{{item.name}}}}
}} 45 | 46 | ---- 47 | } 48 | 49 | 50 | --- 51 | 52 | ## Actors 53 | 54 | ---- 55 | 56 | {actors:repeat: 57 | - **name** : {{item.name}} 58 | - **description** : {{item.description}} 59 | - **is Admin** : {{item.isAdmin}} 60 | - **# of findings** : {{item:call:getFindingCount}} 61 | 62 | {{item.findings:not: 63 | --- 64 | }} 65 | 66 | {{item.findings:if: 67 | ---- 68 | **Findings** 69 | 70 | ---- 71 | 72 | {{item.findings:repeat: 73 | {{{{item.id}}}} -- {{{{item.description}}}} 74 | 75 | - **Targeted Element** : {{{{item.target}}}} 76 | - **Severity** : {{{{item.severity}}}} 77 | - **References** : {{{{item.references}}}} 78 | 79 | ---- 80 | 81 | }} 82 | }} 83 | } 84 | 85 | ## Trust Boundaries 86 | 87 | ---- 88 | 89 | {boundaries:repeat: 90 | - **name** : {{item.name}} 91 | - **description** : {{item.description}} 92 | - **in scope** : {{item.inScope}} 93 | - **immediate parent** : {{item.parents:if:{{item:call:getParentName}}}}{{item.parents:not:N/A, primary boundary}} 94 | - **all parents** : {{item.parents:call:{{{{item.display_name:call:}}}}, }} 95 | - **classification** : {{item.maxClassification}} 96 | - **finding count** : {{item:call:getFindingCount}} 97 | 98 | {{item.findings:not: 99 | --- 100 | }} 101 | 102 | {{item.findings:if: 103 | ---- 104 | **Findings** 105 | 106 | ---- 107 | 108 | {{item.findings:repeat: 109 | {{{{item.id}}}} - {{{{item.description}}}} 110 | 111 | - **Targeted Element** : {{{{item.target}}}} 112 | - **Severity** : {{{{item.severity}}}} 113 | - **References** : {{{{item.references}}}} 114 | ---- 115 | 116 | }} 117 | }} 118 | } 119 | 120 | ## Assets 121 | 122 | {assets:repeat: 123 | 124 | - **name** : {{item.name}} 125 | - **description** : {{item.description}} 126 | - **in scope** : {{item.inScope}} 127 | - **type** : {{item:call:getElementType}} 128 | - **# of findings** : {{item:call:getFindingCount}} 129 | 130 | {{item.findings:not: 131 | --- 132 | }} 133 | 134 | {{item.findings:if: 135 | ---- 136 | **Findings** 137 | 138 | ---- 139 | 140 | {{item.findings:repeat: 141 | {{{{item.id}}}} - {{{{item.description}}}} 142 | 143 | - **Targeted Element** : {{{{item.target}}}} 144 | - **Severity** : {{{{item.severity}}}} 145 | - **References** : {{{{item.references}}}} 146 | ---- 147 | 148 | }} 149 | }} 150 | } 151 | 152 | ## Data Flows 153 | 154 | {dataflows:repeat: 155 | Name|{{item.name}} 156 | |:----|:----| 157 | Description|{{item.description}}| 158 | Sink|{{item.sink}}| 159 | Source|{{item.source}}| 160 | Is Response|{{item.isResponse}}| 161 | In Scope|{{item.inScope}}| 162 | Finding Count|{{item:call:getFindingCount}}| 163 | 164 | {{item.findings:not: 165 | --- 166 | }} 167 | 168 | {{item.findings:if: 169 | ---- 170 | **Findings** 171 | 172 | ---- 173 | 174 | {{item.findings:repeat: 175 | {{{{item.id}}}} - {{{{item.description}}}} 176 | 177 | - **Targeted Element** : {{{{item.target}}}} 178 | - **Severity** : {{{{item.severity}}}} 179 | - **References** : {{{{item.references}}}} 180 | ---- 181 | 182 | }} 183 | }} 184 | } 185 | 186 | -------------------------------------------------------------------------------- /tests/dfd.dot: -------------------------------------------------------------------------------- 1 | digraph tm { 2 | graph [ 3 | fontname = Arial; 4 | fontsize = 14; 5 | ] 6 | node [ 7 | fontname = Arial; 8 | fontsize = 14; 9 | rankdir = lr; 10 | ] 11 | edge [ 12 | shape = none; 13 | arrowtail = onormal; 14 | fontname = Arial; 15 | fontsize = 12; 16 | ] 17 | labelloc = "t"; 18 | fontsize = 20; 19 | nodesep = 1; 20 | 21 | subgraph cluster_boundary_Companynet_88f2d9c06f { 22 | graph [ 23 | fontsize = 10; 24 | fontcolor = black; 25 | style = dashed; 26 | color = firebrick2; 27 | label = <Company net>; 28 | ] 29 | 30 | subgraph cluster_boundary_dmz_579e9aae81 { 31 | graph [ 32 | fontsize = 10; 33 | fontcolor = black; 34 | style = dashed; 35 | color = firebrick2; 36 | label = <dmz>; 37 | ] 38 | 39 | server_Gateway_f8af758679 [ 40 | shape = circle; 41 | color = black; 42 | fontcolor = black; 43 | label = "Gateway"; 44 | margin = 0.02; 45 | ] 46 | 47 | } 48 | 49 | subgraph cluster_boundary_backend_f2eb7a3ff7 { 50 | graph [ 51 | fontsize = 10; 52 | fontcolor = black; 53 | style = dashed; 54 | color = firebrick2; 55 | label = <backend>; 56 | ] 57 | 58 | server_WebServer_2c440ebe53 [ 59 | shape = circle; 60 | color = black; 61 | fontcolor = black; 62 | label = "Web Server"; 63 | margin = 0.02; 64 | ] 65 | 66 | datastore_SQLDatabase_0291419f72 [ 67 | shape = none; 68 | fixedsize = shape; 69 | image = "INSTALL_PATH/pytm/images/datastore_black.png"; 70 | imagescale = true; 71 | color = black; 72 | fontcolor = black; 73 | xlabel = "SQL Database"; 74 | label = ""; 75 | ] 76 | 77 | } 78 | 79 | } 80 | 81 | subgraph cluster_boundary_Internet_acf3059e70 { 82 | graph [ 83 | fontsize = 10; 84 | fontcolor = black; 85 | style = dashed; 86 | color = firebrick2; 87 | label = <Internet>; 88 | ] 89 | 90 | actor_User_d2006ce1bb [ 91 | shape = square; 92 | color = black; 93 | fontcolor = black; 94 | label = "User"; 95 | margin = 0.02; 96 | ] 97 | 98 | } 99 | 100 | actor_User_d2006ce1bb -> server_Gateway_f8af758679 [ 101 | color = black; 102 | fontcolor = black; 103 | dir = forward; 104 | label = "User enters\ncomments (*)"; 105 | ] 106 | 107 | server_Gateway_f8af758679 -> server_WebServer_2c440ebe53 [ 108 | color = black; 109 | fontcolor = black; 110 | dir = forward; 111 | label = "Request"; 112 | ] 113 | 114 | server_WebServer_2c440ebe53 -> datastore_SQLDatabase_0291419f72 [ 115 | color = black; 116 | fontcolor = black; 117 | dir = forward; 118 | label = "Insert query with\ncomments"; 119 | ] 120 | 121 | datastore_SQLDatabase_0291419f72 -> server_WebServer_2c440ebe53 [ 122 | color = black; 123 | fontcolor = black; 124 | dir = forward; 125 | label = "Retrieve comments"; 126 | ] 127 | 128 | server_WebServer_2c440ebe53 -> server_Gateway_f8af758679 [ 129 | color = black; 130 | fontcolor = black; 131 | dir = forward; 132 | label = "Response"; 133 | ] 134 | 135 | server_Gateway_f8af758679 -> actor_User_d2006ce1bb [ 136 | color = black; 137 | fontcolor = black; 138 | dir = forward; 139 | label = "Show comments (*)"; 140 | ] 141 | 142 | } 143 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Below you will find a collection of guidelines for submitting issues as well as contributing code to the PyTM repository. 4 | Please read those before starting an issue or a pull request. 5 | 6 | ## Issues 7 | 8 | Specific PyTM design and development issues, bugs, and feature requests are maintained by GitHub Issues. 9 | 10 | *Please do not post installation, build, usage, or modeling questions, or other requests for help to Issues.* 11 | Use the [PyTM-users list](https://groups.google.com/forum/#!forum/pytm-users) instead. 12 | This helps developers maintain a clear, uncluttered, and efficient view of the state of PyTM. 13 | See the chapter [PyTM-users](#PyTM-users) below for guidance on posting to the users list. 14 | 15 | When reporting an issue, it's most helpful to provide the following information, where applicable: 16 | * How does the problem look like and what steps reproduce it? 17 | * Can you reproduce it using the latest [master](https://github.com/izar/pytm/tree/master)? 18 | * What is your running environment? In particular: 19 | * OS, 20 | * Python version, 21 | * Dot or PlantUML version, if relevant, 22 | * Your model file, if possible. 23 | * **What have you already tried** to solve the problem? How did it fail? Are there any other issues related to yours? 24 | * If the bug is a crash, provide the backtrace (usually printed by PyTM). 25 | 26 | If only a small portion of the code/log is relevant to your issue, you may paste it directly into the post, preferably using Markdown syntax for code block: triple backtick ( \`\`\` ) to open/close a block. 27 | In other cases (multiple files, or long files), please **attach** them to the post - this greatly improves readability. 28 | 29 | If the problem arises during a complex operation (e.g. large model using PyTM), please reduce the example to the minimal size that still causes the error. 30 | Also, minimize influence of external modules, data etc. - this way it will be easier for others to understand and reproduce your issue, and eventually help you. 31 | Sometimes you will find the root cause yourself in the process. 32 | 33 | Try to give your issue a title that is succinct and specific. The devs will rename issues as needed to keep track of them. 34 | 35 | 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/`. 36 | 37 | To regenerate test fixtures for `json.dumps` and report tests add a `print(output)` statement in the test and run `make test 2>/dev/null > tests/output.json` or `make test 2>/dev/null > tests/output.md`. 38 | 39 | ## PyTM-users 40 | 41 | Before you post to the [PyTM-users list](https://groups.google.com/forum/#!forum/pytm-users), make sure you look for existing solutions. 42 | 43 | * [GitHub issues](https://github.com/izar/pytm/issues) tracker (some problems have been answered there), 44 | 45 | Found a post/issue with your exact problem, but with no answer? 46 | Don't just leave a "me too" message - provide the details of your case. 47 | Problems with more available information are easier to solve and attract good attention. 48 | 49 | When posting to the list, make sure you provide as much relevant information as possible - recommendations for an issue report (see above) are a good starting point. 50 | 51 | Formatting recommendations hold: paste short logs/code fragments into the post (use fixed-width text for them), **attach** long logs or multiple files. 52 | 53 | ## Pull Requests 54 | 55 | PyTM welcomes all contributions. 56 | 57 | Briefly: read commit by commit, a PR should tell a clean, compelling story of _one_ improvement to PyTM. In particular: 58 | 59 | * A PR should do one clear thing that obviously improves PyTM, and nothing more. Making many smaller PRs is better than making one large PR; review effort is superlinear in the amount of code involved. 60 | * Similarly, each commit should be a small, atomic change representing one step in development. PRs should be made of many commits where appropriate. 61 | * Please do rewrite PR history to be clean rather than chronological. Within-PR bugfixes, style cleanups, reversions, etc. should be squashed and should not appear in merged PR history. 62 | * Anything nonobvious from the code should be explained in comments, commit messages, or the PR description, as appropriate. 63 | 64 | (With many thanks to the Caffe project for their original CONTRIBUTING.md file) 65 | -------------------------------------------------------------------------------- /tests/dfd_colormap.dot: -------------------------------------------------------------------------------- 1 | digraph tm { 2 | graph [ 3 | fontname = Arial; 4 | fontsize = 14; 5 | ] 6 | node [ 7 | fontname = Arial; 8 | fontsize = 14; 9 | rankdir = lr; 10 | ] 11 | edge [ 12 | shape = none; 13 | arrowtail = onormal; 14 | fontname = Arial; 15 | fontsize = 12; 16 | ] 17 | labelloc = "t"; 18 | fontsize = 20; 19 | nodesep = 1; 20 | 21 | subgraph cluster_boundary_Companynet_88f2d9c06f { 22 | graph [ 23 | fontsize = 10; 24 | fontcolor = black; 25 | style = dashed; 26 | color = black; 27 | label = <Company net>; 28 | ] 29 | 30 | subgraph cluster_boundary_dmz_579e9aae81 { 31 | graph [ 32 | fontsize = 10; 33 | fontcolor = black; 34 | style = dashed; 35 | color = black; 36 | label = <dmz>; 37 | ] 38 | 39 | server_Gateway_f8af758679 [ 40 | shape = circle; 41 | color = firebrick3; fillcolor="#b2222222"; style=filled ; 42 | fontcolor = black; 43 | label = "Gateway"; 44 | margin = 0.02; 45 | ] 46 | 47 | } 48 | 49 | subgraph cluster_boundary_backend_f2eb7a3ff7 { 50 | graph [ 51 | fontsize = 10; 52 | fontcolor = black; 53 | style = dashed; 54 | color = black; 55 | label = <backend>; 56 | ] 57 | 58 | server_WebServer_2c440ebe53 [ 59 | shape = circle; 60 | color = firebrick3; fillcolor="#b2222222"; style=filled ; 61 | fontcolor = black; 62 | label = "Web Server"; 63 | margin = 0.02; 64 | ] 65 | 66 | datastore_SQLDatabase_0291419f72 [ 67 | shape = none; 68 | fixedsize = shape; 69 | image = "INSTALL_PATH/pytm/images/datastore_gold.png"; 70 | imagescale = true; 71 | color = gold; fillcolor="#ffd80022"; style=filled; 72 | fontcolor = black; 73 | xlabel = "SQL Database"; 74 | label = ""; 75 | ] 76 | 77 | } 78 | 79 | } 80 | 81 | subgraph cluster_boundary_Internet_acf3059e70 { 82 | graph [ 83 | fontsize = 10; 84 | fontcolor = black; 85 | style = dashed; 86 | color = black; 87 | label = <Internet>; 88 | ] 89 | 90 | actor_User_d2006ce1bb [ 91 | shape = square; 92 | color = darkgreen; fillcolor="#00630022"; style=filled; 93 | fontcolor = black; 94 | label = "User"; 95 | margin = 0.02; 96 | ] 97 | 98 | } 99 | 100 | actor_User_d2006ce1bb -> server_Gateway_f8af758679 [ 101 | color = gold; fillcolor="#ffd80022"; style=filled; 102 | fontcolor = gold; fillcolor="#ffd80022"; style=filled; 103 | dir = forward; 104 | label = "User enters\ncomments (*)"; 105 | ] 106 | 107 | server_Gateway_f8af758679 -> server_WebServer_2c440ebe53 [ 108 | color = gold; fillcolor="#ffd80022"; style=filled; 109 | fontcolor = gold; fillcolor="#ffd80022"; style=filled; 110 | dir = forward; 111 | label = "Request"; 112 | ] 113 | 114 | server_WebServer_2c440ebe53 -> datastore_SQLDatabase_0291419f72 [ 115 | color = gold; fillcolor="#ffd80022"; style=filled; 116 | fontcolor = gold; fillcolor="#ffd80022"; style=filled; 117 | dir = forward; 118 | label = "Insert query with\ncomments"; 119 | ] 120 | 121 | datastore_SQLDatabase_0291419f72 -> server_WebServer_2c440ebe53 [ 122 | color = gold; fillcolor="#ffd80022"; style=filled; 123 | fontcolor = gold; fillcolor="#ffd80022"; style=filled; 124 | dir = forward; 125 | label = "Retrieve comments"; 126 | ] 127 | 128 | server_WebServer_2c440ebe53 -> server_Gateway_f8af758679 [ 129 | color = gold; fillcolor="#ffd80022"; style=filled; 130 | fontcolor = gold; fillcolor="#ffd80022"; style=filled; 131 | dir = forward; 132 | label = "Response"; 133 | ] 134 | 135 | server_Gateway_f8af758679 -> actor_User_d2006ce1bb [ 136 | color = gold; fillcolor="#ffd80022"; style=filled; 137 | fontcolor = gold; fillcolor="#ffd80022"; style=filled; 138 | dir = forward; 139 | label = "Show comments (*)"; 140 | ] 141 | 142 | } 143 | -------------------------------------------------------------------------------- /tm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from pytm import ( 4 | TM, 5 | Actor, 6 | Boundary, 7 | Classification, 8 | Data, 9 | Dataflow, 10 | Datastore, 11 | Lambda, 12 | Server, 13 | DatastoreType, 14 | Assumption, 15 | ) 16 | 17 | tm = TM("my test tm") 18 | tm.description = """This is a sample threat model of a very simple system - a web-based comment system. 19 | The user enters comments and these are added to a database and displayed back to the user. 20 | The thought is that it is, though simple, a complete enough example to express meaningful threats.""" 21 | tm.isOrdered = True 22 | tm.mergeResponses = True 23 | tm.assumptions = [ 24 | "Here you can document a list of assumptions about the system", 25 | ] 26 | 27 | internet = Boundary("Internet") 28 | 29 | server_db = Boundary("Server/DB") 30 | server_db.levels = [2] 31 | 32 | vpc = Boundary("AWS VPC") 33 | 34 | user = Actor("User") 35 | user.inBoundary = internet 36 | user.levels = [2] 37 | 38 | web = Server("Web Server") 39 | web.OS = "Ubuntu" 40 | web.controls.isHardened = True 41 | web.controls.sanitizesInput = False 42 | web.controls.encodesOutput = True 43 | web.controls.authorizesSource = False 44 | web.sourceFiles = ["pytm/json.py", "docs/template.md"] 45 | web.assumptions = [ 46 | Assumption( 47 | "This webserver does not use PHP", 48 | exclude=["INP16"], 49 | ), 50 | ] 51 | 52 | db = Datastore("SQL Database") 53 | db.OS = "CentOS" 54 | db.controls.isHardened = False 55 | db.inBoundary = server_db 56 | db.type = DatastoreType.SQL 57 | db.inScope = True 58 | db.maxClassification = Classification.RESTRICTED 59 | db.levels = [2] 60 | 61 | secretDb = Datastore("Real Identity Database") 62 | secretDb.OS = "CentOS" 63 | secretDb.sourceFiles = ["pytm/pytm.py"] 64 | secretDb.controls.isHardened = True 65 | secretDb.inBoundary = server_db 66 | secretDb.type = DatastoreType.SQL 67 | secretDb.inScope = True 68 | secretDb.storesPII = True 69 | secretDb.maxClassification = Classification.TOP_SECRET 70 | 71 | my_lambda = Lambda("AWS Lambda") 72 | my_lambda.controls.hasAccessControl = True 73 | my_lambda.inBoundary = vpc 74 | my_lambda.levels = [1, 2] 75 | 76 | token_user_identity = Data( 77 | "Token verifying user identity", classification=Classification.SECRET 78 | ) 79 | db_to_secretDb = Dataflow(db, secretDb, "Database verify real user identity") 80 | db_to_secretDb.protocol = "RDA-TCP" 81 | db_to_secretDb.dstPort = 40234 82 | db_to_secretDb.data = token_user_identity 83 | db_to_secretDb.note = "Verifying that the user is who they say they are." 84 | db_to_secretDb.maxClassification = Classification.SECRET 85 | 86 | comments_in_text = Data( 87 | "Comments in HTML or Markdown", classification=Classification.PUBLIC 88 | ) 89 | user_to_web = Dataflow(user, web, "User enters comments (*)") 90 | user_to_web.protocol = "HTTP" 91 | user_to_web.dstPort = 80 92 | user_to_web.data = comments_in_text 93 | user_to_web.note = "This is a simple web app\nthat stores and retrieves user comments." 94 | 95 | query_insert = Data("Insert query with comments", classification=Classification.PUBLIC) 96 | web_to_db = Dataflow(web, db, "Insert query with comments") 97 | web_to_db.protocol = "MySQL" 98 | web_to_db.dstPort = 3306 99 | web_to_db.data = query_insert 100 | web_to_db.note = ( 101 | "Web server inserts user comments\ninto it's SQL query and stores them in the DB." 102 | ) 103 | 104 | comment_retrieved = Data( 105 | "Web server retrieves comments from DB", classification=Classification.PUBLIC 106 | ) 107 | db_to_web = Dataflow(db, web, "Retrieve comments") 108 | db_to_web.protocol = "MySQL" 109 | db_to_web.dstPort = 80 110 | db_to_web.data = comment_retrieved 111 | db_to_web.responseTo = web_to_db 112 | 113 | comment_to_show = Data( 114 | "Web server shows comments to the end user", classifcation=Classification.PUBLIC 115 | ) 116 | web_to_user = Dataflow(web, user, "Show comments (*)") 117 | web_to_user.protocol = "HTTP" 118 | web_to_user.data = comment_to_show 119 | web_to_user.responseTo = user_to_web 120 | 121 | clear_op = Data("Serverless function clears DB", classification=Classification.PUBLIC) 122 | my_lambda_to_db = Dataflow(my_lambda, db, "Serverless function periodically cleans DB") 123 | my_lambda_to_db.protocol = "MySQL" 124 | my_lambda_to_db.dstPort = 3306 125 | my_lambda_to_db.data = clear_op 126 | 127 | userIdToken = Data( 128 | name="User ID Token", 129 | description="Some unique token that represents the user real data in the secret database", 130 | classification=Classification.TOP_SECRET, 131 | traverses=[user_to_web, db_to_secretDb], 132 | processedBy=[db, secretDb], 133 | ) 134 | 135 | if __name__ == "__main__": 136 | tm.process() 137 | 138 | -------------------------------------------------------------------------------- /docs/advanced_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## System Description 4 | 5 | {tm.description} 6 | 7 | ## Dataflow Diagram - Level 0 DFD 8 | 9 | ![](sample.png) 10 | 11 |   12 | 13 | ## Dataflows 14 | 15 | Name|From|To |Data|Protocol|Port 16 | |:----:|:----:|:---:|:----:|:--------:|:----:| 17 | {dataflows:repeat:|{{item.display_name:call:}}|{{item.source.name}}|{{item.sink.name}}|{{item.data}}|{{item.protocol}}|{{item.dstPort}}| 18 | } 19 | 20 | ## Data Dictionary 21 | 22 | Name|Description|Classification|Carried|Processed 23 | |:----:|:--------:|:----:|:----|:----| 24 | {data:repeat:|{{item.name}}|{{item.description}}|{{item.classification.name}}|{{item.carriedBy:repeat:{{{{item.name}}}}
}}|{{item.processedBy:repeat:{{{{item.name}}}}
}}| 25 | } 26 | 27 | ## Actors 28 | 29 | {actors:repeat: 30 | Name|{{item.name}} 31 | |:----|:----| 32 | Description|{{item.description}}| 33 | Is Admin|{{item.isAdmin}} 34 | Finding Count|{{item:call:getFindingCount}}| 35 | 36 | {{item.findings:if: 37 | 38 | **Threats** 39 | 40 | {{item.findings:repeat: 41 |
42 | {{{{item.id}}}} -- {{{{item.threat_id}}}} -- {{{{item.description}}}} 43 |
Targeted Element
44 |

{{{{item.target}}}}

45 |
Severity
46 |

{{{{item.severity}}}}

47 |
Example Instances
48 |

{{{{item.example}}}}

49 |
Mitigations
50 |

{{{{item.mitigations}}}}

51 |
References
52 |

{{{{item.references}}}}

53 |   54 |
55 | }} 56 | }} 57 | } 58 | 59 | ## Boundaries 60 | 61 | {boundaries:repeat: 62 | Name|{{item.name}} 63 | |:----|:----| 64 | Description|{{item.description}}| 65 | In Scope|{{item.inScope}}| 66 | Immediate Parent|{{item.parents:if:{{item:call:getParentName}}}}{{item.parents:not:N/A, primary boundary}}| 67 | All Parents|{{item.parents:call:{{{{item.display_name:call:}}}}, }}| 68 | Classification|{{item.maxClassification}}| 69 | Finding Count|{{item:call:getFindingCount}}| 70 | 71 | {{item.findings:if: 72 | 73 | **Threats** 74 | 75 | {{item.findings:repeat: 76 |
77 | {{{{item.id}}}} -- {{{{item.threat_id}}}} -- {{{{item.description}}}} 78 |
Targeted Element
79 |

{{{{item.target}}}}

80 |
Severity
81 |

{{{{item.severity}}}}

82 |
Example Instances
83 |

{{{{item.example}}}}

84 |
Mitigations
85 |

{{{{item.mitigations}}}}

86 |
References
87 |

{{{{item.references}}}}

88 |   89 |
90 | }} 91 | }} 92 | } 93 | 94 | ## Assets 95 | 96 | {assets:repeat: 97 | Name|{{item.name}}| 98 | |:----|:----| 99 | Description|{{item.description}}| 100 | In Scope|{{item.inScope}}| 101 | Type|{{item:call:getElementType}}| 102 | Finding Count|{{item:call:getFindingCount}}| 103 | 104 | {{item.findings:if: 105 | 106 | **Threats** 107 | 108 | {{item.findings:repeat: 109 |
110 | {{{{item.id}}}} -- {{{{item.threat_id}}}} -- {{{{item.description}}}} 111 |
Targeted Element
112 |

{{{{item.target}}}}

113 |
Severity
114 |

{{{{item.severity}}}}

115 |
Example Instances
116 |

{{{{item.example}}}}

117 |
Mitigations
118 |

{{{{item.mitigations}}}}

119 |
References
120 |

{{{{item.references}}}}

121 |   122 |
123 | }} 124 | }} 125 | } 126 | 127 | ## Data Flows 128 | 129 | {dataflows:repeat: 130 | Name|{{item.name}} 131 | |:----|:----| 132 | Description|{{item.description}}| 133 | Sink|{{item.sink}}| 134 | Source|{{item.source}}| 135 | Is Response|{{item.isResponse}}| 136 | In Scope|{{item.inScope}}| 137 | Finding Count|{{item:call:getFindingCount}}| 138 | 139 | {{item.findings:if: 140 | 141 | **Threats** 142 | 143 | {{item.findings:repeat: 144 |
145 | {{{{item.id}}}} -- {{{{item.threat_id}}}} -- {{{{item.description}}}} 146 |
Targeted Element
147 |

{{{{item.target}}}}

148 |
Severity
149 |

{{{{item.severity}}}}

150 |
Example Instances
151 |

{{{{item.example}}}}

152 |
Mitigations
153 |

{{{{item.mitigations}}}}

154 |
References
155 |

{{{{item.references}}}}

156 |   157 |
158 | }} 159 | }} 160 | } 161 | 162 | {tm.excluded_findings:if: 163 | # Excluded Threats 164 | } 165 | 166 | {tm.excluded_findings:repeat: 167 |
168 | {{item.id}} -- {{item.threat_id}} -- {{item.description}} 169 |

**{{item.threat_id}}** was excluded for **{{item.target}}** because of the assumption: "{{item.assumption.name}} 170 | "

171 | {{item.assumption.description:if: 172 |
Assumption description
173 |

{{item.assumption.description}}

174 | }} 175 | 176 |
Targeted Element
177 |

{{item.target}}

178 |
Severity
179 |

{{item.severity}}

180 |
Example Instances
181 |

{{item.example}}

182 |
References
183 |

{{item.references}}

184 |
185 | } 186 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.2.0 2 | 3 | ## Breaking changes 4 | 5 | - Replace `usesLatestTLSversion` with `minTLSVersion` in assets and `tlsVersion` in data flows [#123](https://github.com/izar/pytm/pull/123) 6 | - When the `data` attribute of elements is initialied with a string, convert it to a `Data` object with `undefined` as name and the string as description; change the default classification from `PUBLIC` to `UNKNOWN` [#148](https://github.com/izar/pytm/pull/148) 7 | 8 | ## New features 9 | 10 | - Separate actors and assets from elements when dumping the model to JSON [#150](https://github.com/izar/pytm/pull/150) 11 | - Add unique Finding ids [#154](https://github.com/izar/pytm/pull/154) 12 | - Allow to associate the threat model script with source code files and check their age difference [#145](https://github.com/izar/pytm/pull/145) 13 | - Adapt [the DFD3 notation](https://github.com/adamshostack/DFD3) [#143](https://github.com/izar/pytm/pull/143) 14 | - Allow to override findings (threats) attributes [#137](https://github.com/izar/pytm/pull/137) 15 | - Allow to mark data as PII or credentials and check if it's protected [#127](https://github.com/izar/pytm/pull/127) 16 | - Added '--levels' - every element now has a 'levels' attribute, a list of integers denoting different DFD levels for rendering 17 | - Added HTML docs using pdoc [#110](https://github.com/izar/pytm/pull/110) 18 | - Added `checksDestinationRevocation` attribute to account for certificate revocation checks [#109](https://github.com/izar/pytm/pull/109) 19 | 20 | ## Bug fixes 21 | 22 | - Escape HTML entities in Threat attributes [#149](https://github.com/izar/pytm/pull/149) 23 | - Fix generating reports for models with a `Datastore` that has `isEncryptedAtRest` set and a `Data` that has `isStored` set [#141](https://github.com/izar/pytm/pull/141) 24 | - Fix condition on the `Data Leak` threat so it does not always match [#139](https://github.com/izar/pytm/pull/139) 25 | - Fixed printing the data attribute in reports [#123](https://github.com/izar/pytm/pull/123) 26 | - Added a markdown file with threats [#126](https://github.com/izar/pytm/pull/126) 27 | - Fixed drawing nested boudnaries [#117](https://github.com/izar/pytm/pull/117) 28 | - Add missing `provideIntegrity` attribute in `Actor` and `Asset` classes [#116](https://github.com/izar/pytm/pull/116) 29 | 30 | # 1.1.2 31 | 32 | - Added Poetry [#108](https://github.com/izar/pytm/pull/108) 33 | - Fix drawing DFDs for nested Boundaries [#107](https://github.com/izar/pytm/pull/107) 34 | 35 | # 1.1.1 36 | 37 | - Fix pydal dependencies install on pip [#106](https://github.com/izar/pytm/pull/106) 38 | 39 | # 1.1.0 40 | 41 | ## Breaking changes 42 | 43 | - Removed `HandlesResources` attribute from the `Process` class, which duplicates `handlesResources` 44 | - Change default `Dataflow.dstPort` attribute value from `10000` to `-1` 45 | 46 | ## New features 47 | 48 | 49 | - Add dump of elements and findings to sqlite database using "--sqldump " (with result in ./sqldump/) [#103](https://github.com/izar/pytm/pull/103) 50 | - Add Data element and DataLeak finding to support creation of a data dictionary separate from the model [#104](https://github.com/izar/pytm/pull/104) 51 | - Add JSON input [#105](https://github.com/izar/pytm/pull/105) 52 | - Add JSON output [#102](https://github.com/izar/pytm/pull/102) 53 | - Use numbered dataflow labels in sequence diagram [#94](https://github.com/izar/pytm/pull/94) 54 | - Move authenticateDestination to base Element [#88](https://github.com/izar/pytm/pull/88) 55 | - Assign inputs and outputs to all elements [#89](https://github.com/izar/pytm/pull/89) 56 | - Allow detecting and/or hiding duplicate dataflows by setting `TM.onDuplicates` [#100](https://github.com/izar/pytm/pull/100) 57 | - Ignore unused elements if `TM.ignoreUnused` is True [#84](https://github.com/izar/pytm/pull/84) 58 | - Assign findings to elements [#86](https://github.com/izar/pytm/pull/86) 59 | - Add description to class attributes [#91](https://github.com/izar/pytm/pull/91) 60 | - New Element methods to be used in threat conditions [#82](https://github.com/izar/pytm/pull/82) 61 | - Provide a Docker image and allow running make targets in a container [#87](https://github.com/izar/pytm/pull/87) 62 | - Dataflow inherits source and/or sink attribute values [#79](https://github.com/izar/pytm/pull/79) 63 | - Merge edges in DFD when `TM.mergeResponses` is True; allow marking `Dataflow` as responses [#76](https://github.com/izar/pytm/pull/76) 64 | - Automatic ordering of dataflows when `TM.isOrdered` is True [#66](https://github.com/izar/pytm/pull/66) 65 | - Loading a custom threats file by setting `TM.threatsFile` [#68](https://github.com/izar/pytm/pull/68) 66 | - Setting properties on init [#67](https://github.com/izar/pytm/pull/67) 67 | - Wrap long labels in DFDs [#65](https://github.com/izar/pytm/pull/65) 68 | 69 | ## Bug fixes 70 | 71 | - Ensure all items have correct color, based on scope [#93](https://github.com/izar/pytm/pull/93) 72 | - Add missing server isResilient property [#63](https://github.com/izar/pytm/issues/63) 73 | - Advanced templates in repeat blocks [#81](https://github.com/izar/pytm/pull/81) 74 | - Produce stable diagrams [#79](https://github.com/izar/pytm/pull/79) 75 | - Allow overriding classes [#64](https://github.com/izar/pytm/pull/64) 76 | 77 | # 1.0.0 78 | 79 | ## New features 80 | 81 | - New threats [#61](https://github.com/izar/pytm/pull/61) 82 | 83 | ## Bug fixes 84 | 85 | - UnicodeDecodeError: 'charmap' codec can't decode byte 0x9d [#57](https://github.com/izar/pytm/pull/57) 86 | - `_uniq_name` missing 1 required positional argument [#60](https://github.com/izar/pytm/pull/60) 87 | - Render objects with duplicate names [#45](https://github.com/izar/pytm/issues/45) 88 | 89 | # 0.8.1 90 | 91 | ## Bug fixes 92 | 93 | - Draw nested boundaries [#54](https://github.com/izar/pytm/pull/54), [#55](https://github.com/izar/pytm/pull/55) 94 | 95 | # 0.8.0 96 | 97 | ## New features 98 | 99 | - Draw nested boundaries [#52](https://github.com/izar/pytm/pull/52) 100 | -------------------------------------------------------------------------------- /tests/test_private_func.py: -------------------------------------------------------------------------------- 1 | import random 2 | import pytest 3 | 4 | from pytm.pytm import ( 5 | TM, 6 | Actor, 7 | Assumption, 8 | Boundary, 9 | Data, 10 | Dataflow, 11 | Datastore, 12 | DatastoreType, 13 | Finding, 14 | Process, 15 | Server, 16 | Threat, 17 | UIError, 18 | encode_threat_data, 19 | ) 20 | 21 | class TestUniqueNames: 22 | def test_duplicate_boundary_names_have_different_unique_names(self): 23 | random.seed(0) 24 | object_1 = Boundary("foo") 25 | object_2 = Boundary("foo") 26 | 27 | object_1_uniq_name = object_1._uniq_name() 28 | object_2_uniq_name = object_2._uniq_name() 29 | 30 | assert object_1_uniq_name != object_2_uniq_name 31 | assert object_1_uniq_name == "boundary_foo_acf3059e70" 32 | assert object_2_uniq_name == "boundary_foo_88f2d9c06f" 33 | 34 | class TestAttributes: 35 | def test_write_once(self): 36 | user = Actor("User") 37 | with pytest.raises(ValueError): 38 | user.name = "Computer" 39 | 40 | def test_kwargs(self): 41 | user = Actor("User", isAdmin=True) 42 | assert user.isAdmin is True 43 | user = Actor("User") 44 | assert user.isAdmin is False 45 | user.isAdmin = True 46 | assert user.isAdmin is True 47 | 48 | def test_load_threats(self): 49 | tm = TM("TM") 50 | assert len(TM._threats) != 0 51 | with pytest.raises(UIError): 52 | tm.threatsFile = "threats.json" 53 | with pytest.raises(UIError): 54 | TM("TM", threatsFile="threats.json") 55 | 56 | def test_responses(self): 57 | tm = TM("my test tm", description="aa", isOrdered=True) 58 | user = Actor("User") 59 | web = Server("Web Server") 60 | db = Datastore("SQL Database") 61 | http_req = Dataflow(user, web, "http req") 62 | insert = Dataflow(web, db, "insert data") 63 | query = Dataflow(web, db, "query") 64 | query_resp = Dataflow(db, web, "query results", responseTo=query) 65 | http_resp = Dataflow(web, user, "http resp") 66 | http_resp.responseTo = http_req 67 | assert tm.check() 68 | assert http_req.response == http_resp 69 | assert http_resp.isResponse is True 70 | assert query_resp.isResponse is True 71 | assert query_resp.responseTo == query 72 | assert query.response == query_resp 73 | assert insert.response is None 74 | assert insert.isResponse is False 75 | 76 | def test_defaults(self): 77 | tm = TM("TM") 78 | user_data = Data("HTTP") 79 | user = Actor("User", data=user_data) 80 | user.controls.authenticatesDestination = True 81 | json_data = Data("JSON") 82 | server = Server( 83 | "Server", port=443, protocol="HTTPS", isEncrypted=True, data=json_data 84 | ) 85 | sql_resp = Data("SQL resp") 86 | db = Datastore( 87 | "PostgreSQL", 88 | port=5432, 89 | protocol="PostgreSQL", 90 | data=sql_resp, 91 | ) 92 | db.controls.isEncrypted = False 93 | db.type = DatastoreType.SQL 94 | worker = Process("Task queue worker") 95 | req_get_data = Data("HTTP GET") 96 | req_get = Dataflow(user, server, "HTTP GET", data=req_get_data) 97 | server_query_data = Data("SQL") 98 | server_query = Dataflow(server, db, "Query", data=server_query_data) 99 | result_data = Data("Results") 100 | result = Dataflow(db, server, "Results", data=result_data, isResponse=True) 101 | resp_get_data = Data("HTTP Response") 102 | resp_get = Dataflow(server, user, "HTTP Response", data=resp_get_data, isResponse=True) 103 | test_assumption = Assumption("test assumption") 104 | resp_get.assumptions = [test_assumption] 105 | req_post_data = Data("JSON") 106 | req_post = Dataflow(user, server, "HTTP POST", data=req_post_data) 107 | resp_post = Dataflow(server, user, "HTTP Response", isResponse=True) 108 | test_assumption_exclude = Assumption("test assumption", exclude=["ABCD", "BCDE"]) 109 | resp_post.assumptions = [test_assumption_exclude] 110 | sql_data = Data("SQL") 111 | worker_query = Dataflow(worker, db, "Query", data=sql_data) 112 | Dataflow(db, worker, "Results", isResponse=True) 113 | cookie = Data("Auth Cookie", carriedBy=[req_get, req_post]) 114 | assert tm.check() 115 | assert req_get.srcPort == -1 116 | assert req_get.dstPort == server.port 117 | assert req_get.controls.isEncrypted == server.controls.isEncrypted 118 | assert req_get.controls.authenticatesDestination == user.controls.authenticatesDestination 119 | assert req_get.protocol == server.protocol 120 | assert user.data.issubset(req_get.data) 121 | assert server_query.srcPort == -1 122 | assert server_query.dstPort == db.port 123 | assert server_query.controls.isEncrypted == db.controls.isEncrypted 124 | assert server_query.controls.authenticatesDestination == server.controls.authenticatesDestination 125 | assert server_query.protocol == db.protocol 126 | assert server.data.issubset(server_query.data) 127 | assert result.srcPort == db.port 128 | assert result.dstPort == -1 129 | assert result.controls.isEncrypted == db.controls.isEncrypted 130 | assert result.controls.authenticatesDestination is False 131 | assert result.protocol == db.protocol 132 | assert db.data.issubset(result.data) 133 | assert db.assumptions == [] 134 | assert resp_get.srcPort == server.port 135 | assert resp_get.dstPort == -1 136 | assert resp_get.controls.isEncrypted == server.controls.isEncrypted 137 | assert resp_get.controls.authenticatesDestination is False 138 | assert resp_get.protocol == server.protocol 139 | assert server.data.issubset(resp_get.data) 140 | assert resp_get.assumptions == [test_assumption] 141 | assert req_post.srcPort == -1 142 | assert req_post.dstPort == server.port 143 | assert req_post.controls.isEncrypted == server.controls.isEncrypted 144 | assert req_post.controls.authenticatesDestination == user.controls.authenticatesDestination 145 | assert req_post.protocol == server.protocol 146 | assert user.data.issubset(req_post.data) 147 | assert resp_post.srcPort == server.port 148 | assert resp_post.dstPort == -1 149 | assert resp_post.controls.isEncrypted == server.controls.isEncrypted 150 | assert resp_post.controls.authenticatesDestination is False 151 | assert resp_post.protocol == server.protocol 152 | assert server.data.issubset(resp_post.data) 153 | assert resp_post.assumptions == [test_assumption_exclude] 154 | assert resp_post.assumptions[0].exclude == set(test_assumption_exclude.exclude) 155 | assert server.inputs == [req_get, req_post] 156 | assert server.outputs == [server_query] 157 | assert worker.inputs == [] 158 | assert worker.outputs == [worker_query] 159 | assert cookie.carriedBy == [req_get, req_post] 160 | assert set(cookie.processedBy) == set([user, server]) 161 | assert cookie in req_get.data 162 | assert set([d.name for d in req_post.data]) == set([cookie.name, "HTTP", "JSON"]) 163 | 164 | class TestMethod: 165 | def test_defaults(self): 166 | tm = TM("my test tm", description="aa", isOrdered=True) 167 | internet = Boundary("Internet") 168 | cloud = Boundary("Cloud") 169 | user = Actor("User", inBoundary=internet) 170 | server = Server("Server") 171 | db = Datastore("DB", inBoundary=cloud) 172 | db.type = DatastoreType.SQL 173 | func = Datastore("Lambda function", inBoundary=cloud) 174 | request = Dataflow(user, server, "request") 175 | response = Dataflow(server, user, "response", isResponse=True) 176 | user_query = Dataflow(user, db, "user query") 177 | server_query = Dataflow(server, db, "server query") 178 | func_query = Dataflow(func, db, "func query") 179 | default_target = ["Actor", "Boundary", "Dataflow", "Datastore", "Server"] 180 | testCases = [ 181 | {"target": server, "condition": "target.oneOf(Server, Datastore)"}, 182 | {"target": server, "condition": "not target.oneOf(Actor, Dataflow)"}, 183 | {"target": request, "condition": "target.crosses(Boundary)"}, 184 | {"target": user_query, "condition": "target.crosses(Boundary)"}, 185 | {"target": server_query, "condition": "target.crosses(Boundary)"}, 186 | {"target": func_query, "condition": "not target.crosses(Boundary)"}, 187 | {"target": func_query, "condition": "not target.enters(Boundary)"}, 188 | {"target": func_query, "condition": "not target.exits(Boundary)"}, 189 | {"target": request, "condition": "not target.enters(Boundary)"}, 190 | {"target": request, "condition": "target.exits(Boundary)"}, 191 | {"target": response, "condition": "target.enters(Boundary)"}, 192 | {"target": response, "condition": "not target.exits(Boundary)"}, 193 | {"target": user, "condition": "target.inside(Boundary)"}, 194 | {"target": func, "condition": "not any(target.inputs)"}, 195 | { 196 | "target": server, 197 | "condition": "any(f.sink.oneOf(Datastore) and f.sink.type == DatastoreType.SQL " 198 | "for f in target.outputs)", 199 | }, 200 | ] 201 | assert tm.check() 202 | for case in testCases: 203 | t = Threat(SID="", target=default_target, condition=case["condition"]) 204 | assert t.apply(case["target"]), f"Failed to match {case['target']} against {case['condition']}" 205 | 206 | class TestFunction: 207 | def test_encode_threat_data(self): 208 | findings = [ 209 | Finding( 210 | description="A test description", 211 | severity="High", 212 | id="1", 213 | threat_id="INP01", 214 | cvss="9.876", 215 | response="A test response", 216 | ), 217 | Finding( 218 | description="An escape test 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module pytm.report_util

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
class ReportUtils:
 30 |     @staticmethod
 31 |     def getParentName(element):
 32 |         from pytm import Boundary
 33 |         if (isinstance(element, Boundary)):
 34 |             parent = element.inBoundary
 35 |             if (parent is not None):
 36 |                 return parent.name
 37 |             else:
 38 |                 return str("")
 39 |         else:
 40 |             return "ERROR: getParentName method is not valid for " + element.__class__.__name__
 41 | 
 42 | 
 43 |     @staticmethod
 44 |     def getNamesOfParents(element):
 45 |         from pytm import Boundary
 46 |         if (isinstance(element, Boundary)):
 47 |             parents = [p.name for p in element.parents()] 
 48 |             return parents 
 49 |         else:
 50 |             return "ERROR: getNamesOfParents method is not valid for " + element.__class__.__name__
 51 | 
 52 |     @staticmethod
 53 |     def getFindingCount(element):
 54 |         from pytm import Element
 55 |         if (isinstance(element, Element)):
 56 |             return str(len(list(element.findings)))
 57 |         else:
 58 |             return "ERROR: getFindingCount method is not valid for " + element.__class__.__name__
 59 | 
 60 |     @staticmethod
 61 |     def getElementType(element):
 62 |         from pytm import Element
 63 |         if (isinstance(element, Element)):
 64 |             return str(element.__class__.__name__)
 65 |         else:
 66 |             return "ERROR: getElementType method is not valid for " + element.__class__.__name__
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |

Classes

77 |
78 |
79 | class ReportUtils 80 |
81 |
82 |
83 |
84 | 85 | Expand source code 86 | 87 |
class ReportUtils:
 88 |     @staticmethod
 89 |     def getParentName(element):
 90 |         from pytm import Boundary
 91 |         if (isinstance(element, Boundary)):
 92 |             parent = element.inBoundary
 93 |             if (parent is not None):
 94 |                 return parent.name
 95 |             else:
 96 |                 return str("")
 97 |         else:
 98 |             return "ERROR: getParentName method is not valid for " + element.__class__.__name__
 99 | 
100 | 
101 |     @staticmethod
102 |     def getNamesOfParents(element):
103 |         from pytm import Boundary
104 |         if (isinstance(element, Boundary)):
105 |             parents = [p.name for p in element.parents()] 
106 |             return parents 
107 |         else:
108 |             return "ERROR: getNamesOfParents method is not valid for " + element.__class__.__name__
109 | 
110 |     @staticmethod
111 |     def getFindingCount(element):
112 |         from pytm import Element
113 |         if (isinstance(element, Element)):
114 |             return str(len(list(element.findings)))
115 |         else:
116 |             return "ERROR: getFindingCount method is not valid for " + element.__class__.__name__
117 | 
118 |     @staticmethod
119 |     def getElementType(element):
120 |         from pytm import Element
121 |         if (isinstance(element, Element)):
122 |             return str(element.__class__.__name__)
123 |         else:
124 |             return "ERROR: getElementType method is not valid for " + element.__class__.__name__
125 |
126 |

Static methods

127 |
128 |
129 | def getElementType(element) 130 |
131 |
132 |
133 |
134 | 135 | Expand source code 136 | 137 |
@staticmethod
138 | def getElementType(element):
139 |     from pytm import Element
140 |     if (isinstance(element, Element)):
141 |         return str(element.__class__.__name__)
142 |     else:
143 |         return "ERROR: getElementType method is not valid for " + element.__class__.__name__
144 |
145 |
146 |
147 | def getFindingCount(element) 148 |
149 |
150 |
151 |
152 | 153 | Expand source code 154 | 155 |
@staticmethod
156 | def getFindingCount(element):
157 |     from pytm import Element
158 |     if (isinstance(element, Element)):
159 |         return str(len(list(element.findings)))
160 |     else:
161 |         return "ERROR: getFindingCount method is not valid for " + element.__class__.__name__
162 |
163 |
164 |
165 | def getNamesOfParents(element) 166 |
167 |
168 |
169 |
170 | 171 | Expand source code 172 | 173 |
@staticmethod
174 | def getNamesOfParents(element):
175 |     from pytm import Boundary
176 |     if (isinstance(element, Boundary)):
177 |         parents = [p.name for p in element.parents()] 
178 |         return parents 
179 |     else:
180 |         return "ERROR: getNamesOfParents method is not valid for " + element.__class__.__name__
181 |
182 |
183 |
184 | def getParentName(element) 185 |
186 |
187 |
188 |
189 | 190 | Expand source code 191 | 192 |
@staticmethod
193 | def getParentName(element):
194 |     from pytm import Boundary
195 |     if (isinstance(element, Boundary)):
196 |         parent = element.inBoundary
197 |         if (parent is not None):
198 |             return parent.name
199 |         else:
200 |             return str("")
201 |     else:
202 |         return "ERROR: getParentName method is not valid for " + element.__class__.__name__
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 | 236 |
237 | 240 | 241 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![build+test](https://github.com/izar/pytm/workflows/build%2Btest/badge.svg) 2 | [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/11093/badge)](https://www.bestpractices.dev/projects/11093) 3 | 4 | # pytm: A Pythonic framework for threat modeling 5 | 6 | ![pytm logo](docs/pytm-logo.svg) 7 | 8 | ## Introduction 9 | 10 | Traditional threat modeling too often comes late to the party, or sometimes not at all. In addition, creating manual data flows and reports can be extremely time-consuming. The goal of pytm is to shift threat modeling to the left, making threat modeling more automated and developer-centric. 11 | 12 | ## Features 13 | 14 | Based on your input and definition of the architectural design, pytm can automatically generate the following items: 15 | - Data Flow Diagram (DFD) 16 | - Sequence Diagram 17 | - Relevant threats to your system 18 | 19 | ## Requirements 20 | 21 | * Linux/MacOS 22 | * Python 3.x 23 | * Graphviz package 24 | * Java (OpenJDK 10 or 11) 25 | * [plantuml.jar](http://sourceforge.net/projects/plantuml/files/plantuml.jar/download) 26 | 27 | ## Getting Started 28 | 29 | The `tm.py` is an example model. You can run it to generate the report and diagram image files that it references: 30 | 31 | ``` 32 | mkdir -p tm 33 | ./tm.py --report docs/basic_template.md | pandoc -f markdown -t html > tm/report.html 34 | ./tm.py --dfd | dot -Tpng -o tm/dfd.png 35 | ./tm.py --seq | java -Djava.awt.headless=true -jar $PLANTUML_PATH -tpng -pipe > tm/seq.png 36 | ``` 37 | 38 | There's also an example `Makefile` that wraps all these into targets that can be easily shared for multiple models. If you have [GNU make](https://www.gnu.org/software/make/) installed (available by default on Linux distros but not on OSX), simply run: 39 | 40 | ``` 41 | make MODEL=the_name_of_your_model_minus_.py 42 | ``` 43 | 44 | You should either have plantuml.jar on the same directory as your model, or set PLANTUML_PATH. 45 | 46 | To avoid installing all the dependencies, like `pandoc` or `Java`, the script can be run inside a container: 47 | 48 | ``` 49 | # do this only once 50 | export USE_DOCKER=true 51 | make image 52 | 53 | # call this after every change in your model 54 | make 55 | ``` 56 | 57 | ### Getting strated - devbox variant 58 | 59 | To simplify the usage of `pytm` host dependencies can be completely isolated 60 | using [`devbox`](https://github.com/jetify-com/devbox). This is usually a 61 | lower overhead and more convenient alternative to the OCI container approach. 62 | 63 | - Install devbox on Linux/MacOS: `curl -fsSL https://get.jetify.com/devbox | bash` 64 | - Install devbox on [Windows/WSL2](https://www.jetify.com/docs/devbox/installing_devbox/?install-method=wsl) 65 | - update to latest version: `devbox version update` 66 | - `devbox shell` 67 | - `which python` -> `.devbox/nix/profile/default/bin/python` 68 | - `./tm.py --dfd | dot -Tpng -o sample.png` -> `sample.png` 69 | - `exit` 70 | 71 | ## Usage 72 | 73 | All available arguments: 74 | 75 | ```text 76 | usage: tm.py [-h] [--sqldump SQLDUMP] [--debug] [--dfd] [--report REPORT] 77 | [--exclude EXCLUDE] [--seq] [--list] [--describe DESCRIBE] 78 | [--list-elements] [--json JSON] [--levels LEVELS [LEVELS ...]] 79 | [--stale_days STALE_DAYS] 80 | 81 | optional arguments: 82 | -h, --help show this help message and exit 83 | --sqldump SQLDUMP dumps all threat model elements and findings into the 84 | named sqlite file (erased if exists) 85 | --debug print debug messages 86 | --dfd output DFD 87 | --report REPORT output report using the named template file (sample 88 | template file is under docs/template.md) 89 | --exclude EXCLUDE specify threat IDs to be ignored 90 | --seq output sequential diagram 91 | --list list all available threats 92 | --colormap color the risk in the diagram 93 | --describe DESCRIBE describe the properties available for a given element 94 | --list-elements list all elements which can be part of a threat model 95 | --json JSON output a JSON file 96 | --levels LEVELS [LEVELS ...] 97 | Select levels to be drawn in the threat model (int 98 | separated by comma). 99 | --stale_days STALE_DAYS 100 | checks if the delta between the TM script and the code 101 | described by it is bigger than the specified value in 102 | days 103 | ``` 104 | 105 | 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. 106 | 107 | Currently available elements are: TM, Element, Server, ExternalEntity, Datastore, Actor, Process, SetOfProcesses, Dataflow, Boundary and Lambda. 108 | 109 | The available properties of an element can be listed by using `--describe` followed by the name of an element: 110 | 111 | ```text 112 | 113 | (pytm) ➜ pytm git:(master) ✗ ./tm.py --describe Element 114 | Element class attributes: 115 | OS 116 | definesConnectionTimeout default: False 117 | description 118 | handlesResources default: False 119 | implementsAuthenticationScheme default: False 120 | implementsNonce default: False 121 | inBoundary 122 | inScope Is the element in scope of the threat model, default: True 123 | isAdmin default: False 124 | isHardened default: False 125 | name required 126 | onAWS default: False 127 | 128 | ``` 129 | 130 | The *colormap* argument, used together with *dfd*, outputs a color-coded DFD where the elements are painted red, yellow or green depending on their risk level (as identified by running the rules). 131 | 132 | 133 | ## Usage - devbox variant 134 | 135 | - `devbox shell` 136 | - `pytm` usage as usual 137 | - `exit` 138 | 139 | ## Creating a Threat Model 140 | 141 | The following is a sample `tm.py` file that describes a simple application where a User logs into the application 142 | and posts comments on the app. The app server stores those comments into the database. There is an AWS Lambda 143 | that periodically cleans the Database. 144 | 145 | ```python 146 | 147 | #!/usr/bin/env python3 148 | 149 | from pytm.pytm import TM, Server, Datastore, Dataflow, Boundary, Actor, Lambda, Data, Classification 150 | 151 | tm = TM("my test tm") 152 | tm.description = "another test tm" 153 | tm.isOrdered = True 154 | 155 | User_Web = Boundary("User/Web") 156 | Web_DB = Boundary("Web/DB") 157 | 158 | user = Actor("User") 159 | user.inBoundary = User_Web 160 | 161 | web = Server("Web Server") 162 | web.OS = "CloudOS" 163 | web.isHardened = True 164 | web.sourceCode = "server/web.cc" 165 | 166 | db = Datastore("SQL Database (*)") 167 | db.OS = "CentOS" 168 | db.isHardened = False 169 | db.inBoundary = Web_DB 170 | db.isSql = True 171 | db.inScope = False 172 | db.sourceCode = "model/schema.sql" 173 | 174 | comments = Data( 175 | name="Comments", 176 | description="Comments in HTML or Markdown", 177 | classification=Classification.PUBLIC, 178 | isPII=False, 179 | isCredentials=False, 180 | # credentialsLife=Lifetime.LONG, 181 | isStored=True, 182 | isSourceEncryptedAtRest=False, 183 | isDestEncryptedAtRest=True 184 | ) 185 | 186 | results = Data( 187 | name="results", 188 | description="Results of insert op", 189 | classification=Classification.SENSITIVE, 190 | isPII=False, 191 | isCredentials=False, 192 | # credentialsLife=Lifetime.LONG, 193 | isStored=True, 194 | isSourceEncryptedAtRest=False, 195 | isDestEncryptedAtRest=True 196 | ) 197 | 198 | my_lambda = Lambda("cleanDBevery6hours") 199 | my_lambda.hasAccessControl = True 200 | my_lambda.inBoundary = Web_DB 201 | 202 | my_lambda_to_db = Dataflow(my_lambda, db, "(λ)Periodically cleans DB") 203 | my_lambda_to_db.protocol = "SQL" 204 | my_lambda_to_db.dstPort = 3306 205 | 206 | user_to_web = Dataflow(user, web, "User enters comments (*)") 207 | user_to_web.protocol = "HTTP" 208 | user_to_web.dstPort = 80 209 | user_to_web.data = comments 210 | 211 | web_to_user = Dataflow(web, user, "Comments saved (*)") 212 | web_to_user.protocol = "HTTP" 213 | 214 | web_to_db = Dataflow(web, db, "Insert query with comments") 215 | web_to_db.protocol = "MySQL" 216 | web_to_db.dstPort = 3306 217 | 218 | db_to_web = Dataflow(db, web, "Comments contents") 219 | db_to_web.protocol = "MySQL" 220 | db_to_web.data = results 221 | 222 | tm.process() 223 | 224 | ``` 225 | 226 | You also have the option of using [pytmGPT](https://chat.openai.com/g/g-soISG24ix-pytmgpt) to create your models from prose! 227 | 228 | ### Generating Diagrams 229 | 230 | Diagrams are output as [Dot](https://graphviz.gitlab.io/) and [PlantUML](https://plantuml.com/). 231 | 232 | When `--dfd` argument is passed to the above `tm.py` file it generates output to stdout, which is fed to Graphviz's dot to generate the Data Flow Diagram: 233 | 234 | ```bash 235 | 236 | tm.py --dfd | dot -Tpng -o sample.png 237 | 238 | ``` 239 | 240 | Generates this diagram: 241 | 242 | ![dfd.png](.gitbook/assets/dfd.png) 243 | 244 | Adding ".levels = [1,2]" attributes to an element will cause it (and its associated Dataflows if both flow endings are in the same DFD level) to render (or not) depending on the command argument "--levels 1 2". 245 | 246 | The following command generates a Sequence diagram. 247 | 248 | ```bash 249 | 250 | tm.py --seq | java -Djava.awt.headless=true -jar plantuml.jar -tpng -pipe > seq.png 251 | 252 | ``` 253 | 254 | Generates this diagram: 255 | 256 | ![seq.png](.gitbook/assets/seq.png) 257 | 258 | ### Creating a Report 259 | 260 | The diagrams and findings can be included in the template to create a final report: 261 | 262 | ```bash 263 | 264 | tm.py --report docs/basic_template.md | pandoc -f markdown -t html > report.html 265 | 266 | ``` 267 | The templating format used in the report template is very simple: 268 | 269 | ```text 270 | 271 | # Threat Model Sample 272 | *** 273 | 274 | ## System Description 275 | 276 | {tm.description} 277 | 278 | ## Dataflow Diagram 279 | 280 | ![Level 0 DFD](dfd.png) 281 | 282 | ## Dataflows 283 | 284 | Name|From|To |Data|Protocol|Port 285 | ----|----|---|----|--------|---- 286 | {dataflows:repeat:{{item.name}}|{{item.source.name}}|{{item.sink.name}}|{{item.data}}|{{item.protocol}}|{{item.dstPort}} 287 | } 288 | 289 | ## Findings 290 | 291 | {findings:repeat:* {{item.description}} on element "{{item.target}}" 292 | } 293 | 294 | ``` 295 | 296 | To group findings by elements, use a more advanced, nested loop: 297 | 298 | ```text 299 | ## Findings 300 | 301 | {elements:repeat:{{item.findings:if: 302 | ### {{item.name}} 303 | 304 | {{item.findings:repeat: 305 | **Threat**: {{{{item.id}}}} - {{{{item.description}}}} 306 | 307 | **Severity**: {{{{item.severity}}}} 308 | 309 | **Mitigations**: {{{{item.mitigations}}}} 310 | 311 | **References**: {{{{item.references}}}} 312 | 313 | }}}}} 314 | ``` 315 | 316 | All items inside a loop must be escaped, doubling the braces, so `{item.name}` becomes `{{item.name}}`. 317 | The example above uses two nested loops, so items in the inner loop must be escaped twice, that's why they're using four braces. 318 | 319 | ### Overrides 320 | 321 | You can override attributes of findings (threats matching the model assets and/or dataflows), for example to set a custom CVSS score and/or response text: 322 | 323 | ```python 324 | user_to_web = Dataflow(user, web, "User enters comments (*)", protocol="HTTP", dstPort="80") 325 | user_to_web.overrides = [ 326 | Finding( 327 | # Overflow Buffers 328 | threat_id="INP02", 329 | cvss="9.3", 330 | response="""**To Mitigate**: run a memory sanitizer to validate the binary""", 331 | severity="Very High", 332 | ) 333 | ] 334 | ``` 335 | 336 | If you are adding a Finding, make sure to add a severity: "Very High", "High", "Medium", "Low", "Very Low". 337 | 338 | ## Threats database 339 | 340 | For the security practitioner, you may supply your own threats file by setting `TM.threatsFile`. It should contain entries like: 341 | 342 | ```json 343 | { 344 | "SID":"INP01", 345 | "target": ["Lambda","Process"], 346 | "description": "Buffer Overflow via Environment Variables", 347 | "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.", 348 | "Likelihood Of Attack": "High", 349 | "severity": "High", 350 | "condition": "target.usesEnvironmentVariables is True and target.controls.sanitizesInput is False and target.controls.checksInputBounds is False", 351 | "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.", 352 | "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.", 353 | "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.", 354 | "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" 355 | } 356 | ``` 357 | 358 | The `target` field lists classes of model elements to match this threat against. 359 | Those can be assets, like: Actor, Datastore, Server, Process, SetOfProcesses, ExternalEntity, 360 | Lambda or Element, which is the base class and matches any. It can also be a Dataflow that connects two assets. 361 | 362 | All other fields (except `condition`) are available for display and can be used in the template 363 | to list findings in the final [report](#report). 364 | 365 | > **WARNING** 366 | > 367 | > The `threats.json` file contains strings that run through `eval()`. Make sure the file has correct permissions 368 | > or risk having an attacker change the strings and cause you to run code on their behalf. 369 | 370 | The logic lives in the `condition`, where members of `target` can be logically evaluated. 371 | Returning a true means the rule generates a finding, otherwise, it is not a finding. 372 | Condition may compare attributes of `target` and/or control attributes of the 'target.control' and also call one of these methods: 373 | 374 | * `target.oneOf(class, ...)` where `class` is one or more: Actor, Datastore, Server, Process, SetOfProcesses, ExternalEntity, Lambda or Dataflow, 375 | * `target.crosses(Boundary)`, 376 | * `target.enters(Boundary)`, 377 | * `target.exits(Boundary)`, 378 | * `target.inside(Boundary)`. 379 | 380 | If `target` is a Dataflow, remember you can access `target.source` and/or `target.sink` along with other attributes. 381 | 382 | Conditions on assets can analyze all incoming and outgoing Dataflows by inspecting 383 | the `target.input` and `target.output` attributes. For example, to match a threat only against 384 | servers with incoming traffic, use `any(target.inputs)`. A more advanced example, 385 | matching elements connecting to SQL datastores, would be `any(f.sink.oneOf(Datastore) and f.sink.isSQL for f in target.outputs)`. 386 | 387 | ## Importing from JSON 388 | 389 | With a little bit of Python code it is possible to import a threat model from JSON (notice the special format in the exmaple found in `tests/input.json`). The following example imports the `input.json` example found in tests. Save the following code as `tm2.py`. 390 | 391 | ```python 392 | 393 | #!/usr/bin/env python3 394 | # Example tm2.py contents 395 | # Run: python tm2.py --dfd | dot -Tpng -o sample_json.png 396 | 397 | from pytm import ( 398 | TM, 399 | Actor, 400 | Boundary, 401 | Classification, 402 | Data, 403 | Dataflow, 404 | Datastore, 405 | Lambda, 406 | Server, 407 | DatastoreType, 408 | Assumption, 409 | load, 410 | ) 411 | 412 | json_file_string = './tests/input.json' 413 | with open(json_file_string) as input_json: 414 | TM.reset() 415 | tm = load(input_json) 416 | tm.process() 417 | 418 | ``` 419 | 420 | We can call `tm2.py` the same way as we did before, here with `--dfd` and then redirect the output to Graphviz (`dot`): 421 | 422 | ```bash 423 | 424 | python tm2.py --dfd | dot -Tpng -o sample_json.png 425 | 426 | ``` 427 | 428 | ## Making slides! 429 | 430 | Once a threat model is done and ready, the dreaded presentation stage comes in - and now pytm can help you there as well, with a template that expresses your threat model in slides, using the power of (RevealMD)[https://github.com/webpro/reveal-md]! Just use the template docs/revealjs.md and you will get some pretty slides, fully configurable, that you can present and share from your browser. 431 | 432 | 433 | 434 | https://github.com/izar/pytm/assets/368769/30218241-c7cc-4085-91e9-bbec2843f838 435 | 436 | 437 | 438 | ## Currently supported threats 439 | 440 | ```text 441 | INP01 - Buffer Overflow via Environment Variables 442 | INP02 - Overflow Buffers 443 | INP03 - Server Side Include (SSI) Injection 444 | CR01 - Session Sidejacking 445 | INP04 - HTTP Request Splitting 446 | CR02 - Cross Site Tracing 447 | INP05 - Command Line Execution through SQL Injection 448 | INP06 - SQL Injection through SOAP Parameter Tampering 449 | SC01 - JSON Hijacking (aka JavaScript Hijacking) 450 | LB01 - API Manipulation 451 | AA01 - Authentication Abuse/ByPass 452 | DS01 - Excavation 453 | DE01 - Interception 454 | DE02 - Double Encoding 455 | API01 - Exploit Test APIs 456 | AC01 - Privilege Abuse 457 | INP07 - Buffer Manipulation 458 | AC02 - Shared Data Manipulation 459 | DO01 - Flooding 460 | HA01 - Path Traversal 461 | AC03 - Subverting Environment Variable Values 462 | DO02 - Excessive Allocation 463 | DS02 - Try All Common Switches 464 | INP08 - Format String Injection 465 | INP09 - LDAP Injection 466 | INP10 - Parameter Injection 467 | INP11 - Relative Path Traversal 468 | INP12 - Client-side Injection-induced Buffer Overflow 469 | AC04 - XML Schema Poisoning 470 | DO03 - XML Ping of the Death 471 | AC05 - Content Spoofing 472 | INP13 - Command Delimiters 473 | INP14 - Input Data Manipulation 474 | DE03 - Sniffing Attacks 475 | CR03 - Dictionary-based Password Attack 476 | API02 - Exploit Script-Based APIs 477 | HA02 - White Box Reverse Engineering 478 | DS03 - Footprinting 479 | AC06 - Using Malicious Files 480 | HA03 - Web Application Fingerprinting 481 | SC02 - XSS Targeting Non-Script Elements 482 | AC07 - Exploiting Incorrectly Configured Access Control Security Levels 483 | INP15 - IMAP/SMTP Command Injection 484 | HA04 - Reverse Engineering 485 | SC03 - Embedding Scripts within Scripts 486 | INP16 - PHP Remote File Inclusion 487 | AA02 - Principal Spoof 488 | CR04 - Session Credential Falsification through Forging 489 | DO04 - XML Entity Expansion 490 | DS04 - XSS Targeting Error Pages 491 | SC04 - XSS Using Alternate Syntax 492 | CR05 - Encryption Brute Forcing 493 | AC08 - Manipulate Registry Information 494 | DS05 - Lifting Sensitive Data Embedded in Cache 495 | SC05 - Removing Important Client Functionality 496 | INP17 - XSS Using MIME Type Mismatch 497 | AA03 - Exploitation of Trusted Credentials 498 | AC09 - Functionality Misuse 499 | INP18 - Fuzzing and observing application log data/errors for application mapping 500 | CR06 - Communication Channel Manipulation 501 | AC10 - Exploiting Incorrectly Configured SSL 502 | CR07 - XML Routing Detour Attacks 503 | AA04 - Exploiting Trust in Client 504 | CR08 - Client-Server Protocol Manipulation 505 | INP19 - XML External Entities Blowup 506 | INP20 - iFrame Overlay 507 | AC11 - Session Credential Falsification through Manipulation 508 | INP21 - DTD Injection 509 | INP22 - XML Attribute Blowup 510 | INP23 - File Content Injection 511 | DO05 - XML Nested Payloads 512 | AC12 - Privilege Escalation 513 | AC13 - Hijacking a privileged process 514 | AC14 - Catching exception throw/signal from privileged block 515 | INP24 - Filter Failure through Buffer Overflow 516 | INP25 - Resource Injection 517 | INP26 - Code Injection 518 | INP27 - XSS Targeting HTML Attributes 519 | INP28 - XSS Targeting URI Placeholders 520 | INP29 - XSS Using Doubled Characters 521 | INP30 - XSS Using Invalid Characters 522 | INP31 - Command Injection 523 | INP32 - XML Injection 524 | INP33 - Remote Code Inclusion 525 | INP34 - SOAP Array Overflow 526 | INP35 - Leverage Alternate Encoding 527 | DE04 - Audit Log Manipulation 528 | AC15 - Schema Poisoning 529 | INP36 - HTTP Response Smuggling 530 | INP37 - HTTP Request Smuggling 531 | INP38 - DOM-Based XSS 532 | AC16 - Session Credential Falsification through Prediction 533 | INP39 - Reflected XSS 534 | INP40 - Stored XSS 535 | AC17 - Session Hijacking - ServerSide 536 | AC18 - Session Hijacking - ClientSide 537 | INP41 - Argument Injection 538 | AC19 - Reusing Session IDs (aka Session Replay) - ServerSide 539 | AC20 - Reusing Session IDs (aka Session Replay) - ClientSide 540 | AC21 - Cross Site Request Forgery 541 | 542 | 543 | 544 | ``` 545 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "black" 5 | version = "25.11.0" 6 | description = "The uncompromising code formatter." 7 | optional = false 8 | python-versions = ">=3.9" 9 | groups = ["dev"] 10 | files = [ 11 | {file = "black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e"}, 12 | {file = "black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0"}, 13 | {file = "black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37"}, 14 | {file = "black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03"}, 15 | {file = "black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a"}, 16 | {file = "black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170"}, 17 | {file = "black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc"}, 18 | {file = "black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e"}, 19 | {file = "black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac"}, 20 | {file = "black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96"}, 21 | {file = "black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd"}, 22 | {file = "black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409"}, 23 | {file = "black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b"}, 24 | {file = "black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd"}, 25 | {file = "black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993"}, 26 | {file = "black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c"}, 27 | {file = "black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170"}, 28 | {file = "black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545"}, 29 | {file = "black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda"}, 30 | {file = "black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664"}, 31 | {file = "black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06"}, 32 | {file = "black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2"}, 33 | {file = "black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc"}, 34 | {file = "black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc"}, 35 | {file = "black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b"}, 36 | {file = "black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08"}, 37 | ] 38 | 39 | [package.dependencies] 40 | click = ">=8.0.0" 41 | mypy-extensions = ">=0.4.3" 42 | packaging = ">=22.0" 43 | pathspec = ">=0.9.0" 44 | platformdirs = ">=2" 45 | pytokens = ">=0.3.0" 46 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 47 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 48 | 49 | [package.extras] 50 | colorama = ["colorama (>=0.4.3)"] 51 | d = ["aiohttp (>=3.10)"] 52 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 53 | uvloop = ["uvloop (>=0.15.2)"] 54 | 55 | [[package]] 56 | name = "click" 57 | version = "8.1.8" 58 | description = "Composable command line interface toolkit" 59 | optional = false 60 | python-versions = ">=3.7" 61 | groups = ["dev"] 62 | files = [ 63 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, 64 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, 65 | ] 66 | 67 | [package.dependencies] 68 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 69 | 70 | [[package]] 71 | name = "colorama" 72 | version = "0.4.6" 73 | description = "Cross-platform colored terminal text." 74 | optional = false 75 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 76 | groups = ["dev"] 77 | markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" 78 | files = [ 79 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 80 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 81 | ] 82 | 83 | [[package]] 84 | name = "exceptiongroup" 85 | version = "1.3.0" 86 | description = "Backport of PEP 654 (exception groups)" 87 | optional = false 88 | python-versions = ">=3.7" 89 | groups = ["dev"] 90 | markers = "python_version < \"3.11\"" 91 | files = [ 92 | {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, 93 | {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, 94 | ] 95 | 96 | [package.dependencies] 97 | typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} 98 | 99 | [package.extras] 100 | test = ["pytest (>=6)"] 101 | 102 | [[package]] 103 | name = "importlib-metadata" 104 | version = "8.7.0" 105 | description = "Read metadata from Python packages" 106 | optional = false 107 | python-versions = ">=3.9" 108 | groups = ["dev"] 109 | markers = "python_version == \"3.9\"" 110 | files = [ 111 | {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, 112 | {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, 113 | ] 114 | 115 | [package.dependencies] 116 | zipp = ">=3.20" 117 | 118 | [package.extras] 119 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] 120 | cover = ["pytest-cov"] 121 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 122 | enabler = ["pytest-enabler (>=2.2)"] 123 | perf = ["ipython"] 124 | test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] 125 | type = ["pytest-mypy"] 126 | 127 | [[package]] 128 | name = "iniconfig" 129 | version = "2.1.0" 130 | description = "brain-dead simple config-ini parsing" 131 | optional = false 132 | python-versions = ">=3.8" 133 | groups = ["dev"] 134 | files = [ 135 | {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, 136 | {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, 137 | ] 138 | 139 | [[package]] 140 | name = "legacy-cgi" 141 | version = "2.6.4" 142 | description = "Fork of the standard library cgi and cgitb modules removed in Python 3.13" 143 | optional = false 144 | python-versions = ">=3.8" 145 | groups = ["main"] 146 | markers = "python_version >= \"3.13\"" 147 | files = [ 148 | {file = "legacy_cgi-2.6.4-py3-none-any.whl", hash = "sha256:7e235ce58bf1e25d1fc9b2d299015e4e2cd37305eccafec1e6bac3fc04b878cd"}, 149 | {file = "legacy_cgi-2.6.4.tar.gz", hash = "sha256:abb9dfc7835772f7c9317977c63253fd22a7484b5c9bbcdca60a29dcce97c577"}, 150 | ] 151 | 152 | [[package]] 153 | name = "mako" 154 | version = "1.3.10" 155 | description = "A super-fast templating language that borrows the best ideas from the existing templating languages." 156 | optional = false 157 | python-versions = ">=3.8" 158 | groups = ["dev"] 159 | files = [ 160 | {file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"}, 161 | {file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"}, 162 | ] 163 | 164 | [package.dependencies] 165 | MarkupSafe = ">=0.9.2" 166 | 167 | [package.extras] 168 | babel = ["Babel"] 169 | lingua = ["lingua"] 170 | testing = ["pytest"] 171 | 172 | [[package]] 173 | name = "markdown" 174 | version = "3.9" 175 | description = "Python implementation of John Gruber's Markdown." 176 | optional = false 177 | python-versions = ">=3.9" 178 | groups = ["dev"] 179 | files = [ 180 | {file = "markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280"}, 181 | {file = "markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a"}, 182 | ] 183 | 184 | [package.dependencies] 185 | importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} 186 | 187 | [package.extras] 188 | docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] 189 | testing = ["coverage", "pyyaml"] 190 | 191 | [[package]] 192 | name = "markupsafe" 193 | version = "3.0.3" 194 | description = "Safely add untrusted strings to HTML/XML markup." 195 | optional = false 196 | python-versions = ">=3.9" 197 | groups = ["dev"] 198 | files = [ 199 | {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, 200 | {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, 201 | {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, 202 | {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, 203 | {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, 204 | {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, 205 | {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, 206 | {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, 207 | {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, 208 | {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, 209 | {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, 210 | {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, 211 | {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, 212 | {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, 213 | {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, 214 | {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, 215 | {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, 216 | {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, 217 | {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, 218 | {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, 219 | {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, 220 | {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, 221 | {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, 222 | {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, 223 | {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, 224 | {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, 225 | {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, 226 | {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, 227 | {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, 228 | {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, 229 | {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, 230 | {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, 231 | {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, 232 | {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, 233 | {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, 234 | {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, 235 | {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, 236 | {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, 237 | {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, 238 | {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, 239 | {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, 240 | {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, 241 | {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, 242 | {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, 243 | {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, 244 | {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, 245 | {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, 246 | {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, 247 | {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, 248 | {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, 249 | {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, 250 | {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, 251 | {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, 252 | {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, 253 | {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, 254 | {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, 255 | {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, 256 | {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, 257 | {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, 258 | {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, 259 | {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, 260 | {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, 261 | {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, 262 | {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, 263 | {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, 264 | {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, 265 | {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, 266 | {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, 267 | {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, 268 | {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, 269 | {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, 270 | {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, 271 | {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, 272 | {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, 273 | {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, 274 | {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, 275 | {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, 276 | {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, 277 | {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, 278 | {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, 279 | {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, 280 | {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, 281 | {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, 282 | {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, 283 | {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, 284 | {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, 285 | {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, 286 | {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, 287 | {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, 288 | ] 289 | 290 | [[package]] 291 | name = "mypy-extensions" 292 | version = "1.1.0" 293 | description = "Type system extensions for programs checked with the mypy type checker." 294 | optional = false 295 | python-versions = ">=3.8" 296 | groups = ["dev"] 297 | files = [ 298 | {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, 299 | {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, 300 | ] 301 | 302 | [[package]] 303 | name = "packaging" 304 | version = "25.0" 305 | description = "Core utilities for Python packages" 306 | optional = false 307 | python-versions = ">=3.8" 308 | groups = ["dev"] 309 | files = [ 310 | {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, 311 | {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, 312 | ] 313 | 314 | [[package]] 315 | name = "pathspec" 316 | version = "0.12.1" 317 | description = "Utility library for gitignore style pattern matching of file paths." 318 | optional = false 319 | python-versions = ">=3.8" 320 | groups = ["dev"] 321 | files = [ 322 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 323 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 324 | ] 325 | 326 | [[package]] 327 | name = "pdoc3" 328 | version = "0.11.6" 329 | description = "Auto-generate API documentation for Python projects." 330 | optional = false 331 | python-versions = ">=3.9" 332 | groups = ["dev"] 333 | files = [ 334 | {file = "pdoc3-0.11.6-py3-none-any.whl", hash = "sha256:8b72723767bd48d899812d2aec8375fc1c3476e179455db0b4575e6dccb44b93"}, 335 | {file = "pdoc3-0.11.6.tar.gz", hash = "sha256:1ea5e84b87a754d191fb64bf5e517ca6c50d0d84a614c1efecf6b46d290ae387"}, 336 | ] 337 | 338 | [package.dependencies] 339 | mako = "*" 340 | markdown = ">=3.0" 341 | 342 | [[package]] 343 | name = "platformdirs" 344 | version = "4.4.0" 345 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 346 | optional = false 347 | python-versions = ">=3.9" 348 | groups = ["dev"] 349 | files = [ 350 | {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, 351 | {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, 352 | ] 353 | 354 | [package.extras] 355 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] 356 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] 357 | type = ["mypy (>=1.14.1)"] 358 | 359 | [[package]] 360 | name = "pluggy" 361 | version = "1.6.0" 362 | description = "plugin and hook calling mechanisms for python" 363 | optional = false 364 | python-versions = ">=3.9" 365 | groups = ["dev"] 366 | files = [ 367 | {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, 368 | {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, 369 | ] 370 | 371 | [package.extras] 372 | dev = ["pre-commit", "tox"] 373 | testing = ["coverage", "pytest", "pytest-benchmark"] 374 | 375 | [[package]] 376 | name = "pydal" 377 | version = "20200714.1" 378 | description = "a pure Python Database Abstraction Layer (for python version 2.7 and 3.x)" 379 | optional = false 380 | python-versions = "*" 381 | groups = ["main"] 382 | files = [ 383 | {file = "pydal-20200714.1.tar.gz", hash = "sha256:dd35b8ecb009099cce7efa72a40707d2e9bdcdf85924f30683a52d5172d1242f"}, 384 | ] 385 | 386 | [[package]] 387 | name = "pygments" 388 | version = "2.19.2" 389 | description = "Pygments is a syntax highlighting package written in Python." 390 | optional = false 391 | python-versions = ">=3.8" 392 | groups = ["dev"] 393 | files = [ 394 | {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, 395 | {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, 396 | ] 397 | 398 | [package.extras] 399 | windows-terminal = ["colorama (>=0.4.6)"] 400 | 401 | [[package]] 402 | name = "pytest" 403 | version = "8.4.2" 404 | description = "pytest: simple powerful testing with Python" 405 | optional = false 406 | python-versions = ">=3.9" 407 | groups = ["dev"] 408 | files = [ 409 | {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, 410 | {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, 411 | ] 412 | 413 | [package.dependencies] 414 | colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} 415 | exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} 416 | iniconfig = ">=1" 417 | packaging = ">=20" 418 | pluggy = ">=1.5,<2" 419 | pygments = ">=2.7.2" 420 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 421 | 422 | [package.extras] 423 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] 424 | 425 | [[package]] 426 | name = "pytokens" 427 | version = "0.3.0" 428 | description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." 429 | optional = false 430 | python-versions = ">=3.8" 431 | groups = ["dev"] 432 | files = [ 433 | {file = "pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3"}, 434 | {file = "pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a"}, 435 | ] 436 | 437 | [package.extras] 438 | dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] 439 | 440 | [[package]] 441 | name = "tomli" 442 | version = "2.3.0" 443 | description = "A lil' TOML parser" 444 | optional = false 445 | python-versions = ">=3.8" 446 | groups = ["dev"] 447 | markers = "python_version < \"3.11\"" 448 | files = [ 449 | {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, 450 | {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, 451 | {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, 452 | {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, 453 | {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, 454 | {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, 455 | {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, 456 | {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, 457 | {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, 458 | {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, 459 | {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, 460 | {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, 461 | {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, 462 | {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, 463 | {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, 464 | {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, 465 | {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, 466 | {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, 467 | {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, 468 | {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, 469 | {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, 470 | {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, 471 | {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, 472 | {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, 473 | {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, 474 | {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, 475 | {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, 476 | {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, 477 | {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, 478 | {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, 479 | {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, 480 | {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, 481 | {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, 482 | {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, 483 | {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, 484 | {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, 485 | {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, 486 | {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, 487 | {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, 488 | {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, 489 | {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, 490 | {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, 491 | ] 492 | 493 | [[package]] 494 | name = "typing-extensions" 495 | version = "4.15.0" 496 | description = "Backported and Experimental Type Hints for Python 3.9+" 497 | optional = false 498 | python-versions = ">=3.9" 499 | groups = ["dev"] 500 | markers = "python_version < \"3.11\"" 501 | files = [ 502 | {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, 503 | {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, 504 | ] 505 | 506 | [[package]] 507 | name = "zipp" 508 | version = "3.23.0" 509 | description = "Backport of pathlib-compatible object wrapper for zip files" 510 | optional = false 511 | python-versions = ">=3.9" 512 | groups = ["dev"] 513 | markers = "python_version == \"3.9\"" 514 | files = [ 515 | {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, 516 | {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, 517 | ] 518 | 519 | [package.extras] 520 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] 521 | cover = ["pytest-cov"] 522 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 523 | enabler = ["pytest-enabler (>=2.2)"] 524 | test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] 525 | type = ["pytest-mypy"] 526 | 527 | [metadata] 528 | lock-version = "2.1" 529 | python-versions = "^3.9 || ^3.10 || ^3.11" 530 | content-hash = "d13ccd9a0de456c987bd6c6f20034c2f2a71279f65ddd4b2a0d597ef5ca5fd86" 531 | -------------------------------------------------------------------------------- /tests/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "actors": [ 3 | { 4 | "__class__": "Actor", 5 | "assumptions": [], 6 | "controls": { 7 | "authenticatesDestination": false, 8 | "authenticatesSource": false, 9 | "authenticationScheme": "", 10 | "authorizesSource": false, 11 | "checksDestinationRevocation": false, 12 | "checksInputBounds": false, 13 | "definesConnectionTimeout": false, 14 | "disablesDTD": false, 15 | "disablesiFrames": false, 16 | "encodesHeaders": false, 17 | "encodesOutput": false, 18 | "encryptsCookies": false, 19 | "encryptsSessionData": false, 20 | "handlesCrashes": false, 21 | "handlesInterruptions": false, 22 | "handlesResourceConsumption": false, 23 | "hasAccessControl": false, 24 | "implementsAuthenticationScheme": false, 25 | "implementsCSRFToken": false, 26 | "implementsNonce": false, 27 | "implementsPOLP": false, 28 | "implementsServerSideValidation": false, 29 | "implementsStrictHTTPValidation": false, 30 | "invokesScriptFilters": false, 31 | "isEncrypted": false, 32 | "isEncryptedAtRest": false, 33 | "isHardened": false, 34 | "isResilient": false, 35 | "providesConfidentiality": false, 36 | "providesIntegrity": false, 37 | "sanitizesInput": false, 38 | "tracksExecutionFlow": false, 39 | "usesCodeSigning": false, 40 | "usesEncryptionAlgorithm": "", 41 | "usesMFA": false, 42 | "usesParameterizedInput": false, 43 | "usesSecureFunctions": false, 44 | "usesStrongSessionIdentifiers": false, 45 | "usesVPN": false, 46 | "validatesContentType": false, 47 | "validatesHeaders": false, 48 | "validatesInput": false, 49 | "verifySessionIdentifiers": false 50 | }, 51 | "data": [], 52 | "description": "", 53 | "findings": [], 54 | "inBoundary": "Internet", 55 | "inScope": true, 56 | "inputs": [ 57 | "Show comments (*)" 58 | ], 59 | "isAdmin": false, 60 | "levels": [ 61 | 0 62 | ], 63 | "maxClassification": "Classification.UNKNOWN", 64 | "minTLSVersion": "TLSVersion.NONE", 65 | "name": "User", 66 | "outputs": [ 67 | "User enters comments (*)" 68 | ], 69 | "overrides": [], 70 | "port": -1, 71 | "protocol": "", 72 | "severity": 0, 73 | "sourceFiles": [] 74 | } 75 | ], 76 | "assets": [ 77 | { 78 | "OS": "", 79 | "__class__": "Server", 80 | "assumptions": [], 81 | "controls": { 82 | "authenticatesDestination": false, 83 | "authenticatesSource": false, 84 | "authenticationScheme": "", 85 | "authorizesSource": false, 86 | "checksDestinationRevocation": false, 87 | "checksInputBounds": false, 88 | "definesConnectionTimeout": false, 89 | "disablesDTD": false, 90 | "disablesiFrames": false, 91 | "encodesHeaders": false, 92 | "encodesOutput": false, 93 | "encryptsCookies": false, 94 | "encryptsSessionData": false, 95 | "handlesCrashes": false, 96 | "handlesInterruptions": false, 97 | "handlesResourceConsumption": false, 98 | "hasAccessControl": false, 99 | "implementsAuthenticationScheme": false, 100 | "implementsCSRFToken": false, 101 | "implementsNonce": false, 102 | "implementsPOLP": false, 103 | "implementsServerSideValidation": false, 104 | "implementsStrictHTTPValidation": false, 105 | "invokesScriptFilters": false, 106 | "isEncrypted": false, 107 | "isEncryptedAtRest": false, 108 | "isHardened": false, 109 | "isResilient": false, 110 | "providesConfidentiality": false, 111 | "providesIntegrity": false, 112 | "sanitizesInput": false, 113 | "tracksExecutionFlow": false, 114 | "usesCodeSigning": false, 115 | "usesEncryptionAlgorithm": "", 116 | "usesMFA": false, 117 | "usesParameterizedInput": false, 118 | "usesSecureFunctions": false, 119 | "usesStrongSessionIdentifiers": false, 120 | "usesVPN": false, 121 | "validatesContentType": false, 122 | "validatesHeaders": false, 123 | "validatesInput": false, 124 | "verifySessionIdentifiers": false 125 | }, 126 | "data": [], 127 | "description": "", 128 | "findings": [], 129 | "handlesResources": false, 130 | "inBoundary": null, 131 | "inScope": true, 132 | "inputs": [ 133 | "User enters comments (*)", 134 | "Retrieve comments" 135 | ], 136 | "levels": [ 137 | 0 138 | ], 139 | "maxClassification": "Classification.UNKNOWN", 140 | "minTLSVersion": "TLSVersion.NONE", 141 | "name": "Web Server", 142 | "onAWS": false, 143 | "outputs": [ 144 | "Insert query with comments", 145 | "Call func", 146 | "Show comments (*)" 147 | ], 148 | "overrides": [], 149 | "port": -1, 150 | "protocol": "", 151 | "severity": 0, 152 | "sourceFiles": [], 153 | "usesCache": false, 154 | "usesEnvironmentVariables": false, 155 | "usesSessionTokens": false, 156 | "usesVPN": false, 157 | "usesXMLParser": false 158 | }, 159 | { 160 | "OS": "", 161 | "__class__": "Lambda", 162 | "assumptions": [], 163 | "controls": { 164 | "authenticatesDestination": false, 165 | "authenticatesSource": false, 166 | "authenticationScheme": "", 167 | "authorizesSource": false, 168 | "checksDestinationRevocation": false, 169 | "checksInputBounds": false, 170 | "definesConnectionTimeout": false, 171 | "disablesDTD": false, 172 | "disablesiFrames": false, 173 | "encodesHeaders": false, 174 | "encodesOutput": false, 175 | "encryptsCookies": false, 176 | "encryptsSessionData": false, 177 | "handlesCrashes": false, 178 | "handlesInterruptions": false, 179 | "handlesResourceConsumption": false, 180 | "hasAccessControl": false, 181 | "implementsAuthenticationScheme": false, 182 | "implementsCSRFToken": false, 183 | "implementsNonce": false, 184 | "implementsPOLP": false, 185 | "implementsServerSideValidation": false, 186 | "implementsStrictHTTPValidation": false, 187 | "invokesScriptFilters": false, 188 | "isEncrypted": false, 189 | "isEncryptedAtRest": false, 190 | "isHardened": false, 191 | "isResilient": false, 192 | "providesConfidentiality": false, 193 | "providesIntegrity": false, 194 | "sanitizesInput": false, 195 | "tracksExecutionFlow": false, 196 | "usesCodeSigning": false, 197 | "usesEncryptionAlgorithm": "", 198 | "usesMFA": false, 199 | "usesParameterizedInput": false, 200 | "usesSecureFunctions": false, 201 | "usesStrongSessionIdentifiers": false, 202 | "usesVPN": false, 203 | "validatesContentType": false, 204 | "validatesHeaders": false, 205 | "validatesInput": false, 206 | "verifySessionIdentifiers": false 207 | }, 208 | "data": [], 209 | "description": "", 210 | "environment": "", 211 | "findings": [], 212 | "handlesResources": false, 213 | "implementsAPI": false, 214 | "inBoundary": null, 215 | "inScope": true, 216 | "inputs": [ 217 | "Call func" 218 | ], 219 | "levels": [ 220 | 0 221 | ], 222 | "maxClassification": "Classification.UNKNOWN", 223 | "minTLSVersion": "TLSVersion.NONE", 224 | "name": "Lambda func", 225 | "onAWS": true, 226 | "outputs": [], 227 | "overrides": [], 228 | "port": -1, 229 | "protocol": "", 230 | "severity": 0, 231 | "sourceFiles": [], 232 | "usesEnvironmentVariables": false 233 | }, 234 | { 235 | "OS": "", 236 | "__class__": "Process", 237 | "allowsClientSideScripting": false, 238 | "assumptions": [], 239 | "codeType": "Unmanaged", 240 | "controls": { 241 | "authenticatesDestination": false, 242 | "authenticatesSource": false, 243 | "authenticationScheme": "", 244 | "authorizesSource": false, 245 | "checksDestinationRevocation": false, 246 | "checksInputBounds": false, 247 | "definesConnectionTimeout": false, 248 | "disablesDTD": false, 249 | "disablesiFrames": false, 250 | "encodesHeaders": false, 251 | "encodesOutput": false, 252 | "encryptsCookies": false, 253 | "encryptsSessionData": false, 254 | "handlesCrashes": false, 255 | "handlesInterruptions": false, 256 | "handlesResourceConsumption": false, 257 | "hasAccessControl": false, 258 | "implementsAuthenticationScheme": false, 259 | "implementsCSRFToken": false, 260 | "implementsNonce": false, 261 | "implementsPOLP": false, 262 | "implementsServerSideValidation": false, 263 | "implementsStrictHTTPValidation": false, 264 | "invokesScriptFilters": false, 265 | "isEncrypted": false, 266 | "isEncryptedAtRest": false, 267 | "isHardened": false, 268 | "isResilient": false, 269 | "providesConfidentiality": false, 270 | "providesIntegrity": false, 271 | "sanitizesInput": false, 272 | "tracksExecutionFlow": false, 273 | "usesCodeSigning": false, 274 | "usesEncryptionAlgorithm": "", 275 | "usesMFA": false, 276 | "usesParameterizedInput": false, 277 | "usesSecureFunctions": false, 278 | "usesStrongSessionIdentifiers": false, 279 | "usesVPN": false, 280 | "validatesContentType": false, 281 | "validatesHeaders": false, 282 | "validatesInput": false, 283 | "verifySessionIdentifiers": false 284 | }, 285 | "data": [], 286 | "description": "", 287 | "environment": "", 288 | "findings": [], 289 | "handlesResources": false, 290 | "implementsAPI": false, 291 | "implementsCommunicationProtocol": false, 292 | "inBoundary": null, 293 | "inScope": true, 294 | "inputs": [], 295 | "levels": [ 296 | 0 297 | ], 298 | "maxClassification": "Classification.UNKNOWN", 299 | "minTLSVersion": "TLSVersion.NONE", 300 | "name": "Task queue worker", 301 | "onAWS": false, 302 | "outputs": [ 303 | "Query for tasks" 304 | ], 305 | "overrides": [], 306 | "port": -1, 307 | "protocol": "", 308 | "severity": 0, 309 | "sourceFiles": [], 310 | "tracksExecutionFlow": false, 311 | "usesEnvironmentVariables": false 312 | }, 313 | { 314 | "OS": "", 315 | "__class__": "Datastore", 316 | "assumptions": [], 317 | "controls": { 318 | "authenticatesDestination": false, 319 | "authenticatesSource": false, 320 | "authenticationScheme": "", 321 | "authorizesSource": false, 322 | "checksDestinationRevocation": false, 323 | "checksInputBounds": false, 324 | "definesConnectionTimeout": false, 325 | "disablesDTD": false, 326 | "disablesiFrames": false, 327 | "encodesHeaders": false, 328 | "encodesOutput": false, 329 | "encryptsCookies": false, 330 | "encryptsSessionData": false, 331 | "handlesCrashes": false, 332 | "handlesInterruptions": false, 333 | "handlesResourceConsumption": false, 334 | "hasAccessControl": false, 335 | "implementsAuthenticationScheme": false, 336 | "implementsCSRFToken": false, 337 | "implementsNonce": false, 338 | "implementsPOLP": false, 339 | "implementsServerSideValidation": false, 340 | "implementsStrictHTTPValidation": false, 341 | "invokesScriptFilters": false, 342 | "isEncrypted": false, 343 | "isEncryptedAtRest": false, 344 | "isHardened": false, 345 | "isResilient": false, 346 | "providesConfidentiality": false, 347 | "providesIntegrity": false, 348 | "sanitizesInput": false, 349 | "tracksExecutionFlow": false, 350 | "usesCodeSigning": false, 351 | "usesEncryptionAlgorithm": "", 352 | "usesMFA": false, 353 | "usesParameterizedInput": false, 354 | "usesSecureFunctions": false, 355 | "usesStrongSessionIdentifiers": false, 356 | "usesVPN": false, 357 | "validatesContentType": false, 358 | "validatesHeaders": false, 359 | "validatesInput": false, 360 | "verifySessionIdentifiers": false 361 | }, 362 | "data": [], 363 | "description": "", 364 | "findings": [], 365 | "handlesResources": false, 366 | "hasWriteAccess": false, 367 | "inBoundary": "Server/DB", 368 | "inScope": true, 369 | "inputs": [ 370 | "Insert query with comments", 371 | "Query for tasks" 372 | ], 373 | "isSQL": true, 374 | "isShared": false, 375 | "levels": [ 376 | 0 377 | ], 378 | "maxClassification": "Classification.UNKNOWN", 379 | "minTLSVersion": "TLSVersion.NONE", 380 | "name": "SQL Database", 381 | "onAWS": false, 382 | "onRDS": false, 383 | "outputs": [ 384 | "Retrieve comments" 385 | ], 386 | "overrides": [], 387 | "port": -1, 388 | "protocol": "", 389 | "severity": 0, 390 | "sourceFiles": [], 391 | "storesLogData": false, 392 | "storesPII": false, 393 | "storesSensitiveData": false, 394 | "type": "DatastoreType.UNKNOWN", 395 | "usesEnvironmentVariables": false 396 | } 397 | ], 398 | "assumptions": [], 399 | "boundaries": [ 400 | { 401 | "assumptions": [], 402 | "controls": { 403 | "authenticatesDestination": false, 404 | "authenticatesSource": false, 405 | "authenticationScheme": "", 406 | "authorizesSource": false, 407 | "checksDestinationRevocation": false, 408 | "checksInputBounds": false, 409 | "definesConnectionTimeout": false, 410 | "disablesDTD": false, 411 | "disablesiFrames": false, 412 | "encodesHeaders": false, 413 | "encodesOutput": false, 414 | "encryptsCookies": false, 415 | "encryptsSessionData": false, 416 | "handlesCrashes": false, 417 | "handlesInterruptions": false, 418 | "handlesResourceConsumption": false, 419 | "hasAccessControl": false, 420 | "implementsAuthenticationScheme": false, 421 | "implementsCSRFToken": false, 422 | "implementsNonce": false, 423 | "implementsPOLP": false, 424 | "implementsServerSideValidation": false, 425 | "implementsStrictHTTPValidation": false, 426 | "invokesScriptFilters": false, 427 | "isEncrypted": false, 428 | "isEncryptedAtRest": false, 429 | "isHardened": false, 430 | "isResilient": false, 431 | "providesConfidentiality": false, 432 | "providesIntegrity": false, 433 | "sanitizesInput": false, 434 | "tracksExecutionFlow": false, 435 | "usesCodeSigning": false, 436 | "usesEncryptionAlgorithm": "", 437 | "usesMFA": false, 438 | "usesParameterizedInput": false, 439 | "usesSecureFunctions": false, 440 | "usesStrongSessionIdentifiers": false, 441 | "usesVPN": false, 442 | "validatesContentType": false, 443 | "validatesHeaders": false, 444 | "validatesInput": false, 445 | "verifySessionIdentifiers": false 446 | }, 447 | "description": "", 448 | "findings": [], 449 | "inBoundary": null, 450 | "inScope": true, 451 | "levels": [ 452 | 0 453 | ], 454 | "maxClassification": "Classification.UNKNOWN", 455 | "minTLSVersion": "TLSVersion.NONE", 456 | "name": "Internet", 457 | "overrides": [], 458 | "severity": 0, 459 | "sourceFiles": [] 460 | }, 461 | { 462 | "assumptions": [], 463 | "controls": { 464 | "authenticatesDestination": false, 465 | "authenticatesSource": false, 466 | "authenticationScheme": "", 467 | "authorizesSource": false, 468 | "checksDestinationRevocation": false, 469 | "checksInputBounds": false, 470 | "definesConnectionTimeout": false, 471 | "disablesDTD": false, 472 | "disablesiFrames": false, 473 | "encodesHeaders": false, 474 | "encodesOutput": false, 475 | "encryptsCookies": false, 476 | "encryptsSessionData": false, 477 | "handlesCrashes": false, 478 | "handlesInterruptions": false, 479 | "handlesResourceConsumption": false, 480 | "hasAccessControl": false, 481 | "implementsAuthenticationScheme": false, 482 | "implementsCSRFToken": false, 483 | "implementsNonce": false, 484 | "implementsPOLP": false, 485 | "implementsServerSideValidation": false, 486 | "implementsStrictHTTPValidation": false, 487 | "invokesScriptFilters": false, 488 | "isEncrypted": false, 489 | "isEncryptedAtRest": false, 490 | "isHardened": false, 491 | "isResilient": false, 492 | "providesConfidentiality": false, 493 | "providesIntegrity": false, 494 | "sanitizesInput": false, 495 | "tracksExecutionFlow": false, 496 | "usesCodeSigning": false, 497 | "usesEncryptionAlgorithm": "", 498 | "usesMFA": false, 499 | "usesParameterizedInput": false, 500 | "usesSecureFunctions": false, 501 | "usesStrongSessionIdentifiers": false, 502 | "usesVPN": false, 503 | "validatesContentType": false, 504 | "validatesHeaders": false, 505 | "validatesInput": false, 506 | "verifySessionIdentifiers": false 507 | }, 508 | "description": "", 509 | "findings": [], 510 | "inBoundary": null, 511 | "inScope": true, 512 | "levels": [ 513 | 0 514 | ], 515 | "maxClassification": "Classification.UNKNOWN", 516 | "minTLSVersion": "TLSVersion.NONE", 517 | "name": "Server/DB", 518 | "overrides": [], 519 | "severity": 0, 520 | "sourceFiles": [] 521 | } 522 | ], 523 | "colormap": false, 524 | "data": [ 525 | { 526 | "carriedBy": [ 527 | "User enters comments (*)" 528 | ], 529 | "classification": "Classification.PUBLIC", 530 | "credentialsLife": "Lifetime.NONE", 531 | "description": "auth cookie description", 532 | "format": "", 533 | "isCredentials": false, 534 | "isDestEncryptedAtRest": false, 535 | "isPII": false, 536 | "isSourceEncryptedAtRest": false, 537 | "isStored": false, 538 | "name": "auth cookie", 539 | "processedBy": [ 540 | "User", 541 | "Web Server" 542 | ] 543 | } 544 | ], 545 | "description": "aaa", 546 | "elements": [ 547 | { 548 | "__class__": "Actor", 549 | "assumptions": [], 550 | "controls": { 551 | "authenticatesDestination": false, 552 | "authenticatesSource": false, 553 | "authenticationScheme": "", 554 | "authorizesSource": false, 555 | "checksDestinationRevocation": false, 556 | "checksInputBounds": false, 557 | "definesConnectionTimeout": false, 558 | "disablesDTD": false, 559 | "disablesiFrames": false, 560 | "encodesHeaders": false, 561 | "encodesOutput": false, 562 | "encryptsCookies": false, 563 | "encryptsSessionData": false, 564 | "handlesCrashes": false, 565 | "handlesInterruptions": false, 566 | "handlesResourceConsumption": false, 567 | "hasAccessControl": false, 568 | "implementsAuthenticationScheme": false, 569 | "implementsCSRFToken": false, 570 | "implementsNonce": false, 571 | "implementsPOLP": false, 572 | "implementsServerSideValidation": false, 573 | "implementsStrictHTTPValidation": false, 574 | "invokesScriptFilters": false, 575 | "isEncrypted": false, 576 | "isEncryptedAtRest": false, 577 | "isHardened": false, 578 | "isResilient": false, 579 | "providesConfidentiality": false, 580 | "providesIntegrity": false, 581 | "sanitizesInput": false, 582 | "tracksExecutionFlow": false, 583 | "usesCodeSigning": false, 584 | "usesEncryptionAlgorithm": "", 585 | "usesMFA": false, 586 | "usesParameterizedInput": false, 587 | "usesSecureFunctions": false, 588 | "usesStrongSessionIdentifiers": false, 589 | "usesVPN": false, 590 | "validatesContentType": false, 591 | "validatesHeaders": false, 592 | "validatesInput": false, 593 | "verifySessionIdentifiers": false 594 | }, 595 | "data": [], 596 | "description": "", 597 | "findings": [], 598 | "inBoundary": "Internet", 599 | "inScope": true, 600 | "inputs": [ 601 | "Show comments (*)" 602 | ], 603 | "isAdmin": false, 604 | "levels": [ 605 | 0 606 | ], 607 | "maxClassification": "Classification.UNKNOWN", 608 | "minTLSVersion": "TLSVersion.NONE", 609 | "name": "User", 610 | "outputs": [ 611 | "User enters comments (*)" 612 | ], 613 | "overrides": [], 614 | "port": -1, 615 | "protocol": "", 616 | "severity": 0, 617 | "sourceFiles": [] 618 | }, 619 | { 620 | "OS": "", 621 | "__class__": "Server", 622 | "assumptions": [], 623 | "controls": { 624 | "authenticatesDestination": false, 625 | "authenticatesSource": false, 626 | "authenticationScheme": "", 627 | "authorizesSource": false, 628 | "checksDestinationRevocation": false, 629 | "checksInputBounds": false, 630 | "definesConnectionTimeout": false, 631 | "disablesDTD": false, 632 | "disablesiFrames": false, 633 | "encodesHeaders": false, 634 | "encodesOutput": false, 635 | "encryptsCookies": false, 636 | "encryptsSessionData": false, 637 | "handlesCrashes": false, 638 | "handlesInterruptions": false, 639 | "handlesResourceConsumption": false, 640 | "hasAccessControl": false, 641 | "implementsAuthenticationScheme": false, 642 | "implementsCSRFToken": false, 643 | "implementsNonce": false, 644 | "implementsPOLP": false, 645 | "implementsServerSideValidation": false, 646 | "implementsStrictHTTPValidation": false, 647 | "invokesScriptFilters": false, 648 | "isEncrypted": false, 649 | "isEncryptedAtRest": false, 650 | "isHardened": false, 651 | "isResilient": false, 652 | "providesConfidentiality": false, 653 | "providesIntegrity": false, 654 | "sanitizesInput": false, 655 | "tracksExecutionFlow": false, 656 | "usesCodeSigning": false, 657 | "usesEncryptionAlgorithm": "", 658 | "usesMFA": false, 659 | "usesParameterizedInput": false, 660 | "usesSecureFunctions": false, 661 | "usesStrongSessionIdentifiers": false, 662 | "usesVPN": false, 663 | "validatesContentType": false, 664 | "validatesHeaders": false, 665 | "validatesInput": false, 666 | "verifySessionIdentifiers": false 667 | }, 668 | "data": [], 669 | "description": "", 670 | "findings": [], 671 | "handlesResources": false, 672 | "inBoundary": null, 673 | "inScope": true, 674 | "inputs": [ 675 | "User enters comments (*)", 676 | "Retrieve comments" 677 | ], 678 | "levels": [ 679 | 0 680 | ], 681 | "maxClassification": "Classification.UNKNOWN", 682 | "minTLSVersion": "TLSVersion.NONE", 683 | "name": "Web Server", 684 | "onAWS": false, 685 | "outputs": [ 686 | "Insert query with comments", 687 | "Call func", 688 | "Show comments (*)" 689 | ], 690 | "overrides": [], 691 | "port": -1, 692 | "protocol": "", 693 | "severity": 0, 694 | "sourceFiles": [], 695 | "usesCache": false, 696 | "usesEnvironmentVariables": false, 697 | "usesSessionTokens": false, 698 | "usesVPN": false, 699 | "usesXMLParser": false 700 | }, 701 | { 702 | "OS": "", 703 | "__class__": "Lambda", 704 | "assumptions": [], 705 | "controls": { 706 | "authenticatesDestination": false, 707 | "authenticatesSource": false, 708 | "authenticationScheme": "", 709 | "authorizesSource": false, 710 | "checksDestinationRevocation": false, 711 | "checksInputBounds": false, 712 | "definesConnectionTimeout": false, 713 | "disablesDTD": false, 714 | "disablesiFrames": false, 715 | "encodesHeaders": false, 716 | "encodesOutput": false, 717 | "encryptsCookies": false, 718 | "encryptsSessionData": false, 719 | "handlesCrashes": false, 720 | "handlesInterruptions": false, 721 | "handlesResourceConsumption": false, 722 | "hasAccessControl": false, 723 | "implementsAuthenticationScheme": false, 724 | "implementsCSRFToken": false, 725 | "implementsNonce": false, 726 | "implementsPOLP": false, 727 | "implementsServerSideValidation": false, 728 | "implementsStrictHTTPValidation": false, 729 | "invokesScriptFilters": false, 730 | "isEncrypted": false, 731 | "isEncryptedAtRest": false, 732 | "isHardened": false, 733 | "isResilient": false, 734 | "providesConfidentiality": false, 735 | "providesIntegrity": false, 736 | "sanitizesInput": false, 737 | "tracksExecutionFlow": false, 738 | "usesCodeSigning": false, 739 | "usesEncryptionAlgorithm": "", 740 | "usesMFA": false, 741 | "usesParameterizedInput": false, 742 | "usesSecureFunctions": false, 743 | "usesStrongSessionIdentifiers": false, 744 | "usesVPN": false, 745 | "validatesContentType": false, 746 | "validatesHeaders": false, 747 | "validatesInput": false, 748 | "verifySessionIdentifiers": false 749 | }, 750 | "data": [], 751 | "description": "", 752 | "environment": "", 753 | "findings": [], 754 | "handlesResources": false, 755 | "implementsAPI": false, 756 | "inBoundary": null, 757 | "inScope": true, 758 | "inputs": [ 759 | "Call func" 760 | ], 761 | "levels": [ 762 | 0 763 | ], 764 | "maxClassification": "Classification.UNKNOWN", 765 | "minTLSVersion": "TLSVersion.NONE", 766 | "name": "Lambda func", 767 | "onAWS": true, 768 | "outputs": [], 769 | "overrides": [], 770 | "port": -1, 771 | "protocol": "", 772 | "severity": 0, 773 | "sourceFiles": [], 774 | "usesEnvironmentVariables": false 775 | }, 776 | { 777 | "OS": "", 778 | "__class__": "Process", 779 | "allowsClientSideScripting": false, 780 | "assumptions": [], 781 | "codeType": "Unmanaged", 782 | "controls": { 783 | "authenticatesDestination": false, 784 | "authenticatesSource": false, 785 | "authenticationScheme": "", 786 | "authorizesSource": false, 787 | "checksDestinationRevocation": false, 788 | "checksInputBounds": false, 789 | "definesConnectionTimeout": false, 790 | "disablesDTD": false, 791 | "disablesiFrames": false, 792 | "encodesHeaders": false, 793 | "encodesOutput": false, 794 | "encryptsCookies": false, 795 | "encryptsSessionData": false, 796 | "handlesCrashes": false, 797 | "handlesInterruptions": false, 798 | "handlesResourceConsumption": false, 799 | "hasAccessControl": false, 800 | "implementsAuthenticationScheme": false, 801 | "implementsCSRFToken": false, 802 | "implementsNonce": false, 803 | "implementsPOLP": false, 804 | "implementsServerSideValidation": false, 805 | "implementsStrictHTTPValidation": false, 806 | "invokesScriptFilters": false, 807 | "isEncrypted": false, 808 | "isEncryptedAtRest": false, 809 | "isHardened": false, 810 | "isResilient": false, 811 | "providesConfidentiality": false, 812 | "providesIntegrity": false, 813 | "sanitizesInput": false, 814 | "tracksExecutionFlow": false, 815 | "usesCodeSigning": false, 816 | "usesEncryptionAlgorithm": "", 817 | "usesMFA": false, 818 | "usesParameterizedInput": false, 819 | "usesSecureFunctions": false, 820 | "usesStrongSessionIdentifiers": false, 821 | "usesVPN": false, 822 | "validatesContentType": false, 823 | "validatesHeaders": false, 824 | "validatesInput": false, 825 | "verifySessionIdentifiers": false 826 | }, 827 | "data": [], 828 | "description": "", 829 | "environment": "", 830 | "findings": [], 831 | "handlesResources": false, 832 | "implementsAPI": false, 833 | "implementsCommunicationProtocol": false, 834 | "inBoundary": null, 835 | "inScope": true, 836 | "inputs": [], 837 | "levels": [ 838 | 0 839 | ], 840 | "maxClassification": "Classification.UNKNOWN", 841 | "minTLSVersion": "TLSVersion.NONE", 842 | "name": "Task queue worker", 843 | "onAWS": false, 844 | "outputs": [ 845 | "Query for tasks" 846 | ], 847 | "overrides": [], 848 | "port": -1, 849 | "protocol": "", 850 | "severity": 0, 851 | "sourceFiles": [], 852 | "tracksExecutionFlow": false, 853 | "usesEnvironmentVariables": false 854 | }, 855 | { 856 | "OS": "", 857 | "__class__": "Datastore", 858 | "assumptions": [], 859 | "controls": { 860 | "authenticatesDestination": false, 861 | "authenticatesSource": false, 862 | "authenticationScheme": "", 863 | "authorizesSource": false, 864 | "checksDestinationRevocation": false, 865 | "checksInputBounds": false, 866 | "definesConnectionTimeout": false, 867 | "disablesDTD": false, 868 | "disablesiFrames": false, 869 | "encodesHeaders": false, 870 | "encodesOutput": false, 871 | "encryptsCookies": false, 872 | "encryptsSessionData": false, 873 | "handlesCrashes": false, 874 | "handlesInterruptions": false, 875 | "handlesResourceConsumption": false, 876 | "hasAccessControl": false, 877 | "implementsAuthenticationScheme": false, 878 | "implementsCSRFToken": false, 879 | "implementsNonce": false, 880 | "implementsPOLP": false, 881 | "implementsServerSideValidation": false, 882 | "implementsStrictHTTPValidation": false, 883 | "invokesScriptFilters": false, 884 | "isEncrypted": false, 885 | "isEncryptedAtRest": false, 886 | "isHardened": false, 887 | "isResilient": false, 888 | "providesConfidentiality": false, 889 | "providesIntegrity": false, 890 | "sanitizesInput": false, 891 | "tracksExecutionFlow": false, 892 | "usesCodeSigning": false, 893 | "usesEncryptionAlgorithm": "", 894 | "usesMFA": false, 895 | "usesParameterizedInput": false, 896 | "usesSecureFunctions": false, 897 | "usesStrongSessionIdentifiers": false, 898 | "usesVPN": false, 899 | "validatesContentType": false, 900 | "validatesHeaders": false, 901 | "validatesInput": false, 902 | "verifySessionIdentifiers": false 903 | }, 904 | "data": [], 905 | "description": "", 906 | "findings": [], 907 | "handlesResources": false, 908 | "hasWriteAccess": false, 909 | "inBoundary": "Server/DB", 910 | "inScope": true, 911 | "inputs": [ 912 | "Insert query with comments", 913 | "Query for tasks" 914 | ], 915 | "isSQL": true, 916 | "isShared": false, 917 | "levels": [ 918 | 0 919 | ], 920 | "maxClassification": "Classification.UNKNOWN", 921 | "minTLSVersion": "TLSVersion.NONE", 922 | "name": "SQL Database", 923 | "onAWS": false, 924 | "onRDS": false, 925 | "outputs": [ 926 | "Retrieve comments" 927 | ], 928 | "overrides": [], 929 | "port": -1, 930 | "protocol": "", 931 | "severity": 0, 932 | "sourceFiles": [], 933 | "storesLogData": false, 934 | "storesPII": false, 935 | "storesSensitiveData": false, 936 | "type": "DatastoreType.UNKNOWN", 937 | "usesEnvironmentVariables": false 938 | } 939 | ], 940 | "excluded_findings": [], 941 | "findings": [], 942 | "flows": [ 943 | { 944 | "assumptions": [], 945 | "controls": { 946 | "authenticatesDestination": false, 947 | "authenticatesSource": false, 948 | "authenticationScheme": "", 949 | "authorizesSource": false, 950 | "checksDestinationRevocation": false, 951 | "checksInputBounds": false, 952 | "definesConnectionTimeout": false, 953 | "disablesDTD": false, 954 | "disablesiFrames": false, 955 | "encodesHeaders": false, 956 | "encodesOutput": false, 957 | "encryptsCookies": false, 958 | "encryptsSessionData": false, 959 | "handlesCrashes": false, 960 | "handlesInterruptions": false, 961 | "handlesResourceConsumption": false, 962 | "hasAccessControl": false, 963 | "implementsAuthenticationScheme": false, 964 | "implementsCSRFToken": false, 965 | "implementsNonce": false, 966 | "implementsPOLP": false, 967 | "implementsServerSideValidation": false, 968 | "implementsStrictHTTPValidation": false, 969 | "invokesScriptFilters": false, 970 | "isEncrypted": false, 971 | "isEncryptedAtRest": false, 972 | "isHardened": false, 973 | "isResilient": false, 974 | "providesConfidentiality": false, 975 | "providesIntegrity": false, 976 | "sanitizesInput": false, 977 | "tracksExecutionFlow": false, 978 | "usesCodeSigning": false, 979 | "usesEncryptionAlgorithm": "", 980 | "usesMFA": false, 981 | "usesParameterizedInput": false, 982 | "usesSecureFunctions": false, 983 | "usesStrongSessionIdentifiers": false, 984 | "usesVPN": false, 985 | "validatesContentType": false, 986 | "validatesHeaders": false, 987 | "validatesInput": false, 988 | "verifySessionIdentifiers": false 989 | }, 990 | "data": [ 991 | "auth cookie" 992 | ], 993 | "description": "", 994 | "dstPort": -1, 995 | "findings": [], 996 | "implementsCommunicationProtocol": false, 997 | "inBoundary": null, 998 | "inScope": true, 999 | "isResponse": false, 1000 | "levels": [ 1001 | 0 1002 | ], 1003 | "maxClassification": "Classification.UNKNOWN", 1004 | "minTLSVersion": "TLSVersion.NONE", 1005 | "name": "User enters comments (*)", 1006 | "note": "bbb", 1007 | "order": 1, 1008 | "overrides": [], 1009 | "protocol": "", 1010 | "response": null, 1011 | "responseTo": null, 1012 | "severity": 0, 1013 | "sink": "Web Server", 1014 | "source": "User", 1015 | "sourceFiles": [], 1016 | "srcPort": -1, 1017 | "tlsVersion": "TLSVersion.NONE", 1018 | "usesSessionTokens": false, 1019 | "usesVPN": false 1020 | }, 1021 | { 1022 | "assumptions": [], 1023 | "controls": { 1024 | "authenticatesDestination": false, 1025 | "authenticatesSource": false, 1026 | "authenticationScheme": "", 1027 | "authorizesSource": false, 1028 | "checksDestinationRevocation": false, 1029 | "checksInputBounds": false, 1030 | "definesConnectionTimeout": false, 1031 | "disablesDTD": false, 1032 | "disablesiFrames": false, 1033 | "encodesHeaders": false, 1034 | "encodesOutput": false, 1035 | "encryptsCookies": false, 1036 | "encryptsSessionData": false, 1037 | "handlesCrashes": false, 1038 | "handlesInterruptions": false, 1039 | "handlesResourceConsumption": false, 1040 | "hasAccessControl": false, 1041 | "implementsAuthenticationScheme": false, 1042 | "implementsCSRFToken": false, 1043 | "implementsNonce": false, 1044 | "implementsPOLP": false, 1045 | "implementsServerSideValidation": false, 1046 | "implementsStrictHTTPValidation": false, 1047 | "invokesScriptFilters": false, 1048 | "isEncrypted": false, 1049 | "isEncryptedAtRest": false, 1050 | "isHardened": false, 1051 | "isResilient": false, 1052 | "providesConfidentiality": false, 1053 | "providesIntegrity": false, 1054 | "sanitizesInput": false, 1055 | "tracksExecutionFlow": false, 1056 | "usesCodeSigning": false, 1057 | "usesEncryptionAlgorithm": "", 1058 | "usesMFA": false, 1059 | "usesParameterizedInput": false, 1060 | "usesSecureFunctions": false, 1061 | "usesStrongSessionIdentifiers": false, 1062 | "usesVPN": false, 1063 | "validatesContentType": false, 1064 | "validatesHeaders": false, 1065 | "validatesInput": false, 1066 | "verifySessionIdentifiers": false 1067 | }, 1068 | "data": [], 1069 | "description": "", 1070 | "dstPort": -1, 1071 | "findings": [], 1072 | "implementsCommunicationProtocol": false, 1073 | "inBoundary": null, 1074 | "inScope": true, 1075 | "isResponse": false, 1076 | "levels": [ 1077 | 0 1078 | ], 1079 | "maxClassification": "Classification.UNKNOWN", 1080 | "minTLSVersion": "TLSVersion.NONE", 1081 | "name": "Insert query with comments", 1082 | "note": "ccc", 1083 | "order": 2, 1084 | "overrides": [], 1085 | "protocol": "", 1086 | "response": null, 1087 | "responseTo": null, 1088 | "severity": 0, 1089 | "sink": "SQL Database", 1090 | "source": "Web Server", 1091 | "sourceFiles": [], 1092 | "srcPort": -1, 1093 | "tlsVersion": "TLSVersion.NONE", 1094 | "usesSessionTokens": false, 1095 | "usesVPN": false 1096 | }, 1097 | { 1098 | "assumptions": [], 1099 | "controls": { 1100 | "authenticatesDestination": false, 1101 | "authenticatesSource": false, 1102 | "authenticationScheme": "", 1103 | "authorizesSource": false, 1104 | "checksDestinationRevocation": false, 1105 | "checksInputBounds": false, 1106 | "definesConnectionTimeout": false, 1107 | "disablesDTD": false, 1108 | "disablesiFrames": false, 1109 | "encodesHeaders": false, 1110 | "encodesOutput": false, 1111 | "encryptsCookies": false, 1112 | "encryptsSessionData": false, 1113 | "handlesCrashes": false, 1114 | "handlesInterruptions": false, 1115 | "handlesResourceConsumption": false, 1116 | "hasAccessControl": false, 1117 | "implementsAuthenticationScheme": false, 1118 | "implementsCSRFToken": false, 1119 | "implementsNonce": false, 1120 | "implementsPOLP": false, 1121 | "implementsServerSideValidation": false, 1122 | "implementsStrictHTTPValidation": false, 1123 | "invokesScriptFilters": false, 1124 | "isEncrypted": false, 1125 | "isEncryptedAtRest": false, 1126 | "isHardened": false, 1127 | "isResilient": false, 1128 | "providesConfidentiality": false, 1129 | "providesIntegrity": false, 1130 | "sanitizesInput": false, 1131 | "tracksExecutionFlow": false, 1132 | "usesCodeSigning": false, 1133 | "usesEncryptionAlgorithm": "", 1134 | "usesMFA": false, 1135 | "usesParameterizedInput": false, 1136 | "usesSecureFunctions": false, 1137 | "usesStrongSessionIdentifiers": false, 1138 | "usesVPN": false, 1139 | "validatesContentType": false, 1140 | "validatesHeaders": false, 1141 | "validatesInput": false, 1142 | "verifySessionIdentifiers": false 1143 | }, 1144 | "data": [], 1145 | "description": "", 1146 | "dstPort": -1, 1147 | "findings": [], 1148 | "implementsCommunicationProtocol": false, 1149 | "inBoundary": null, 1150 | "inScope": true, 1151 | "isResponse": false, 1152 | "levels": [ 1153 | 0 1154 | ], 1155 | "maxClassification": "Classification.UNKNOWN", 1156 | "minTLSVersion": "TLSVersion.NONE", 1157 | "name": "Call func", 1158 | "note": "", 1159 | "order": 3, 1160 | "overrides": [], 1161 | "protocol": "", 1162 | "response": null, 1163 | "responseTo": null, 1164 | "severity": 0, 1165 | "sink": "Lambda func", 1166 | "source": "Web Server", 1167 | "sourceFiles": [], 1168 | "srcPort": -1, 1169 | "tlsVersion": "TLSVersion.NONE", 1170 | "usesSessionTokens": false, 1171 | "usesVPN": false 1172 | }, 1173 | { 1174 | "assumptions": [], 1175 | "controls": { 1176 | "authenticatesDestination": false, 1177 | "authenticatesSource": false, 1178 | "authenticationScheme": "", 1179 | "authorizesSource": false, 1180 | "checksDestinationRevocation": false, 1181 | "checksInputBounds": false, 1182 | "definesConnectionTimeout": false, 1183 | "disablesDTD": false, 1184 | "disablesiFrames": false, 1185 | "encodesHeaders": false, 1186 | "encodesOutput": false, 1187 | "encryptsCookies": false, 1188 | "encryptsSessionData": false, 1189 | "handlesCrashes": false, 1190 | "handlesInterruptions": false, 1191 | "handlesResourceConsumption": false, 1192 | "hasAccessControl": false, 1193 | "implementsAuthenticationScheme": false, 1194 | "implementsCSRFToken": false, 1195 | "implementsNonce": false, 1196 | "implementsPOLP": false, 1197 | "implementsServerSideValidation": false, 1198 | "implementsStrictHTTPValidation": false, 1199 | "invokesScriptFilters": false, 1200 | "isEncrypted": false, 1201 | "isEncryptedAtRest": false, 1202 | "isHardened": false, 1203 | "isResilient": false, 1204 | "providesConfidentiality": false, 1205 | "providesIntegrity": false, 1206 | "sanitizesInput": false, 1207 | "tracksExecutionFlow": false, 1208 | "usesCodeSigning": false, 1209 | "usesEncryptionAlgorithm": "", 1210 | "usesMFA": false, 1211 | "usesParameterizedInput": false, 1212 | "usesSecureFunctions": false, 1213 | "usesStrongSessionIdentifiers": false, 1214 | "usesVPN": false, 1215 | "validatesContentType": false, 1216 | "validatesHeaders": false, 1217 | "validatesInput": false, 1218 | "verifySessionIdentifiers": false 1219 | }, 1220 | "data": [], 1221 | "description": "", 1222 | "dstPort": -1, 1223 | "findings": [], 1224 | "implementsCommunicationProtocol": false, 1225 | "inBoundary": null, 1226 | "inScope": true, 1227 | "isResponse": false, 1228 | "levels": [ 1229 | 0 1230 | ], 1231 | "maxClassification": "Classification.UNKNOWN", 1232 | "minTLSVersion": "TLSVersion.NONE", 1233 | "name": "Retrieve comments", 1234 | "note": "", 1235 | "order": 4, 1236 | "overrides": [], 1237 | "protocol": "", 1238 | "response": null, 1239 | "responseTo": null, 1240 | "severity": 0, 1241 | "sink": "Web Server", 1242 | "source": "SQL Database", 1243 | "sourceFiles": [], 1244 | "srcPort": -1, 1245 | "tlsVersion": "TLSVersion.NONE", 1246 | "usesSessionTokens": false, 1247 | "usesVPN": false 1248 | }, 1249 | { 1250 | "assumptions": [], 1251 | "controls": { 1252 | "authenticatesDestination": false, 1253 | "authenticatesSource": false, 1254 | "authenticationScheme": "", 1255 | "authorizesSource": false, 1256 | "checksDestinationRevocation": false, 1257 | "checksInputBounds": false, 1258 | "definesConnectionTimeout": false, 1259 | "disablesDTD": false, 1260 | "disablesiFrames": false, 1261 | "encodesHeaders": false, 1262 | "encodesOutput": false, 1263 | "encryptsCookies": false, 1264 | "encryptsSessionData": false, 1265 | "handlesCrashes": false, 1266 | "handlesInterruptions": false, 1267 | "handlesResourceConsumption": false, 1268 | "hasAccessControl": false, 1269 | "implementsAuthenticationScheme": false, 1270 | "implementsCSRFToken": false, 1271 | "implementsNonce": false, 1272 | "implementsPOLP": false, 1273 | "implementsServerSideValidation": false, 1274 | "implementsStrictHTTPValidation": false, 1275 | "invokesScriptFilters": false, 1276 | "isEncrypted": false, 1277 | "isEncryptedAtRest": false, 1278 | "isHardened": false, 1279 | "isResilient": false, 1280 | "providesConfidentiality": false, 1281 | "providesIntegrity": false, 1282 | "sanitizesInput": false, 1283 | "tracksExecutionFlow": false, 1284 | "usesCodeSigning": false, 1285 | "usesEncryptionAlgorithm": "", 1286 | "usesMFA": false, 1287 | "usesParameterizedInput": false, 1288 | "usesSecureFunctions": false, 1289 | "usesStrongSessionIdentifiers": false, 1290 | "usesVPN": false, 1291 | "validatesContentType": false, 1292 | "validatesHeaders": false, 1293 | "validatesInput": false, 1294 | "verifySessionIdentifiers": false 1295 | }, 1296 | "data": [], 1297 | "description": "", 1298 | "dstPort": -1, 1299 | "findings": [], 1300 | "implementsCommunicationProtocol": false, 1301 | "inBoundary": null, 1302 | "inScope": true, 1303 | "isResponse": false, 1304 | "levels": [ 1305 | 0 1306 | ], 1307 | "maxClassification": "Classification.UNKNOWN", 1308 | "minTLSVersion": "TLSVersion.NONE", 1309 | "name": "Show comments (*)", 1310 | "note": "", 1311 | "order": 5, 1312 | "overrides": [], 1313 | "protocol": "", 1314 | "response": null, 1315 | "responseTo": null, 1316 | "severity": 0, 1317 | "sink": "User", 1318 | "source": "Web Server", 1319 | "sourceFiles": [], 1320 | "srcPort": -1, 1321 | "tlsVersion": "TLSVersion.NONE", 1322 | "usesSessionTokens": false, 1323 | "usesVPN": false 1324 | }, 1325 | { 1326 | "assumptions": [], 1327 | "controls": { 1328 | "authenticatesDestination": false, 1329 | "authenticatesSource": false, 1330 | "authenticationScheme": "", 1331 | "authorizesSource": false, 1332 | "checksDestinationRevocation": false, 1333 | "checksInputBounds": false, 1334 | "definesConnectionTimeout": false, 1335 | "disablesDTD": false, 1336 | "disablesiFrames": false, 1337 | "encodesHeaders": false, 1338 | "encodesOutput": false, 1339 | "encryptsCookies": false, 1340 | "encryptsSessionData": false, 1341 | "handlesCrashes": false, 1342 | "handlesInterruptions": false, 1343 | "handlesResourceConsumption": false, 1344 | "hasAccessControl": false, 1345 | "implementsAuthenticationScheme": false, 1346 | "implementsCSRFToken": false, 1347 | "implementsNonce": false, 1348 | "implementsPOLP": false, 1349 | "implementsServerSideValidation": false, 1350 | "implementsStrictHTTPValidation": false, 1351 | "invokesScriptFilters": false, 1352 | "isEncrypted": false, 1353 | "isEncryptedAtRest": false, 1354 | "isHardened": false, 1355 | "isResilient": false, 1356 | "providesConfidentiality": false, 1357 | "providesIntegrity": false, 1358 | "sanitizesInput": false, 1359 | "tracksExecutionFlow": false, 1360 | "usesCodeSigning": false, 1361 | "usesEncryptionAlgorithm": "", 1362 | "usesMFA": false, 1363 | "usesParameterizedInput": false, 1364 | "usesSecureFunctions": false, 1365 | "usesStrongSessionIdentifiers": false, 1366 | "usesVPN": false, 1367 | "validatesContentType": false, 1368 | "validatesHeaders": false, 1369 | "validatesInput": false, 1370 | "verifySessionIdentifiers": false 1371 | }, 1372 | "data": [], 1373 | "description": "", 1374 | "dstPort": -1, 1375 | "findings": [], 1376 | "implementsCommunicationProtocol": false, 1377 | "inBoundary": null, 1378 | "inScope": true, 1379 | "isResponse": false, 1380 | "levels": [ 1381 | 0 1382 | ], 1383 | "maxClassification": "Classification.UNKNOWN", 1384 | "minTLSVersion": "TLSVersion.NONE", 1385 | "name": "Query for tasks", 1386 | "note": "", 1387 | "order": 6, 1388 | "overrides": [], 1389 | "protocol": "", 1390 | "response": null, 1391 | "responseTo": null, 1392 | "severity": 0, 1393 | "sink": "SQL Database", 1394 | "source": "Task queue worker", 1395 | "sourceFiles": [], 1396 | "srcPort": -1, 1397 | "tlsVersion": "TLSVersion.NONE", 1398 | "usesSessionTokens": false, 1399 | "usesVPN": false 1400 | } 1401 | ], 1402 | "ignoreUnused": false, 1403 | "isOrdered": true, 1404 | "mergeResponses": false, 1405 | "name": "my test tm", 1406 | "onDuplicates": "Action.NO_ACTION", 1407 | "threatsExcluded": [], 1408 | "threatsFile": "pytm/threatlib/threats.json" 1409 | } 1410 | --------------------------------------------------------------------------------