{"id":15903146,"url":"https://github.com/OWASP/pytm","last_synced_at":"2025-10-18T06:30:34.935Z","repository":{"id":33150862,"uuid":"133432149","full_name":"OWASP/pytm","owner":"OWASP","description":"A Pythonic framework for threat modeling","archived":false,"fork":false,"pushed_at":"2024-12-25T01:42:36.000Z","size":1687,"stargazers_count":950,"open_issues_count":46,"forks_count":181,"subscribers_count":42,"default_branch":"master","last_synced_at":"2025-01-27T06:59:48.749Z","etag":null,"topics":["data-flow-diagram","dataflow","dfd","diagram","pythonic-framework","secure-development","sequence-diagram","threat-modeling","threat-modeling-from-code","threats"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/OWASP.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":"ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-05-14T23:16:07.000Z","updated_at":"2025-01-23T16:07:56.000Z","dependencies_parsed_at":"2023-02-13T23:50:20.309Z","dependency_job_id":"1c46b485-cf00-4c9a-9d6e-ecd94c524f54","html_url":"https://github.com/OWASP/pytm","commit_stats":{"total_commits":333,"total_committers":42,"mean_commits":7.928571428571429,"dds":0.8618618618618619,"last_synced_commit":"e35c2e7408246341a830e96560e3adce06455f0a"},"previous_names":["owasp/pytm","izar/pytm"],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OWASP%2Fpytm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OWASP%2Fpytm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OWASP%2Fpytm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OWASP%2Fpytm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/OWASP","download_url":"https://codeload.github.com/OWASP/pytm/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":236907711,"owners_count":19223639,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["data-flow-diagram","dataflow","dfd","diagram","pythonic-framework","secure-development","sequence-diagram","threat-modeling","threat-modeling-from-code","threats"],"created_at":"2024-10-06T12:01:08.958Z","updated_at":"2025-10-18T06:30:34.928Z","avatar_url":"https://github.com/OWASP.png","language":"Python","readme":"![build+test](https://github.com/izar/pytm/workflows/build%2Btest/badge.svg)\n\n\n# pytm: A Pythonic framework for threat modeling\n\n![pytm logo](docs/pytm-logo.svg)\n\n## Introduction\n\nTraditional 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.\n\n## Features\n\nBased on your input and definition of the architectural design, pytm can automatically generate the following items:\n- Data Flow Diagram (DFD)\n- Sequence Diagram\n- Relevant threats to your system\n\n## Requirements\n\n* Linux/MacOS\n* Python 3.x\n* Graphviz package\n* Java (OpenJDK 10 or 11)\n* [plantuml.jar](http://sourceforge.net/projects/plantuml/files/plantuml.jar/download)\n\n## Getting Started\n\nThe `tm.py` is an example model. You can run it to generate the report and diagram image files that it references:\n\n```\nmkdir -p tm\n./tm.py --report docs/basic_template.md | pandoc -f markdown -t html \u003e tm/report.html\n./tm.py --dfd | dot -Tpng -o tm/dfd.png\n./tm.py --seq | java -Djava.awt.headless=true -jar $PLANTUML_PATH -tpng -pipe \u003e tm/seq.png\n```\n\nThere'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:\n\n```\nmake MODEL=the_name_of_your_model_minus_.py\n```\n\nYou should either have plantuml.jar on the same directory as your model, or set PLANTUML_PATH.\n\nTo avoid installing all the dependencies, like `pandoc` or `Java`, the script can be run inside a container:\n\n```\n# do this only once\nexport USE_DOCKER=true\nmake image\n\n# call this after every change in your model\nmake\n```\n\n### Getting strated - devbox variant\n\nTo simplify the usage of `pytm` host dependencies can be completely isolated\nusing [`devbox`](https://github.com/jetify-com/devbox). This is usually a\nlower overhead and more convenient alternative to the OCI container approach.\n\n- Install devbox on Linux/MacOS: `curl -fsSL https://get.jetify.com/devbox | bash`\n- Install devbox on [Windows/WSL2](https://www.jetify.com/docs/devbox/installing_devbox/?install-method=wsl)\n- update to latest version: `devbox version update`\n- `devbox shell`\n- `which python` -\u003e `.devbox/nix/profile/default/bin/python`\n- `./tm.py --dfd | dot -Tpng -o sample.png` -\u003e `sample.png`\n- `exit`\n\n## Usage\n\nAll available arguments:\n\n```text\nusage: tm.py [-h] [--sqldump SQLDUMP] [--debug] [--dfd] [--report REPORT]\n             [--exclude EXCLUDE] [--seq] [--list] [--describe DESCRIBE]\n             [--list-elements] [--json JSON] [--levels LEVELS [LEVELS ...]]\n             [--stale_days STALE_DAYS]\n\noptional arguments:\n  -h, --help            show this help message and exit\n  --sqldump SQLDUMP     dumps all threat model elements and findings into the\n                        named sqlite file (erased if exists)\n  --debug               print debug messages\n  --dfd                 output DFD\n  --report REPORT       output report using the named template file (sample\n                        template file is under docs/template.md)\n  --exclude EXCLUDE     specify threat IDs to be ignored\n  --seq                 output sequential diagram\n  --list                list all available threats\n  --colormap            color the risk in the diagram\n  --describe DESCRIBE   describe the properties available for a given element\n  --list-elements       list all elements which can be part of a threat model\n  --json JSON           output a JSON file\n  --levels LEVELS [LEVELS ...]\n                        Select levels to be drawn in the threat model (int\n                        separated by comma).\n  --stale_days STALE_DAYS\n                        checks if the delta between the TM script and the code\n                        described by it is bigger than the specified value in\n                        days\n```\n\nThe *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.\n\nCurrently available elements are: TM, Element, Server, ExternalEntity, Datastore, Actor, Process, SetOfProcesses, Dataflow, Boundary and Lambda.\n\nThe available properties of an element can be listed by using `--describe` followed by the name of an element:\n\n```text\n\n(pytm) ➜  pytm git:(master) ✗ ./tm.py --describe Element\nElement class attributes:\n  OS\n  definesConnectionTimeout        default: False\n  description\n  handlesResources                default: False\n  implementsAuthenticationScheme  default: False\n  implementsNonce                 default: False\n  inBoundary\n  inScope                         Is the element in scope of the threat model, default: True\n  isAdmin                         default: False\n  isHardened                      default: False\n  name                            required\n  onAWS                           default: False\n\n```\n\nThe *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).\n\n\n## Usage - devbox variant\n\n- `devbox shell`\n- `pytm` usage as usual\n- `exit`\n\n## Creating a Threat Model\n\nThe following is a sample `tm.py` file that describes a simple application where a User logs into the application\nand posts comments on the app. The app server stores those comments into the database. There is an AWS Lambda\nthat periodically cleans the Database.\n\n```python\n\n#!/usr/bin/env python3\n\nfrom pytm.pytm import TM, Server, Datastore, Dataflow, Boundary, Actor, Lambda, Data, Classification\n\ntm = TM(\"my test tm\")\ntm.description = \"another test tm\"\ntm.isOrdered = True\n\nUser_Web = Boundary(\"User/Web\")\nWeb_DB = Boundary(\"Web/DB\")\n\nuser = Actor(\"User\")\nuser.inBoundary = User_Web\n\nweb = Server(\"Web Server\")\nweb.OS = \"CloudOS\"\nweb.isHardened = True\nweb.sourceCode = \"server/web.cc\"\n\ndb = Datastore(\"SQL Database (*)\")\ndb.OS = \"CentOS\"\ndb.isHardened = False\ndb.inBoundary = Web_DB\ndb.isSql = True\ndb.inScope = False\ndb.sourceCode = \"model/schema.sql\"\n\ncomments = Data(\n    name=\"Comments\", \n    description=\"Comments in HTML or Markdown\",  \n    classification=Classification.PUBLIC,  \n    isPII=False,\n    isCredentials=False,  \n    # credentialsLife=Lifetime.LONG,  \n    isStored=True, \n    isSourceEncryptedAtRest=False, \n    isDestEncryptedAtRest=True \n)\n\nresults = Data(\n    name=\"results\", \n    description=\"Results of insert op\",  \n    classification=Classification.SENSITIVE,  \n    isPII=False, \n    isCredentials=False,  \n    # credentialsLife=Lifetime.LONG,  \n    isStored=True, \n    isSourceEncryptedAtRest=False, \n    isDestEncryptedAtRest=True \n)\n\nmy_lambda = Lambda(\"cleanDBevery6hours\")\nmy_lambda.hasAccessControl = True\nmy_lambda.inBoundary = Web_DB\n\nmy_lambda_to_db = Dataflow(my_lambda, db, \"(\u0026lambda;)Periodically cleans DB\")\nmy_lambda_to_db.protocol = \"SQL\"\nmy_lambda_to_db.dstPort = 3306\n\nuser_to_web = Dataflow(user, web, \"User enters comments (*)\")\nuser_to_web.protocol = \"HTTP\"\nuser_to_web.dstPort = 80\nuser_to_web.data = comments\n\nweb_to_user = Dataflow(web, user, \"Comments saved (*)\")\nweb_to_user.protocol = \"HTTP\"\n\nweb_to_db = Dataflow(web, db, \"Insert query with comments\")\nweb_to_db.protocol = \"MySQL\"\nweb_to_db.dstPort = 3306\n\ndb_to_web = Dataflow(db, web, \"Comments contents\")\ndb_to_web.protocol = \"MySQL\"\ndb_to_web.data = results\n\ntm.process()\n\n```\n\nYou also have the option of using [pytmGPT](https://chat.openai.com/g/g-soISG24ix-pytmgpt) to create your models from prose!\n\n### Generating Diagrams\n\nDiagrams are output as [Dot](https://graphviz.gitlab.io/) and [PlantUML](https://plantuml.com/).\n\nWhen `--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:\n\n```bash\n\ntm.py --dfd | dot -Tpng -o sample.png\n\n```\n\nGenerates this diagram:\n\n![dfd.png](.gitbook/assets/dfd.png)\n\nAdding \".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\".\n\nThe following command generates a Sequence diagram.\n\n```bash\n\ntm.py --seq | java -Djava.awt.headless=true -jar plantuml.jar -tpng -pipe \u003e seq.png\n\n```\n\nGenerates this diagram:\n\n![seq.png](.gitbook/assets/seq.png)\n\n### Creating a Report\n\nThe diagrams and findings can be included in the template to create a final report:\n\n```bash\n\ntm.py --report docs/basic_template.md | pandoc -f markdown -t html \u003e report.html\n\n```\nThe templating format used in the report template is very simple:\n\n```text\n\n# Threat Model Sample\n***\n\n## System Description\n\n{tm.description}\n\n## Dataflow Diagram\n\n![Level 0 DFD](dfd.png)\n\n## Dataflows\n\nName|From|To |Data|Protocol|Port\n----|----|---|----|--------|----\n{dataflows:repeat:{{item.name}}|{{item.source.name}}|{{item.sink.name}}|{{item.data}}|{{item.protocol}}|{{item.dstPort}}\n}\n\n## Findings\n\n{findings:repeat:* {{item.description}} on element \"{{item.target}}\"\n}\n\n```\n\nTo group findings by elements, use a more advanced, nested loop:\n\n```text\n## Findings\n\n{elements:repeat:{{item.findings:if:\n### {{item.name}}\n\n{{item.findings:repeat:\n**Threat**: {{{{item.id}}}} - {{{{item.description}}}}\n\n**Severity**: {{{{item.severity}}}}\n\n**Mitigations**: {{{{item.mitigations}}}}\n\n**References**: {{{{item.references}}}}\n\n}}}}}\n```\n\nAll items inside a loop must be escaped, doubling the braces, so `{item.name}` becomes `{{item.name}}`.\nThe example above uses two nested loops, so items in the inner loop must be escaped twice, that's why they're using four braces.\n\n### Overrides\n\nYou 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:\n\n```python\nuser_to_web = Dataflow(user, web, \"User enters comments (*)\", protocol=\"HTTP\", dstPort=\"80\")\nuser_to_web.overrides = [\n    Finding(\n        # Overflow Buffers\n        threat_id=\"INP02\",\n        cvss=\"9.3\",\n        response=\"\"\"**To Mitigate**: run a memory sanitizer to validate the binary\"\"\",\n        severity=\"Very High\",\n    )\n]\n```\n\nIf you are adding a Finding, make sure to add a severity: \"Very High\", \"High\", \"Medium\", \"Low\", \"Very Low\".\n\n## Threats database\n\nFor the security practitioner, you may supply your own threats file by setting `TM.threatsFile`. It should contain entries like:\n\n```json\n{\n   \"SID\":\"INP01\",\n   \"target\": [\"Lambda\",\"Process\"],\n   \"description\": \"Buffer Overflow via Environment Variables\",\n   \"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.\",\n   \"Likelihood Of Attack\": \"High\",\n   \"severity\": \"High\",\n   \"condition\": \"target.usesEnvironmentVariables is True and target.controls.sanitizesInput is False and target.controls.checksInputBounds is False\",\n   \"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.\",\n   \"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.\",\n   \"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.\",\n   \"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\"\n }\n```\n\nThe `target` field lists classes of model elements to match this threat against.\nThose can be assets, like: Actor, Datastore, Server, Process, SetOfProcesses, ExternalEntity,\nLambda or Element, which is the base class and matches any. It can also be a Dataflow that connects two assets.\n\nAll other fields (except `condition`) are available for display and can be used in the template\nto list findings in the final [report](#report).\n\n\u003e **WARNING**\n\u003e\n\u003e The `threats.json` file contains strings that run through `eval()`. Make sure the file has correct permissions\n\u003e or risk having an attacker change the strings and cause you to run code on their behalf.\n\nThe logic lives in the `condition`, where members of `target` can be logically evaluated.\nReturning a true means the rule generates a finding, otherwise, it is not a finding.\nCondition may compare attributes of `target` and/or control attributes of the 'target.control' and also call one of these methods:\n\n* `target.oneOf(class, ...)` where `class` is one or more: Actor, Datastore, Server, Process, SetOfProcesses, ExternalEntity, Lambda or Dataflow,\n* `target.crosses(Boundary)`,\n* `target.enters(Boundary)`,\n* `target.exits(Boundary)`,\n* `target.inside(Boundary)`.\n\nIf `target` is a Dataflow, remember you can access `target.source` and/or `target.sink` along with other attributes.\n\nConditions on assets can analyze all incoming and outgoing Dataflows by inspecting\nthe `target.input` and `target.output` attributes. For example, to match a threat only against\nservers with incoming traffic, use `any(target.inputs)`. A more advanced example,\nmatching elements connecting to SQL datastores, would be `any(f.sink.oneOf(Datastore) and f.sink.isSQL for f in target.outputs)`.\n\n## Importing from JSON\n\nWith 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`. \n\n```python\n\n#!/usr/bin/env python3\n# Example tm2.py contents\n# Run: python tm2.py --dfd | dot -Tpng -o sample_json.png\n\nfrom pytm import (\n    TM,\n    Actor,\n    Boundary,\n    Classification,\n    Data,\n    Dataflow,\n    Datastore,\n    Lambda,\n    Server,\n    DatastoreType,\n    Assumption,\n    load,\n)\n\njson_file_string = './tests/input.json'\nwith open(json_file_string) as input_json:\n    TM.reset()\n    tm = load(input_json)\n    tm.process()\n\n```\n\nWe can call `tm2.py` the same way as we did before, here with `--dfd` and then redirect the output to Graphviz (`dot`): \n\n```bash\n\npython tm2.py --dfd | dot -Tpng -o sample_json.png\n\n```\n\n## Making slides!\n\nOnce 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.\n\n\n\nhttps://github.com/izar/pytm/assets/368769/30218241-c7cc-4085-91e9-bbec2843f838\n\n\n\n## Currently supported threats\n\n```text\nINP01 - Buffer Overflow via Environment Variables\nINP02 - Overflow Buffers\nINP03 - Server Side Include (SSI) Injection\nCR01 - Session Sidejacking\nINP04 - HTTP Request Splitting\nCR02 - Cross Site Tracing\nINP05 - Command Line Execution through SQL Injection\nINP06 - SQL Injection through SOAP Parameter Tampering\nSC01 - JSON Hijacking (aka JavaScript Hijacking)\nLB01 - API Manipulation\nAA01 - Authentication Abuse/ByPass\nDS01 - Excavation\nDE01 - Interception\nDE02 - Double Encoding\nAPI01 - Exploit Test APIs\nAC01 - Privilege Abuse\nINP07 - Buffer Manipulation\nAC02 - Shared Data Manipulation\nDO01 - Flooding\nHA01 - Path Traversal\nAC03 - Subverting Environment Variable Values\nDO02 - Excessive Allocation\nDS02 - Try All Common Switches\nINP08 - Format String Injection\nINP09 - LDAP Injection\nINP10 - Parameter Injection\nINP11 - Relative Path Traversal\nINP12 - Client-side Injection-induced Buffer Overflow\nAC04 - XML Schema Poisoning\nDO03 - XML Ping of the Death\nAC05 - Content Spoofing\nINP13 - Command Delimiters\nINP14 - Input Data Manipulation\nDE03 - Sniffing Attacks\nCR03 - Dictionary-based Password Attack\nAPI02 - Exploit Script-Based APIs\nHA02 - White Box Reverse Engineering\nDS03 - Footprinting\nAC06 - Using Malicious Files\nHA03 - Web Application Fingerprinting\nSC02 - XSS Targeting Non-Script Elements\nAC07 - Exploiting Incorrectly Configured Access Control Security Levels\nINP15 - IMAP/SMTP Command Injection\nHA04 - Reverse Engineering\nSC03 - Embedding Scripts within Scripts\nINP16 - PHP Remote File Inclusion\nAA02 - Principal Spoof\nCR04 - Session Credential Falsification through Forging\nDO04 - XML Entity Expansion\nDS04 - XSS Targeting Error Pages\nSC04 - XSS Using Alternate Syntax\nCR05 - Encryption Brute Forcing\nAC08 - Manipulate Registry Information\nDS05 - Lifting Sensitive Data Embedded in Cache\nSC05 - Removing Important Client Functionality\nINP17 - XSS Using MIME Type Mismatch\nAA03 - Exploitation of Trusted Credentials\nAC09 - Functionality Misuse\nINP18 - Fuzzing and observing application log data/errors for application mapping\nCR06 - Communication Channel Manipulation\nAC10 - Exploiting Incorrectly Configured SSL\nCR07 - XML Routing Detour Attacks\nAA04 - Exploiting Trust in Client\nCR08 - Client-Server Protocol Manipulation\nINP19 - XML External Entities Blowup\nINP20 - iFrame Overlay\nAC11 - Session Credential Falsification through Manipulation\nINP21 - DTD Injection\nINP22 - XML Attribute Blowup\nINP23 - File Content Injection\nDO05 - XML Nested Payloads\nAC12 - Privilege Escalation\nAC13 - Hijacking a privileged process\nAC14 - Catching exception throw/signal from privileged block\nINP24 - Filter Failure through Buffer Overflow\nINP25 - Resource Injection\nINP26 - Code Injection\nINP27 - XSS Targeting HTML Attributes\nINP28 - XSS Targeting URI Placeholders\nINP29 - XSS Using Doubled Characters\nINP30 - XSS Using Invalid Characters\nINP31 - Command Injection\nINP32 - XML Injection\nINP33 - Remote Code Inclusion\nINP34 - SOAP Array Overflow\nINP35 - Leverage Alternate Encoding\nDE04 - Audit Log Manipulation\nAC15 - Schema Poisoning\nINP36 - HTTP Response Smuggling\nINP37 - HTTP Request Smuggling\nINP38 - DOM-Based XSS\nAC16 - Session Credential Falsification through Prediction\nINP39 - Reflected XSS\nINP40 - Stored XSS\nAC17 - Session Hijacking - ServerSide\nAC18 - Session Hijacking - ClientSide\nINP41 - Argument Injection\nAC19 - Reusing Session IDs (aka Session Replay) - ServerSide\nAC20 - Reusing Session IDs (aka Session Replay) - ClientSide\nAC21 - Cross Site Request Forgery\n\n\n\n```\n","funding_links":[],"categories":["Tools","Python"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FOWASP%2Fpytm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FOWASP%2Fpytm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FOWASP%2Fpytm/lists"}