├── .dockerignore ├── .gitignore ├── LICENSE.txt ├── README.md ├── adminbot ├── Dockerfile ├── REQUIREMENTS-base.txt ├── REQUIREMENTS.txt ├── __init__.py ├── bot.py └── tasks.py ├── common └── static │ ├── css │ ├── baseline.css │ ├── custom-flex.css │ ├── custom-utility.css │ ├── fontawesome.css │ ├── normalize.css │ ├── pwnedhub.css │ ├── pwnedspa.css │ ├── skeleton-flexbox.css │ └── skeleton.css │ ├── fonts │ ├── Open-Sans-300 │ │ ├── LICENSE.txt │ │ ├── Open-Sans-300.eot │ │ ├── Open-Sans-300.svg │ │ ├── Open-Sans-300.ttf │ │ ├── Open-Sans-300.woff │ │ └── Open-Sans-300.woff2 │ ├── Open-Sans-600 │ │ ├── LICENSE.txt │ │ ├── Open-Sans-600.eot │ │ ├── Open-Sans-600.svg │ │ ├── Open-Sans-600.ttf │ │ ├── Open-Sans-600.woff │ │ └── Open-Sans-600.woff2 │ └── Open-Sans-regular │ │ ├── LICENSE.txt │ │ ├── Open-Sans-regular.eot │ │ ├── Open-Sans-regular.svg │ │ ├── Open-Sans-regular.ttf │ │ ├── Open-Sans-regular.woff │ │ └── Open-Sans-regular.woff2 │ ├── images │ ├── app-store.png │ ├── avatars │ │ ├── admin.png │ │ ├── bacon.png │ │ ├── c-man.png │ │ ├── default.png │ │ ├── kitty.jpg │ │ └── wolf.jpg │ ├── background-dark.jpg │ ├── background.jpg │ ├── get_flash_player.gif │ ├── google_signin.png │ ├── hub.svg │ ├── logo-filled.png │ ├── logo.png │ └── play-store.png │ ├── js │ └── tailwind-3.4.3.min.js │ └── webfonts │ ├── fa-brands-400.eot │ ├── fa-brands-400.svg │ ├── fa-brands-400.ttf │ ├── fa-brands-400.woff │ ├── fa-brands-400.woff2 │ ├── fa-regular-400.eot │ ├── fa-regular-400.svg │ ├── fa-regular-400.ttf │ ├── fa-regular-400.woff │ ├── fa-regular-400.woff2 │ ├── fa-solid-900.eot │ ├── fa-solid-900.svg │ ├── fa-solid-900.ttf │ ├── fa-solid-900.woff │ └── fa-solid-900.woff2 ├── database ├── cs │ ├── 01-init.sql │ ├── 02-pwnedhub.sql │ ├── 03-pwnedhub-test.sql │ └── 04-pwnedhub-admin.sql ├── ctf │ ├── 01-init.sql │ ├── 02-pwnedhub.sql │ ├── 03-pwnedhub-test.sql │ └── 04-pwnedhub-admin.sql └── init │ ├── 01-init.sql │ ├── 02-pwnedhub.sql │ ├── 03-pwnedhub-test.sql │ └── 04-pwnedhub-admin.sql ├── docker-compose.yaml ├── proxy ├── nginx.conf └── proxy_params ├── pwnedadmin ├── Dockerfile ├── REQUIREMENTS-base.txt ├── REQUIREMENTS.txt ├── __init__.py ├── config.py ├── constants.py ├── extensions.py ├── fixtures │ └── base │ │ └── configs.json ├── models.py ├── routes │ ├── config.py │ └── email.py ├── templates │ ├── config.html │ ├── emails.html │ └── layout.html ├── utils.py └── wsgi.py ├── pwnedapi ├── Dockerfile ├── REQUIREMENTS-base.txt ├── REQUIREMENTS.txt ├── __init__.py ├── config.py ├── constants.py ├── decorators.py ├── extensions.py ├── fixtures │ └── base │ │ ├── messages.json │ │ ├── rooms.json │ │ ├── tools.json │ │ └── users.json ├── models.py ├── routes │ ├── __init__.py │ ├── api.py │ └── websockets.py ├── static │ ├── openapi.json │ ├── postman.json │ └── swaggerui │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── index.css │ │ ├── index.html │ │ ├── oauth2-redirect.html │ │ ├── swagger-initializer.js │ │ ├── swagger-ui-bundle.js │ │ ├── swagger-ui-bundle.js.map │ │ ├── swagger-ui-es-bundle-core.js │ │ ├── swagger-ui-es-bundle-core.js.map │ │ ├── swagger-ui-es-bundle.js │ │ ├── swagger-ui-es-bundle.js.map │ │ ├── swagger-ui-standalone-preset.js │ │ ├── swagger-ui-standalone-preset.js.map │ │ ├── swagger-ui.css │ │ ├── swagger-ui.css.map │ │ ├── swagger-ui.js │ │ └── swagger-ui.js.map ├── tasks.py ├── utils.py ├── validators.py └── wsgi.py ├── pwnedhub ├── Dockerfile ├── REQUIREMENTS-base.txt ├── REQUIREMENTS.txt ├── __init__.py ├── config.py ├── constants.py ├── decorators.py ├── extensions.py ├── fixtures │ └── base │ │ ├── mail.json │ │ ├── messages.json │ │ ├── tools.json │ │ └── users.json ├── models.py ├── oauth.py ├── routes │ ├── __init__.py │ ├── auth.py │ ├── core.py │ └── errors.py ├── static │ ├── favicon.ico │ └── js │ │ ├── jquery-1.6.2.min.js │ │ ├── purify.min.js │ │ ├── pwnedhub.js │ │ └── showdown.js ├── templates │ ├── 404.html │ ├── 500.html │ ├── about.html │ ├── admin_tools.html │ ├── admin_users.html │ ├── artifacts.html │ ├── diagnostics.html │ ├── index.html │ ├── layout.html │ ├── login.html │ ├── macros.html │ ├── mail_compose.html │ ├── mail_inbox.html │ ├── mail_reply.html │ ├── mail_view.html │ ├── messages.html │ ├── mobile.html │ ├── notes.html │ ├── profile.html │ ├── profile_view.html │ ├── register.html │ ├── reset_init.html │ ├── reset_password.html │ ├── reset_question.html │ └── tools.html ├── utils.py ├── validators.py └── wsgi.py ├── pwnedspa ├── Dockerfile ├── REQUIREMENTS-base.txt ├── REQUIREMENTS.txt ├── __init__.py ├── config.py ├── routes │ └── core.py ├── static │ ├── favicon.ico │ └── vue │ │ ├── app.js │ │ ├── components │ │ ├── background.css │ │ ├── background.js │ │ ├── google-login.js │ │ ├── link-preview.css │ │ ├── link-preview.js │ │ ├── modal.css │ │ ├── modal.js │ │ ├── navigation.css │ │ ├── navigation.js │ │ ├── password-field.js │ │ ├── toasts.css │ │ └── toasts.js │ │ ├── helpers │ │ ├── fetch-wrapper.js │ │ └── socket.js │ │ ├── libs │ │ ├── marked.js │ │ ├── pinia.js │ │ ├── socket.io.js │ │ ├── vue-demi.js │ │ ├── vue-router.js │ │ ├── vue.js │ │ └── vue3-infinite-loading.js │ │ ├── main.js │ │ ├── modals │ │ └── scans-modal.js │ │ ├── router.js │ │ ├── services │ │ └── api.js │ │ ├── stores │ │ ├── app-store.js │ │ └── auth-store.js │ │ ├── style.css │ │ └── views │ │ ├── account.css │ │ ├── account.js │ │ ├── activate.js │ │ ├── login.css │ │ ├── login.js │ │ ├── messages.css │ │ ├── messages.js │ │ ├── notes.css │ │ ├── notes.js │ │ ├── passwordless.css │ │ ├── passwordless.js │ │ ├── profile.css │ │ ├── profile.js │ │ ├── reset.css │ │ ├── scans.css │ │ ├── scans.js │ │ ├── signup.css │ │ ├── signup.js │ │ ├── tools.css │ │ ├── tools.js │ │ ├── users.css │ │ └── users.js ├── templates │ └── spa.html └── wsgi.py └── pwnedsso ├── Dockerfile ├── REQUIREMENTS-base.txt ├── REQUIREMENTS.txt ├── __init__.py ├── config.py ├── extensions.py ├── models.py ├── routes ├── __init__.py └── sso.py ├── utils.py └── wsgi.py /.dockerignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | **/.venv 3 | **/.classpath 4 | **/.dockerignore 5 | **/.env 6 | **/.git 7 | **/.gitignore 8 | **/.project 9 | **/.settings 10 | **/.toolstarget 11 | **/.vs 12 | **/.vscode 13 | **/*.*proj.user 14 | **/*.dbmdl 15 | **/*.jfm 16 | **/bin 17 | **/charts 18 | **/docker-compose* 19 | **/compose* 20 | **/Dockerfile* 21 | **/node_modules 22 | **/npm-debug.log 23 | **/obj 24 | **/secrets.dev.yaml 25 | **/values.dev.yaml 26 | LICENSE.txt 27 | README.md 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.db 4 | *sublime* 5 | venv/ 6 | unused/ 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2015-2024 Practical Security Services LLC 2 | 3 | This software is intended for private use only. This license prohibits the distibution, modification, sublicensing and/or commercial use of this software. 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | PwnedHub is a vulnerable application designed exclusively for [PractiSec training courses](https://www.practisec.com/training/). PwnedHub contains intentional vulnerability and should never be exposed to the open Internet. This software is NOT Open Source in a traditional sense. See the `LICENSE.txt` file for more information. 4 | 5 | ## Requirements 6 | 7 | * Docker 8 | 9 | ## Installation and Usage 10 | 11 | 1. Install Docker Desktop. 12 | 2. Clone the PwnedHub repository. 13 | 14 | ``` 15 | $ git clone https://github.com/lanmaster53/pwnedhub.git 16 | ``` 17 | 18 | 3. Change into the PwnedHub directory. 19 | 20 | ``` 21 | $ cd pwnedhub 22 | ``` 23 | 24 | 4. Build the PwnedHub Docker images. 25 | 26 | ``` 27 | docker compose build 28 | ``` 29 | 30 | 5. Launch the PwnedHub environment using Docker Compose. 31 | 32 | ``` 33 | docker compose up 34 | ``` 35 | 36 | * To launch as a daemon (no terminal logging), add the `-d` switch. 37 | 38 | 6. Modify the hosts file to create the following records: 39 | 40 | ``` 41 | 127.0.0.1 www.pwnedhub.com 42 | 127.0.0.1 sso.pwnedhub.com 43 | 127.0.0.1 test.pwnedhub.com 44 | 127.0.0.1 api.pwnedhub.com 45 | 127.0.0.1 admin.pwnedhub.com 46 | ``` 47 | 48 | 7. Access the various target applications and interfaces: 49 | * http://www.pwnedhub.com 50 | * http://test.pwnedhub.com 51 | * http://api.pwnedhub.com/static/swaggerui/index.html 52 | * http://api.pwnedhub.com/static/postman.json (for use with [Postman](https://www.postman.com/)) 53 | 8. When done using PwnedHub, shut down the Docker environment with the following command: 54 | 55 | ``` 56 | docker compose down 57 | ``` 58 | 59 | ## Information 60 | 61 | The PwnedHub environment includes several resources that are not targets. 62 | 63 | * http://admin.pwnedhub.com/inbox/ - A webmail interface for receiving email from out-of-band systems. PwnedHub does not send email to external mail services, so when an application sends an email, this is where the user will receive it. 64 | * http://admin.pwnedhub.com/config/ - A configuration interface for enabling/disabling security controls and features. Modifying these settings change how the target applications behave. 65 | -------------------------------------------------------------------------------- /adminbot/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-alpine 2 | 3 | ENV BUILD_DEPS="" 4 | ENV RUNTIME_DEPS="firefox" 5 | 6 | ENV PYTHONDONTWRITEBYTECODE=1 7 | ENV PYTHONUNBUFFERED=1 8 | 9 | RUN mkdir -p /src 10 | 11 | WORKDIR /src 12 | 13 | ADD ./REQUIREMENTS.txt /src/REQUIREMENTS.txt 14 | 15 | RUN apk update &&\ 16 | apk add --no-cache $BUILD_DEPS $RUNTIME_DEPS &&\ 17 | pip install --no-cache-dir --upgrade pip &&\ 18 | pip install --no-cache-dir -r REQUIREMENTS.txt &&\ 19 | apk del $BUILD_DEPS &&\ 20 | rm -rf /var/cache/apk/* 21 | -------------------------------------------------------------------------------- /adminbot/REQUIREMENTS-base.txt: -------------------------------------------------------------------------------- 1 | selenium 2 | rq 3 | -------------------------------------------------------------------------------- /adminbot/REQUIREMENTS.txt: -------------------------------------------------------------------------------- 1 | async-timeout==4.0.2 2 | attrs==23.1.0 3 | certifi==2023.5.7 4 | click==8.1.4 5 | exceptiongroup==1.1.2 6 | h11==0.14.0 7 | idna==3.4 8 | outcome==1.2.0 9 | PySocks==1.7.1 10 | redis==4.6.0 11 | rq==1.15.1 12 | selenium==4.10.0 13 | sniffio==1.3.0 14 | sortedcontainers==2.4.0 15 | trio==0.22.1 16 | trio-websocket==0.10.3 17 | urllib3==2.0.3 18 | wsproto==1.2.0 19 | -------------------------------------------------------------------------------- /adminbot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/adminbot/__init__.py -------------------------------------------------------------------------------- /adminbot/tasks.py: -------------------------------------------------------------------------------- 1 | from adminbot.bot import bot_driver, HubBot, Hub20Bot 2 | 3 | def www_login_read_first_mail_respond(name, username, password, receiver_id, subject, content): 4 | with bot_driver() as driver: 5 | bot = HubBot(driver, name) 6 | # login 7 | bot.log_in(username, password) 8 | # read first mail 9 | bot.read_mail() 10 | # respond 11 | bot.compose_mail(receiver_id, subject, content) 12 | 13 | def test_login_send_private_message(name, email, room_id, message): 14 | with bot_driver() as driver: 15 | bot = Hub20Bot(driver, name) 16 | # login 17 | bot.log_in(email) 18 | # send private message 19 | bot.send_private_message(room_id, message) 20 | -------------------------------------------------------------------------------- /common/static/css/custom-flex.css: -------------------------------------------------------------------------------- 1 | /*! custom-flex.css v1.0.0 */ 2 | 3 | html, body, #app { 4 | height: 100%; 5 | } 6 | 7 | /* 8 | flex item default = flex: 0 1 auto 9 | */ 10 | 11 | /* flex classes */ 12 | 13 | .flex-row { 14 | display: flex; 15 | flex-direction: row; 16 | } 17 | 18 | .flex-row-reverse { 19 | display: flex; 20 | flex-direction: row-reverse; 21 | } 22 | 23 | .flex-column { 24 | display: flex; 25 | flex-direction: column; 26 | } 27 | 28 | .flex-column-reverse { 29 | display: flex; 30 | flex-direction: column-reverse; 31 | } 32 | 33 | .flex-no-basis { 34 | flex-basis: 0; 35 | } 36 | 37 | .flex-no-shrink { 38 | flex-shrink: 0; 39 | } 40 | 41 | .flex-grow { 42 | flex-grow: 1; 43 | } 44 | 45 | .flex-shrink { 46 | flex-shrink: 1; 47 | } 48 | 49 | .flex-both { 50 | flex-grow: 1; 51 | flex-shrink: 1; 52 | } 53 | 54 | .flex-align-center { 55 | align-items: center; 56 | } 57 | 58 | .flex-justify-center { 59 | justify-content: center; 60 | } 61 | 62 | .flex-justify-end { 63 | justify-content: flex-end; 64 | } 65 | 66 | .flex-justify-space-between { 67 | justify-content: space-between; 68 | } 69 | 70 | .flex-justify-space-around { 71 | justify-content: space-around; 72 | } 73 | 74 | .flex-justify-space-evenly { 75 | justify-content: space-evenly; 76 | } 77 | 78 | .flex-wrap { 79 | flex-wrap: wrap; 80 | } 81 | 82 | .flex-break { 83 | flex-basis: 100%; 84 | height: 0; 85 | } 86 | 87 | /* grid 1/10/1 */ 88 | 89 | .flex-width-10 { 90 | max-width: 83.33333333%; 91 | } 92 | 93 | .flex-basis-10 { 94 | flex-basis: 83.33333333%; 95 | } 96 | 97 | .flex-offset-1 { 98 | margin-left: 8.33333333%; 99 | } 100 | 101 | /* grid 2/8/8 */ 102 | 103 | .flex-width-8 { 104 | max-width: 66.66666667%; 105 | } 106 | 107 | .flex-basis-8 { 108 | flex-basis: 66.66666667%; 109 | } 110 | 111 | .flex-offset-2 { 112 | margin-left: 16.66666667%; 113 | } 114 | 115 | /* grid 3/6/4 */ 116 | 117 | .flex-width-6 { 118 | max-width: 50%; 119 | } 120 | 121 | .flex-basis-6 { 122 | flex-basis: 50%; 123 | } 124 | 125 | .flex-offset-3 { 126 | margin-left: 25%; 127 | } 128 | 129 | /* grid 4/4/4 */ 130 | 131 | .flex-width-4 { 132 | max-width: 33.33333333%; 133 | } 134 | 135 | .flex-basis-4 { 136 | flex-basis: 33.33333333%; 137 | } 138 | 139 | .flex-offset-4 { 140 | margin-left: 33.33333333%; 141 | } 142 | -------------------------------------------------------------------------------- /common/static/css/custom-utility.css: -------------------------------------------------------------------------------- 1 | /*! custom-utility.css v1.0.0 */ 2 | 3 | .center { 4 | margin-left: auto; 5 | margin-right: auto; 6 | } 7 | 8 | .center-content { 9 | text-align: center; 10 | } 11 | 12 | .left-content { 13 | text-align: left; 14 | } 15 | 16 | .right-content { 17 | text-align: right; 18 | } 19 | 20 | .clear { 21 | clear: both; 22 | } 23 | 24 | .bolded { 25 | font-weight: bold; 26 | } 27 | 28 | .rounded { 29 | -webkit-border-radius: .5rem; 30 | -moz-border-radius: .5rem; 31 | border-radius: .5rem; 32 | } 33 | 34 | .circular { 35 | -webkit-border-radius: 50%; 36 | -moz-border-radius: 50%; 37 | border-radius: 50%; 38 | } 39 | 40 | .shaded { 41 | -webkit-box-shadow: 0 0 1rem rgba(0, 0, 0, 0.2); 42 | -moz-box-shadow: 0 0 1rem rgba(0, 0, 0, 0.2); 43 | box-shadow: 0 0 1rem rgba(0, 0, 0, 0.2); 44 | } 45 | 46 | .shaded-light { 47 | -webkit-box-shadow: 0.5rem 0.5rem 1.5rem rgba(0, 0, 0, 0.1); 48 | -moz-box-shadow: 0.5rem 0.5rem 1.5rem rgba(0, 0, 0, 0.1); 49 | box-shadow: 0.5rem 0.5rem 1.5rem rgba(0, 0, 0, 0.1); 50 | } 51 | 52 | .gutter-right { 53 | margin-right: 1rem; 54 | } 55 | 56 | .gutter-half-right { 57 | margin-right: 0.5; 58 | } 59 | 60 | .gutter-bottom { 61 | margin-bottom: 1rem; 62 | } 63 | 64 | .gutter-half-bottom { 65 | margin-bottom: 0.5; 66 | } 67 | 68 | .gutter-left { 69 | margin-left: 1rem; 70 | } 71 | 72 | .gutter-half-left { 73 | margin-left: 0.5; 74 | } 75 | 76 | /* 77 | The following: 78 | * Creates issues when the parent already has assigned margins. 79 | * Doesn't allow for a border when using border box. 80 | 81 | Both of these can be fixed by using a wrapper between the 82 | parent and child and making the wrapper the parent. 83 | */ 84 | 85 | .gutter-parent { 86 | margin-left: -0.5rem; 87 | margin-right: -0.5rem; 88 | } 89 | 90 | .gutter-child { 91 | padding-left: 0.5rem; 92 | padding-right: 0.5rem; 93 | } 94 | -------------------------------------------------------------------------------- /common/static/fonts/Open-Sans-300/Open-Sans-300.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/fonts/Open-Sans-300/Open-Sans-300.eot -------------------------------------------------------------------------------- /common/static/fonts/Open-Sans-300/Open-Sans-300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/fonts/Open-Sans-300/Open-Sans-300.ttf -------------------------------------------------------------------------------- /common/static/fonts/Open-Sans-300/Open-Sans-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/fonts/Open-Sans-300/Open-Sans-300.woff -------------------------------------------------------------------------------- /common/static/fonts/Open-Sans-300/Open-Sans-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/fonts/Open-Sans-300/Open-Sans-300.woff2 -------------------------------------------------------------------------------- /common/static/fonts/Open-Sans-600/Open-Sans-600.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/fonts/Open-Sans-600/Open-Sans-600.eot -------------------------------------------------------------------------------- /common/static/fonts/Open-Sans-600/Open-Sans-600.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/fonts/Open-Sans-600/Open-Sans-600.ttf -------------------------------------------------------------------------------- /common/static/fonts/Open-Sans-600/Open-Sans-600.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/fonts/Open-Sans-600/Open-Sans-600.woff -------------------------------------------------------------------------------- /common/static/fonts/Open-Sans-600/Open-Sans-600.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/fonts/Open-Sans-600/Open-Sans-600.woff2 -------------------------------------------------------------------------------- /common/static/fonts/Open-Sans-regular/Open-Sans-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/fonts/Open-Sans-regular/Open-Sans-regular.eot -------------------------------------------------------------------------------- /common/static/fonts/Open-Sans-regular/Open-Sans-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/fonts/Open-Sans-regular/Open-Sans-regular.ttf -------------------------------------------------------------------------------- /common/static/fonts/Open-Sans-regular/Open-Sans-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/fonts/Open-Sans-regular/Open-Sans-regular.woff -------------------------------------------------------------------------------- /common/static/fonts/Open-Sans-regular/Open-Sans-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/fonts/Open-Sans-regular/Open-Sans-regular.woff2 -------------------------------------------------------------------------------- /common/static/images/app-store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/images/app-store.png -------------------------------------------------------------------------------- /common/static/images/avatars/admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/images/avatars/admin.png -------------------------------------------------------------------------------- /common/static/images/avatars/bacon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/images/avatars/bacon.png -------------------------------------------------------------------------------- /common/static/images/avatars/c-man.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/images/avatars/c-man.png -------------------------------------------------------------------------------- /common/static/images/avatars/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/images/avatars/default.png -------------------------------------------------------------------------------- /common/static/images/avatars/kitty.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/images/avatars/kitty.jpg -------------------------------------------------------------------------------- /common/static/images/avatars/wolf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/images/avatars/wolf.jpg -------------------------------------------------------------------------------- /common/static/images/background-dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/images/background-dark.jpg -------------------------------------------------------------------------------- /common/static/images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/images/background.jpg -------------------------------------------------------------------------------- /common/static/images/get_flash_player.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/images/get_flash_player.gif -------------------------------------------------------------------------------- /common/static/images/google_signin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/images/google_signin.png -------------------------------------------------------------------------------- /common/static/images/hub.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 10 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /common/static/images/logo-filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/images/logo-filled.png -------------------------------------------------------------------------------- /common/static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/images/logo.png -------------------------------------------------------------------------------- /common/static/images/play-store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/images/play-store.png -------------------------------------------------------------------------------- /common/static/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /common/static/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /common/static/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /common/static/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /common/static/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /common/static/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /common/static/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /common/static/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /common/static/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /common/static/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /common/static/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /common/static/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/common/static/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /database/cs/01-init.sql: -------------------------------------------------------------------------------- 1 | -- Setup databases and users 2 | CREATE DATABASE IF NOT EXISTS `pwnedhub`; 3 | CREATE DATABASE IF NOT EXISTS `pwnedhub-test`; 4 | CREATE DATABASE IF NOT EXISTS `pwnedhub-admin`; 5 | CREATE USER 'pwnedhub'@'%' IDENTIFIED BY 'dbconnectpass'; 6 | GRANT ALL PRIVILEGES ON `pwnedhub` . * TO 'pwnedhub'@'%'; 7 | GRANT ALL PRIVILEGES ON `pwnedhub-test` . * TO 'pwnedhub'@'%'; 8 | GRANT ALL PRIVILEGES ON `pwnedhub-admin` . * TO 'pwnedhub'@'%'; 9 | -------------------------------------------------------------------------------- /database/cs/04-pwnedhub-admin.sql: -------------------------------------------------------------------------------- 1 | -- Attach to database 2 | USE `pwnedhub-admin`; 3 | 4 | -- MySQL dump 10.13 Distrib 8.0.33, for Linux (x86_64) 5 | -- 6 | -- Host: localhost Database: pwnedhub-admin 7 | -- ------------------------------------------------------ 8 | -- Server version 8.0.33 9 | 10 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 11 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 12 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 13 | /*!50503 SET NAMES utf8mb4 */; 14 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 15 | /*!40103 SET TIME_ZONE='+00:00' */; 16 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 17 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 18 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 19 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 20 | 21 | -- 22 | -- Table structure for table `configs` 23 | -- 24 | 25 | DROP TABLE IF EXISTS `configs`; 26 | /*!40101 SET @saved_cs_client = @@character_set_client */; 27 | /*!50503 SET character_set_client = utf8mb4 */; 28 | CREATE TABLE `configs` ( 29 | `id` int NOT NULL AUTO_INCREMENT, 30 | `name` varchar(255) NOT NULL, 31 | `description` text NOT NULL, 32 | `type` varchar(255) NOT NULL, 33 | `value` tinyint(1) NOT NULL, 34 | PRIMARY KEY (`id`) 35 | ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 36 | /*!40101 SET character_set_client = @saved_cs_client */; 37 | 38 | -- 39 | -- Dumping data for table `configs` 40 | -- 41 | 42 | LOCK TABLES `configs` WRITE; 43 | /*!40000 ALTER TABLE `configs` DISABLE KEYS */; 44 | INSERT INTO `configs` VALUES (1,'CSRF_PROTECT','Profile CSRF Protection (PwnedHub)','security control',1),(2,'OSCI_PROTECT','Tools OSCI Protection (PwnedHub)','security control',0),(3,'SQLI_PROTECT','Login SQLi Protection (PwnedHub)','security control',0),(4,'CSP_PROTECT','Content Security Policy (PwnedHub)','security control',0),(5,'CORS_RESTRICT','Restricted CORS (PwnedAPI)','security control',1),(6,'JWT_VERIFY','Verify JWT Signatures (PwnedAPI)','security control',1),(7,'JWT_ENCRYPT','Encrypt JWTs (PwnedAPI)','security control',0),(8,'BEARER_AUTH_ENABLE','Bearer Token Authentication (PwnedAPI)','feature',1),(9,'OIDC_ENABLE','OpenID Connect Authentication (PwnedHub)','feature',0),(10,'SSO_ENABLE','SSO Authentication (PwnedHub)','feature',0),(11,'OOB_RESET_ENABLE','Out-of-Band Password Reset (PwnedHub)','feature',0),(12,'CTF_MODE','CTF Mode (Warning: Disables this interface!)','feature',0); 45 | /*!40000 ALTER TABLE `configs` ENABLE KEYS */; 46 | UNLOCK TABLES; 47 | 48 | -- 49 | -- Table structure for table `emails` 50 | -- 51 | 52 | DROP TABLE IF EXISTS `emails`; 53 | /*!40101 SET @saved_cs_client = @@character_set_client */; 54 | /*!50503 SET character_set_client = utf8mb4 */; 55 | CREATE TABLE `emails` ( 56 | `id` int NOT NULL AUTO_INCREMENT, 57 | `created` datetime NOT NULL, 58 | `sender` varchar(255) NOT NULL, 59 | `receiver` varchar(255) NOT NULL, 60 | `subject` text NOT NULL, 61 | `body` text NOT NULL, 62 | PRIMARY KEY (`id`) 63 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 64 | /*!40101 SET character_set_client = @saved_cs_client */; 65 | 66 | -- 67 | -- Dumping data for table `emails` 68 | -- 69 | 70 | LOCK TABLES `emails` WRITE; 71 | /*!40000 ALTER TABLE `emails` DISABLE KEYS */; 72 | /*!40000 ALTER TABLE `emails` ENABLE KEYS */; 73 | UNLOCK TABLES; 74 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 75 | 76 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 77 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 78 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 79 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 80 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 81 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 82 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 83 | 84 | -- Dump completed on 2023-07-17 4:14:26 85 | -------------------------------------------------------------------------------- /database/ctf/01-init.sql: -------------------------------------------------------------------------------- 1 | -- Setup databases and users 2 | CREATE DATABASE IF NOT EXISTS `pwnedhub`; 3 | CREATE DATABASE IF NOT EXISTS `pwnedhub-test`; 4 | CREATE DATABASE IF NOT EXISTS `pwnedhub-admin`; 5 | CREATE USER 'pwnedhub'@'%' IDENTIFIED BY 'dbconnectpass'; 6 | GRANT ALL PRIVILEGES ON `pwnedhub` . * TO 'pwnedhub'@'%'; 7 | GRANT ALL PRIVILEGES ON `pwnedhub-test` . * TO 'pwnedhub'@'%'; 8 | GRANT ALL PRIVILEGES ON `pwnedhub-admin` . * TO 'pwnedhub'@'%'; 9 | -------------------------------------------------------------------------------- /database/ctf/04-pwnedhub-admin.sql: -------------------------------------------------------------------------------- 1 | -- Attach to database 2 | USE `pwnedhub-admin`; 3 | 4 | -- MySQL dump 10.13 Distrib 8.0.33, for Linux (x86_64) 5 | -- 6 | -- Host: localhost Database: pwnedhub-admin 7 | -- ------------------------------------------------------ 8 | -- Server version 8.0.33 9 | 10 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 11 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 12 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 13 | /*!50503 SET NAMES utf8mb4 */; 14 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 15 | /*!40103 SET TIME_ZONE='+00:00' */; 16 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 17 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 18 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 19 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 20 | 21 | -- 22 | -- Table structure for table `configs` 23 | -- 24 | 25 | DROP TABLE IF EXISTS `configs`; 26 | /*!40101 SET @saved_cs_client = @@character_set_client */; 27 | /*!50503 SET character_set_client = utf8mb4 */; 28 | CREATE TABLE `configs` ( 29 | `id` int NOT NULL AUTO_INCREMENT, 30 | `name` varchar(255) NOT NULL, 31 | `description` text NOT NULL, 32 | `type` varchar(255) NOT NULL, 33 | `value` tinyint(1) NOT NULL, 34 | PRIMARY KEY (`id`) 35 | ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 36 | /*!40101 SET character_set_client = @saved_cs_client */; 37 | 38 | -- 39 | -- Dumping data for table `configs` 40 | -- 41 | 42 | LOCK TABLES `configs` WRITE; 43 | /*!40000 ALTER TABLE `configs` DISABLE KEYS */; 44 | INSERT INTO `configs` VALUES (1,'CSRF_PROTECT','Profile CSRF Protection (PwnedHub)','security control',0),(2,'OSCI_PROTECT','Tools OSCI Protection (PwnedHub)','security control',1),(3,'SQLI_PROTECT','Login SQLi Protection (PwnedHub)','security control',1),(4,'CSP_PROTECT','Content Security Policy (PwnedHub)','security control',1),(5,'CORS_RESTRICT','Restricted CORS (PwnedAPI)','security control',0),(6,'JWT_VERIFY','Verify JWT Signatures (PwnedAPI)','security control',1),(7,'JWT_ENCRYPT','Encrypt JWTs (PwnedAPI)','security control',0),(8,'BEARER_AUTH_ENABLE','Bearer Token Authentication (PwnedAPI)','feature',0),(9,'OIDC_ENABLE','OpenID Connect Authentication (PwnedHub)','feature',0),(10,'SSO_ENABLE','SSO Authentication (PwnedHub)','feature',0),(11,'OOB_RESET_ENABLE','Out-of-Band Password Reset (PwnedHub)','feature',1),(12,'CTF_MODE','CTF Mode (Warning: Disables this interface!)','feature',1); 45 | /*!40000 ALTER TABLE `configs` ENABLE KEYS */; 46 | UNLOCK TABLES; 47 | 48 | -- 49 | -- Table structure for table `emails` 50 | -- 51 | 52 | DROP TABLE IF EXISTS `emails`; 53 | /*!40101 SET @saved_cs_client = @@character_set_client */; 54 | /*!50503 SET character_set_client = utf8mb4 */; 55 | CREATE TABLE `emails` ( 56 | `id` int NOT NULL AUTO_INCREMENT, 57 | `created` datetime NOT NULL, 58 | `sender` varchar(255) NOT NULL, 59 | `receiver` varchar(255) NOT NULL, 60 | `subject` text NOT NULL, 61 | `body` text NOT NULL, 62 | PRIMARY KEY (`id`) 63 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 64 | /*!40101 SET character_set_client = @saved_cs_client */; 65 | 66 | -- 67 | -- Dumping data for table `emails` 68 | -- 69 | 70 | LOCK TABLES `emails` WRITE; 71 | /*!40000 ALTER TABLE `emails` DISABLE KEYS */; 72 | /*!40000 ALTER TABLE `emails` ENABLE KEYS */; 73 | UNLOCK TABLES; 74 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 75 | 76 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 77 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 78 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 79 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 80 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 81 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 82 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 83 | 84 | -- Dump completed on 2023-07-17 4:14:26 85 | -------------------------------------------------------------------------------- /database/init/01-init.sql: -------------------------------------------------------------------------------- 1 | -- Setup databases and users 2 | CREATE DATABASE IF NOT EXISTS `pwnedhub`; 3 | CREATE DATABASE IF NOT EXISTS `pwnedhub-test`; 4 | CREATE DATABASE IF NOT EXISTS `pwnedhub-admin`; 5 | CREATE USER 'pwnedhub'@'%' IDENTIFIED BY 'dbconnectpass'; 6 | GRANT ALL PRIVILEGES ON `pwnedhub` . * TO 'pwnedhub'@'%'; 7 | GRANT ALL PRIVILEGES ON `pwnedhub-test` . * TO 'pwnedhub'@'%'; 8 | GRANT ALL PRIVILEGES ON `pwnedhub-admin` . * TO 'pwnedhub'@'%'; 9 | -------------------------------------------------------------------------------- /database/init/04-pwnedhub-admin.sql: -------------------------------------------------------------------------------- 1 | -- Attach to database 2 | USE `pwnedhub-admin`; 3 | 4 | -- MySQL dump 10.13 Distrib 8.0.33, for Linux (x86_64) 5 | -- 6 | -- Host: localhost Database: pwnedhub-admin 7 | -- ------------------------------------------------------ 8 | -- Server version 8.0.33 9 | 10 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 11 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 12 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 13 | /*!50503 SET NAMES utf8mb4 */; 14 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 15 | /*!40103 SET TIME_ZONE='+00:00' */; 16 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 17 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 18 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 19 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 20 | 21 | -- 22 | -- Table structure for table `configs` 23 | -- 24 | 25 | DROP TABLE IF EXISTS `configs`; 26 | /*!40101 SET @saved_cs_client = @@character_set_client */; 27 | /*!50503 SET character_set_client = utf8mb4 */; 28 | CREATE TABLE `configs` ( 29 | `id` int NOT NULL AUTO_INCREMENT, 30 | `name` varchar(255) NOT NULL, 31 | `description` text NOT NULL, 32 | `type` varchar(255) NOT NULL, 33 | `value` tinyint(1) NOT NULL, 34 | PRIMARY KEY (`id`) 35 | ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 36 | /*!40101 SET character_set_client = @saved_cs_client */; 37 | 38 | -- 39 | -- Dumping data for table `configs` 40 | -- 41 | 42 | LOCK TABLES `configs` WRITE; 43 | /*!40000 ALTER TABLE `configs` DISABLE KEYS */; 44 | INSERT INTO `configs` VALUES (1,'CSRF_PROTECT','Profile CSRF Protection (PwnedHub)','security control',1),(2,'OSCI_PROTECT','Tools OSCI Protection (PwnedHub)','security control',0),(3,'SQLI_PROTECT','Login SQLi Protection (PwnedHub)','security control',0),(4,'CSP_PROTECT','Content Security Policy (PwnedHub)','security control',0),(5,'CORS_RESTRICT','Restricted CORS (PwnedAPI)','security control',1),(6,'JWT_VERIFY','Verify JWT Signatures (PwnedAPI)','security control',1),(7,'JWT_ENCRYPT','Encrypt JWTs (PwnedAPI)','security control',0),(8,'BEARER_AUTH_ENABLE','Bearer Token Authentication (PwnedAPI)','feature',1),(9,'OIDC_ENABLE','OpenID Connect Authentication (PwnedHub)','feature',0),(10,'SSO_ENABLE','SSO Authentication (PwnedHub)','feature',0),(11,'OOB_RESET_ENABLE','Out-of-Band Password Reset (PwnedHub)','feature',0),(12,'CTF_MODE','CTF Mode (Warning: Disables this interface!)','feature',0); 45 | /*!40000 ALTER TABLE `configs` ENABLE KEYS */; 46 | UNLOCK TABLES; 47 | 48 | -- 49 | -- Table structure for table `emails` 50 | -- 51 | 52 | DROP TABLE IF EXISTS `emails`; 53 | /*!40101 SET @saved_cs_client = @@character_set_client */; 54 | /*!50503 SET character_set_client = utf8mb4 */; 55 | CREATE TABLE `emails` ( 56 | `id` int NOT NULL AUTO_INCREMENT, 57 | `created` datetime NOT NULL, 58 | `sender` varchar(255) NOT NULL, 59 | `receiver` varchar(255) NOT NULL, 60 | `subject` text NOT NULL, 61 | `body` text NOT NULL, 62 | PRIMARY KEY (`id`) 63 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 64 | /*!40101 SET character_set_client = @saved_cs_client */; 65 | 66 | -- 67 | -- Dumping data for table `emails` 68 | -- 69 | 70 | LOCK TABLES `emails` WRITE; 71 | /*!40000 ALTER TABLE `emails` DISABLE KEYS */; 72 | /*!40000 ALTER TABLE `emails` ENABLE KEYS */; 73 | UNLOCK TABLES; 74 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 75 | 76 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 77 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 78 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 79 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 80 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 81 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 82 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 83 | 84 | -- Dump completed on 2023-07-17 4:14:26 85 | -------------------------------------------------------------------------------- /proxy/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | 9 | include mime.types; 10 | default_type application/octet-stream; 11 | 12 | sendfile on; 13 | 14 | keepalive_timeout 65; 15 | 16 | server { 17 | listen 80; 18 | server_name localhost; 19 | 20 | location / { 21 | root html; 22 | index index.html index.htm; 23 | } 24 | 25 | error_page 500 502 503 504 /50x.html; 26 | location = /50x.html { 27 | root html; 28 | } 29 | } 30 | 31 | upstream app_server { 32 | server app:80 fail_timeout=0; 33 | } 34 | 35 | server { 36 | listen 80; 37 | server_name www.pwnedhub.com; 38 | 39 | location / { 40 | include proxy_params; 41 | proxy_pass http://app_server; 42 | } 43 | } 44 | 45 | upstream sso_server { 46 | server sso:80 fail_timeout=0; 47 | } 48 | 49 | server { 50 | listen 80; 51 | server_name sso.pwnedhub.com; 52 | 53 | location / { 54 | include proxy_params; 55 | proxy_pass http://sso_server; 56 | } 57 | } 58 | 59 | upstream spa_server { 60 | server spa:80 fail_timeout=0; 61 | } 62 | 63 | server { 64 | listen 80; 65 | server_name test.pwnedhub.com; 66 | 67 | location / { 68 | include proxy_params; 69 | proxy_pass http://spa_server; 70 | } 71 | } 72 | 73 | upstream api_server { 74 | server api:80 fail_timeout=0; 75 | } 76 | 77 | server { 78 | listen 80; 79 | server_name api.pwnedhub.com; 80 | 81 | location / { 82 | include proxy_params; 83 | proxy_pass http://api_server; 84 | } 85 | 86 | location /socket.io { 87 | include proxy_params; 88 | proxy_http_version 1.1; 89 | proxy_buffering off; 90 | proxy_set_header Upgrade $http_upgrade; 91 | proxy_set_header Connection "Upgrade"; 92 | proxy_pass http://api_server/socket.io; 93 | } 94 | } 95 | 96 | upstream admin_server { 97 | server admin:80 fail_timeout=0; 98 | } 99 | 100 | server { 101 | listen 80; 102 | server_name admin.pwnedhub.com; 103 | 104 | location / { 105 | include proxy_params; 106 | proxy_pass http://admin_server; 107 | } 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /proxy/proxy_params: -------------------------------------------------------------------------------- 1 | proxy_set_header Host $http_host; 2 | proxy_set_header X-Real-IP $remote_addr; 3 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 4 | proxy_set_header X-Forwarded-Proto $scheme; 5 | -------------------------------------------------------------------------------- /pwnedadmin/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-alpine 2 | 3 | ENV BUILD_DEPS="build-base gcc libc-dev mariadb-dev" 4 | ENV RUNTIME_DEPS="mariadb-connector-c-dev" 5 | 6 | ENV PYTHONDONTWRITEBYTECODE=1 7 | ENV PYTHONUNBUFFERED=1 8 | 9 | RUN mkdir -p /src 10 | 11 | WORKDIR /src 12 | 13 | ADD ./REQUIREMENTS.txt /src/REQUIREMENTS.txt 14 | 15 | RUN apk update &&\ 16 | apk add --no-cache $BUILD_DEPS $RUNTIME_DEPS &&\ 17 | pip install --no-cache-dir --upgrade pip &&\ 18 | pip install --no-cache-dir -r REQUIREMENTS.txt &&\ 19 | apk del $BUILD_DEPS &&\ 20 | rm -rf /var/cache/apk/* 21 | -------------------------------------------------------------------------------- /pwnedadmin/REQUIREMENTS-base.txt: -------------------------------------------------------------------------------- 1 | flask 2 | flask-mysqldb 3 | flask-sqlalchemy 4 | gunicorn 5 | mysqlclient 6 | -------------------------------------------------------------------------------- /pwnedadmin/REQUIREMENTS.txt: -------------------------------------------------------------------------------- 1 | blinker==1.6.2 2 | click==8.1.4 3 | Flask==2.3.2 4 | Flask-MySQLdb==1.0.1 5 | Flask-SQLAlchemy==3.0.5 6 | greenlet==2.0.2 7 | gunicorn==20.1.0 8 | itsdangerous==2.1.2 9 | Jinja2==3.1.2 10 | MarkupSafe==2.1.3 11 | mysqlclient==2.2.0 12 | SQLAlchemy==2.0.18 13 | typing_extensions==4.7.1 14 | Werkzeug==2.3.6 15 | -------------------------------------------------------------------------------- /pwnedadmin/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, Blueprint 2 | from pwnedadmin.extensions import db 3 | import click 4 | import os 5 | 6 | def create_app(): 7 | 8 | # create the Flask application 9 | app = Flask(__name__, static_url_path='/static') 10 | 11 | # configure the Flask application 12 | config_class = os.getenv('CONFIG', default='Development') 13 | app.config.from_object('pwnedadmin.config.{}'.format(config_class.title())) 14 | 15 | db.init_app(app) 16 | 17 | # custom jinja global for accessing dynamic configuration values 18 | from pwnedadmin.models import Config 19 | app.jinja_env.globals['app_config'] = Config.get_value 20 | 21 | # misc jinja configuration variables 22 | app.jinja_env.trim_blocks = True 23 | app.jinja_env.lstrip_blocks = True 24 | 25 | StaticBlueprint = Blueprint('common', __name__, static_url_path='/static/common', static_folder='../common/static') 26 | app.register_blueprint(StaticBlueprint) 27 | 28 | from pwnedadmin.routes.config import blp as ConfigBlurprint 29 | from pwnedadmin.routes.email import blp as EmailBlurprint 30 | app.register_blueprint(ConfigBlurprint) 31 | app.register_blueprint(EmailBlurprint) 32 | 33 | @app.cli.command('init') 34 | @click.argument('dataset') 35 | def init_data(dataset): 36 | from flask import current_app 37 | from pwnedadmin import models 38 | import json 39 | import os 40 | db.create_all(bind_key=None) 41 | for cls in models.BaseModel.__subclasses__(): 42 | fixture_path = os.path.join(current_app.root_path, 'fixtures', dataset, f"{cls.__table__.name}.json") 43 | if os.path.exists(fixture_path): 44 | print(f"Processing {fixture_path}.") 45 | with open(fixture_path) as fp: 46 | for row in json.load(fp): 47 | db.session.add(cls(**row)) 48 | db.session.commit() 49 | print('Database initialized.') 50 | 51 | @app.cli.command('export') 52 | def export_data(): 53 | from pwnedadmin.models import BaseModel 54 | import json 55 | for cls in BaseModel.__subclasses__(): 56 | objs = [obj.serialize_for_export() for obj in cls.query.all()] 57 | if objs: 58 | print(f"\n***** {cls.__table__.name}.json *****\n") 59 | print(json.dumps(objs, indent=4, default=str)) 60 | print('Database exported.') 61 | 62 | @app.cli.command('purge') 63 | def purge_data(): 64 | db.drop_all(bind_key=None) 65 | db.session.commit() 66 | print('Database purged.') 67 | 68 | return app 69 | -------------------------------------------------------------------------------- /pwnedadmin/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class BaseConfig(object): 5 | 6 | # base 7 | DEBUG = False 8 | TESTING = False 9 | SECRET_KEY = os.getenv('SECRET_KEY', default='$ecretKey') 10 | # prevents connection pool exhaustion but disables interactive debugging 11 | PRESERVE_CONTEXT_ON_EXCEPTION = False 12 | 13 | # database 14 | DATABASE_HOST = os.environ.get('DATABASE_HOST', 'localhost') 15 | SQLALCHEMY_DATABASE_URI = f"mysql://pwnedhub:dbconnectpass@{DATABASE_HOST}/pwnedhub-admin" 16 | SQLALCHEMY_TRACK_MODIFICATIONS = False 17 | 18 | 19 | class Development(BaseConfig): 20 | 21 | DEBUG = True 22 | 23 | 24 | class Test(BaseConfig): 25 | 26 | DEBUG = True 27 | TESTING = True 28 | 29 | 30 | class Production(BaseConfig): 31 | 32 | pass 33 | -------------------------------------------------------------------------------- /pwnedadmin/constants.py: -------------------------------------------------------------------------------- 1 | RESTRICTED_USERS = [ 2 | 'admin@pwnedhub.com', 3 | 'cooper@pwnedhub.com', 4 | 'taylor@pwnedhub.com', 5 | 'tanner@pwnedhub.com', 6 | 'emilee@pwnedhub.com', 7 | ] 8 | 9 | class ConfigTypes: 10 | CONTROL = 'security control' 11 | FEATURE = 'feature' 12 | 13 | def __init__(self): 14 | pass 15 | 16 | @property 17 | def serialized(self): 18 | return [self.CONTROL, self.FEATURE] 19 | -------------------------------------------------------------------------------- /pwnedadmin/extensions.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | db = SQLAlchemy() 4 | -------------------------------------------------------------------------------- /pwnedadmin/fixtures/base/configs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "name": "CSRF_PROTECT", 5 | "description": "Profile CSRF Protection (PwnedHub)", 6 | "type": "security control", 7 | "value": true 8 | }, 9 | { 10 | "id": 2, 11 | "name": "OSCI_PROTECT", 12 | "description": "Tools OSCI Protection (PwnedHub)", 13 | "type": "security control", 14 | "value": false 15 | }, 16 | { 17 | "id": 3, 18 | "name": "SQLI_PROTECT", 19 | "description": "Login SQLi Protection (PwnedHub)", 20 | "type": "security control", 21 | "value": false 22 | }, 23 | { 24 | "id": 4, 25 | "name": "CSP_PROTECT", 26 | "description": "Content Security Policy (PwnedHub)", 27 | "type": "security control", 28 | "value": false 29 | }, 30 | { 31 | "id": 5, 32 | "name": "CORS_RESTRICT", 33 | "description": "Restricted CORS (PwnedAPI)", 34 | "type": "security control", 35 | "value": true 36 | }, 37 | { 38 | "id": 6, 39 | "name": "JWT_VERIFY", 40 | "description": "Verify JWT Signatures (PwnedAPI)", 41 | "type": "security control", 42 | "value": true 43 | }, 44 | { 45 | "id": 7, 46 | "name": "JWT_ENCRYPT", 47 | "description": "Encrypt JWTs (PwnedAPI)", 48 | "type": "security control", 49 | "value": false 50 | }, 51 | { 52 | "id": 8, 53 | "name": "BEARER_AUTH_ENABLE", 54 | "description": "Bearer Token Authentication (PwnedAPI)", 55 | "type": "feature", 56 | "value": true 57 | }, 58 | { 59 | "id": 9, 60 | "name": "OIDC_ENABLE", 61 | "description": "OpenID Connect Authentication (PwnedHub)", 62 | "type": "feature", 63 | "value": false 64 | }, 65 | { 66 | "id": 10, 67 | "name": "SSO_ENABLE", 68 | "description": "SSO Authentication (PwnedHub)", 69 | "type": "feature", 70 | "value": false 71 | }, 72 | { 73 | "id": 11, 74 | "name": "OOB_RESET_ENABLE", 75 | "description": "Out-of-Band Password Reset (PwnedHub)", 76 | "type": "feature", 77 | "value": false 78 | }, 79 | { 80 | "id": 12, 81 | "name": "CTF_MODE", 82 | "description": "CTF Mode (Warning: Disables this interface!)", 83 | "type": "feature", 84 | "value": false 85 | } 86 | ] 87 | -------------------------------------------------------------------------------- /pwnedadmin/models.py: -------------------------------------------------------------------------------- 1 | from pwnedadmin import db 2 | from pwnedadmin.constants import RESTRICTED_USERS 3 | from pwnedadmin.utils import get_current_utc_time, get_local_from_utc 4 | 5 | 6 | class BaseModel(db.Model): 7 | __abstract__ = True 8 | 9 | def serialize_for_export(self): 10 | return {c.name: getattr(self, c.name) for c in self.__mapper__.columns} 11 | 12 | 13 | class Config(BaseModel): 14 | __tablename__ = 'configs' 15 | id = db.Column(db.Integer, primary_key=True) 16 | name = db.Column(db.String(255), nullable=False) 17 | description = db.Column(db.Text, nullable=False) 18 | type = db.Column(db.String(255), nullable=False) 19 | value = db.Column(db.Boolean, nullable=False) 20 | 21 | @staticmethod 22 | def get_by_name(name): 23 | return Config.query.filter_by(name=name).first() 24 | 25 | @staticmethod 26 | def get_value(name): 27 | return Config.query.filter_by(name=name).first().value 28 | 29 | def __repr__(self): 30 | return "".format(self.name) 31 | 32 | 33 | class Email(BaseModel): 34 | __tablename__ = 'emails' 35 | id = db.Column(db.Integer, primary_key=True) 36 | created = db.Column(db.DateTime, nullable=False, default=get_current_utc_time) 37 | sender = db.Column(db.String(255), nullable=False) 38 | receiver = db.Column(db.String(255), nullable=False) 39 | subject = db.Column(db.Text, nullable=False) 40 | body = db.Column(db.Text, nullable=False) 41 | 42 | @property 43 | def created_as_string(self): 44 | return get_local_from_utc(self.created).strftime("%Y-%m-%d %H:%M:%S") 45 | 46 | @staticmethod 47 | def get_unrestricted(): 48 | return Email.query.filter(Email.receiver.notin_(RESTRICTED_USERS)) 49 | 50 | @staticmethod 51 | def get_by_receiver(receiver): 52 | return Email.query.filter_by(receiver=receiver) 53 | 54 | def __repr__(self): 55 | return "".format(self.id) 56 | -------------------------------------------------------------------------------- /pwnedadmin/routes/config.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, render_template, abort, redirect, url_for 2 | from pwnedadmin import db 3 | from pwnedadmin.constants import ConfigTypes 4 | from pwnedadmin.models import Config 5 | 6 | blp = Blueprint('config', __name__, url_prefix='/config') 7 | 8 | @blp.route('/', methods=['GET', 'POST']) 9 | def index(): 10 | if Config.get_value('CTF_MODE'): 11 | abort(404) 12 | configs = Config.query.all() 13 | if request.method == 'POST': 14 | for config in configs: 15 | config.value = request.form.get(config.name.lower()) == 'on' 16 | db.session.commit() 17 | return redirect(url_for('config.index')) 18 | return render_template('config.html', configs=configs, config_types=ConfigTypes().serialized) 19 | -------------------------------------------------------------------------------- /pwnedadmin/routes/email.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, render_template, redirect, url_for 2 | from pwnedadmin import db 3 | from pwnedadmin.models import Email 4 | 5 | blp = Blueprint('email', __name__, url_prefix='/inbox') 6 | 7 | @blp.route('/', methods=['GET', 'POST']) 8 | def index(): 9 | if user := request.args.get('user'): 10 | emails = Email.get_by_receiver(user) 11 | else: 12 | emails = Email.get_unrestricted() 13 | return render_template('emails.html', emails=emails.order_by(Email.created.desc()).all()) 14 | 15 | @blp.route('/empty') 16 | def empty(): 17 | emails = Email.query.delete() 18 | db.session.commit() 19 | return redirect(url_for('email.index')) 20 | -------------------------------------------------------------------------------- /pwnedadmin/templates/config.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block title %}PwnedAdmin | Config{% endblock %} 3 | {% block content %} 4 |
5 | {% for config_type in config_types %} 6 |
7 |
8 |
{{ config_type|title }}
9 |
10 |
11 | {% for config in configs %} 12 | {% if config.type == config_type %} 13 | 17 | {% endif %} 18 | {% endfor %} 19 |
20 |
21 | {% endfor %} 22 |
23 | 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /pwnedadmin/templates/emails.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block title %}PwnedAdmin | Inbox{% endblock %} 3 | {% block content %} 4 |
5 |
6 | 7 |
8 | {% if emails|length > 0 %} 9 | {% for email in emails %} 10 |
11 |
12 |
13 |
{{ email.subject }}
14 |
{{ email.created_as_string }}
15 |
16 |
From: {{ email.sender }}
17 |
To: {{ email.receiver }}
18 |
19 |
20 |
{{ email.body|safe }}
21 |
22 |
23 | {% endfor %} 24 | {% else %} 25 |
26 |
Inbox is empty.
27 |
28 | {% endif %} 29 |
30 | 37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /pwnedadmin/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}{% endblock %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% block content %}{% endblock %} 14 | 15 | 16 | -------------------------------------------------------------------------------- /pwnedadmin/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | 3 | def get_current_utc_time(): 4 | return datetime.now(tz=timezone.utc) 5 | 6 | def get_local_from_utc(dtg): 7 | return dtg.replace(tzinfo=timezone.utc).astimezone(tz=None) 8 | -------------------------------------------------------------------------------- /pwnedadmin/wsgi.py: -------------------------------------------------------------------------------- 1 | from pwnedadmin import create_app 2 | 3 | app = create_app() 4 | if __name__ == '__main__': 5 | app.run() 6 | -------------------------------------------------------------------------------- /pwnedapi/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-alpine 2 | 3 | ENV BUILD_DEPS="build-base gcc libc-dev libxslt-dev mariadb-dev" 4 | ENV RUNTIME_DEPS="libxslt mariadb-connector-c-dev" 5 | 6 | ENV PYTHONDONTWRITEBYTECODE=1 7 | ENV PYTHONUNBUFFERED=1 8 | 9 | RUN mkdir -p /src 10 | 11 | WORKDIR /src 12 | 13 | ADD ./REQUIREMENTS.txt /src/REQUIREMENTS.txt 14 | 15 | RUN apk update &&\ 16 | apk add --no-cache $BUILD_DEPS $RUNTIME_DEPS &&\ 17 | pip install --no-cache-dir --upgrade pip &&\ 18 | pip install --no-cache-dir -r REQUIREMENTS.txt &&\ 19 | apk del $BUILD_DEPS &&\ 20 | rm -rf /var/cache/apk/* 21 | -------------------------------------------------------------------------------- /pwnedapi/REQUIREMENTS-base.txt: -------------------------------------------------------------------------------- 1 | eventlet 2 | flask 3 | flask-cors 4 | flask-mysqldb 5 | flask-restful 6 | flask-socketio 7 | flask-sqlalchemy 8 | # https://github.com/benoitc/gunicorn/issues/2828 9 | # https://www.pythonfixing.com/2022/03/fixed-gunicorn-importerror-cannot.html 10 | # https://github.com/benoitc/gunicorn/archive/refs/heads/master.zip#egg=gunicorn==20.1.0 11 | gunicorn 12 | jsonpickle 13 | lxml 14 | mysqlclient 15 | pyjwt 16 | redis 17 | rq 18 | requests 19 | cryptography 20 | jwcrypto 21 | -------------------------------------------------------------------------------- /pwnedapi/REQUIREMENTS.txt: -------------------------------------------------------------------------------- 1 | aniso8601==9.0.1 2 | async-timeout==4.0.2 3 | bidict==0.22.1 4 | blinker==1.6.2 5 | certifi==2023.5.7 6 | cffi==1.16.0 7 | charset-normalizer==3.2.0 8 | click==8.1.4 9 | cryptography==41.0.4 10 | Deprecated==1.2.14 11 | dnspython==2.3.0 12 | eventlet==0.33.3 13 | Flask==2.3.2 14 | Flask-Cors==4.0.0 15 | Flask-MySQLdb==1.0.1 16 | Flask-RESTful==0.3.10 17 | Flask-SocketIO==5.3.4 18 | Flask-SQLAlchemy==3.0.5 19 | greenlet==2.0.2 20 | gunicorn==21.2.0 21 | idna==3.4 22 | itsdangerous==2.1.2 23 | Jinja2==3.1.2 24 | jsonpickle==3.0.1 25 | jwcrypto==1.5.0 26 | lxml==4.9.3 27 | MarkupSafe==2.1.3 28 | mysqlclient==2.2.0 29 | packaging==23.1 30 | pycparser==2.21 31 | PyJWT==2.7.0 32 | python-engineio==4.5.1 33 | python-socketio==5.8.0 34 | pytz==2023.3 35 | redis==4.6.0 36 | requests==2.31.0 37 | rq==1.15.1 38 | six==1.16.0 39 | SQLAlchemy==2.0.18 40 | typing_extensions==4.7.1 41 | urllib3==2.0.3 42 | Werkzeug==2.3.6 43 | wrapt==1.15.0 44 | -------------------------------------------------------------------------------- /pwnedapi/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, Blueprint 2 | from pwnedapi.extensions import db, cors, socketio 3 | from redis import Redis 4 | import click 5 | import os 6 | import rq 7 | 8 | def create_app(): 9 | 10 | # create the Flask application 11 | app = Flask(__name__, static_url_path='/static') 12 | 13 | # configure the Flask application 14 | config_class = os.getenv('CONFIG', default='Development') 15 | app.config.from_object('pwnedapi.config.{}'.format(config_class.title())) 16 | 17 | app.redis = Redis.from_url(app.config['REDIS_URL']) 18 | app.api_task_queue = rq.Queue('pwnedapi-tasks', connection=app.redis) 19 | app.bot_task_queue = rq.Queue('adminbot-tasks', connection=app.redis) 20 | 21 | def is_allowed_origin(response): 22 | for k, v in response.headers: 23 | if k == 'Access-Control-Allow-Origin': 24 | return v in app.config['ALLOWED_ORIGINS'] 25 | return False 26 | 27 | def remove_cors_headers(response): 28 | to_remove = [] 29 | for k, v in response.headers: 30 | if any(k.startswith(s) for s in ['Access-Control-', 'Vary']): 31 | to_remove.append(k) 32 | for name in to_remove: 33 | del response.headers[name] 34 | return response 35 | 36 | # must be set before initializing the CORS extension to modify 37 | # headers created by the extension's `after_request` methods 38 | @app.after_request 39 | def config_cors(response): 40 | from pwnedapi.models import Config 41 | if Config.get_value('CORS_RESTRICT'): 42 | # apply the CORS whitelist from the config 43 | if not is_allowed_origin(response): 44 | response = remove_cors_headers(response) 45 | return response 46 | 47 | db.init_app(app) 48 | cors.init_app(app) 49 | socketio.init_app(app, cors_allowed_origins=app.config['ALLOWED_ORIGINS']) 50 | 51 | StaticBlueprint = Blueprint('common', __name__, static_url_path='/static/common', static_folder='../common/static') 52 | app.register_blueprint(StaticBlueprint) 53 | 54 | from pwnedapi.routes.api import blp as ApiBlueprint 55 | app.register_blueprint(ApiBlueprint) 56 | 57 | from pwnedapi.routes import websockets 58 | 59 | @app.cli.command('init') 60 | @click.argument('dataset') 61 | def init_data(dataset): 62 | from flask import current_app 63 | from pwnedapi import models 64 | import json 65 | import os 66 | db.create_all(bind_key=None) 67 | for cls in models.BaseModel.__subclasses__(): 68 | fixture_path = os.path.join(current_app.root_path, 'fixtures', dataset, f"{cls.__table__.name}.json") 69 | if os.path.exists(fixture_path): 70 | print(f"Processing {fixture_path}.") 71 | with open(fixture_path) as fp: 72 | for row in json.load(fp): 73 | db.session.add(cls(**row)) 74 | db.session.commit() 75 | print('Database initialized.') 76 | 77 | @app.cli.command('export') 78 | def export_data(): 79 | from pwnedapi.models import BaseModel 80 | import json 81 | for cls in BaseModel.__subclasses__(): 82 | objs = [obj.serialize_for_export() for obj in cls.query.all()] 83 | if objs: 84 | print(f"\n***** {cls.__table__.name}.json *****\n") 85 | print(json.dumps(objs, indent=4, default=str)) 86 | print('Database exported.') 87 | 88 | @app.cli.command('purge') 89 | def purge_data(): 90 | db.drop_all(bind_key=None) 91 | db.session.commit() 92 | print('Database purged.') 93 | 94 | return app, socketio 95 | -------------------------------------------------------------------------------- /pwnedapi/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class BaseConfig(object): 5 | 6 | # base 7 | DEBUG = False 8 | TESTING = False 9 | SECRET_KEY = os.getenv('SECRET_KEY', default='$ecretKey') 10 | # prevents connection pool exhaustion but disables interactive debugging 11 | PRESERVE_CONTEXT_ON_EXCEPTION = False 12 | 13 | # database 14 | DATABASE_HOST = os.environ.get('DATABASE_HOST', 'localhost') 15 | SQLALCHEMY_DATABASE_URI = f"mysql://pwnedhub:dbconnectpass@{DATABASE_HOST}/pwnedhub-test" 16 | SQLALCHEMY_BINDS = { 17 | 'admin': f"mysql://pwnedhub:dbconnectpass@{DATABASE_HOST}/pwnedhub-admin" 18 | } 19 | SQLALCHEMY_TRACK_MODIFICATIONS = False 20 | 21 | # csrf 22 | CSRF_TOKEN_NAME = 'X-Csrf-Token' 23 | 24 | # redis 25 | REDIS_URL = os.environ.get('REDIS_URL', 'redis://') 26 | 27 | # cors 28 | CORS_SUPPORTS_CREDENTIALS = True 29 | ALLOWED_ORIGINS = ['http://www.pwnedhub.com', 'http://test.pwnedhub.com'] 30 | 31 | # oidc 32 | OAUTH_PROVIDERS = { 33 | 'google': { 34 | 'CLIENT_ID': '1098478339188-pvi39gpsvclmmucvu16vhrh0179sd100.apps.googleusercontent.com', 35 | 'CLIENT_SECRET': '5LFAbNk7rLa00PZOHceQfudp', 36 | 'DISCOVERY_DOC': 'https://accounts.google.com/.well-known/openid-configuration', 37 | }, 38 | } 39 | 40 | # unused 41 | API_CONFIG_KEY_NAME = 'X-API-Key' 42 | API_CONFIG_KEY_VALUE = 'verysekrit' 43 | 44 | 45 | class Development(BaseConfig): 46 | 47 | DEBUG = True 48 | 49 | 50 | class Test(BaseConfig): 51 | 52 | DEBUG = True 53 | TESTING = True 54 | 55 | 56 | class Production(BaseConfig): 57 | 58 | pass 59 | -------------------------------------------------------------------------------- /pwnedapi/constants.py: -------------------------------------------------------------------------------- 1 | ROLES = { 2 | 0: 'admin', 3 | 1: 'user', 4 | } 5 | 6 | USER_STATUSES = { 7 | 0: 'disabled', 8 | 1: 'enabled', 9 | } 10 | 11 | DEFAULT_NOTE = '''##### Welcome to PwnedHub 2.0! 12 | 13 | A collaborative space to conduct hosted security assessments. 14 | 15 | **Find flaws.** 16 | 17 | * This is your notes space. Keep your personal notes here. 18 | * Leverage popular security testing tools right from your browser in the tools space. 19 | 20 | **Collaborate.** 21 | 22 | * Privately collaborate with coworkers in the messaging space. 23 | * Join public rooms in the messaging space to share information and socialize. 24 | 25 | **On the Move** 26 | 27 | * PwnedHub 2.0 is built with mobility in mind. No need for a separate app! 28 | 29 | Happy hunting! 30 | 31 | \- The PwnedHub Team 32 | 33 | ''' 34 | -------------------------------------------------------------------------------- /pwnedapi/decorators.py: -------------------------------------------------------------------------------- 1 | from flask import g, request, current_app, abort 2 | from pwnedapi.constants import ROLES 3 | from pwnedapi.models import Config 4 | from pwnedapi.utils import CsrfToken, ParamValidator 5 | from functools import wraps 6 | import base64 7 | import jsonpickle 8 | 9 | def token_auth_required(func): 10 | @wraps(func) 11 | def wrapped(*args, **kwargs): 12 | if g.user: 13 | return func(*args, **kwargs) 14 | abort(401) 15 | return wrapped 16 | 17 | def key_auth_required(func): 18 | @wraps(func) 19 | def wrapped(*args, **kwargs): 20 | key = request.headers.get(current_app.config['API_CONFIG_KEY_NAME']) 21 | if key == current_app.config['API_CONFIG_KEY_VALUE']: 22 | return func(*args, **kwargs) 23 | abort(401) 24 | return wrapped 25 | 26 | def roles_required(*roles): 27 | def wrapper(func): 28 | @wraps(func) 29 | def wrapped(*args, **kwargs): 30 | if ROLES[g.user.role] not in roles: 31 | return abort(403) 32 | return func(*args, **kwargs) 33 | return wrapped 34 | return wrapper 35 | 36 | def validate_json(params): 37 | def wrapper(func): 38 | @wraps(func) 39 | def wrapped(*args, **kwargs): 40 | input_dict = getattr(request, 'json', {}) 41 | v = ParamValidator(input_dict, params) 42 | v.validate() 43 | if not v.passed: 44 | abort(400, v.reason) 45 | return func(*args, **kwargs) 46 | return wrapped 47 | return wrapper 48 | 49 | def csrf_protect(func): 50 | @wraps(func) 51 | def wrapped(*args, **kwargs): 52 | if not Config.get_value('BEARER_AUTH_ENABLE'): 53 | # no Bearer token means cookies (default) are used and CSRF is an issue 54 | csrf_token = request.headers.get(current_app.config['CSRF_TOKEN_NAME']) 55 | try: 56 | untrusted_csrf_obj = jsonpickle.decode(base64.b64decode(csrf_token)) 57 | untrusted_csrf_obj.sign(current_app.config['SECRET_KEY']) 58 | trusted_csrf_obj = CsrfToken(g.user.id, untrusted_csrf_obj.ts) 59 | trusted_csrf_obj.sign(current_app.config['SECRET_KEY']) 60 | except: 61 | untrusted_csrf_obj = None 62 | if not untrusted_csrf_obj or trusted_csrf_obj.sig != untrusted_csrf_obj.sig: 63 | abort(400, 'CSRF detected.') 64 | return func(*args, **kwargs) 65 | return wrapped 66 | -------------------------------------------------------------------------------- /pwnedapi/extensions.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | from flask_cors import CORS 3 | from flask_socketio import SocketIO 4 | 5 | db = SQLAlchemy() 6 | cors = CORS() 7 | socketio = SocketIO() 8 | -------------------------------------------------------------------------------- /pwnedapi/fixtures/base/messages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "comment": "Hey, did you guys hear that we're having a security assessment this week?", 4 | "user_id": 3, 5 | "room_id": 1, 6 | "id": 1, 7 | "created": "2019-02-18 04:55:11", 8 | "modified": "2019-02-18 04:55:11" 9 | }, 10 | { 11 | "comment": "No.", 12 | "user_id": 4, 13 | "room_id": 1, 14 | "id": 2, 15 | "created": "2019-02-18 04:55:19", 16 | "modified": "2019-02-18 04:55:19" 17 | }, 18 | { 19 | "comment": "First I'm hearing of it. I hope they don't find any bugs. This is my \"get rich quick\" scheme.", 20 | "user_id": 2, 21 | "room_id": 1, 22 | "id": 3, 23 | "created": "2019-02-18 04:56:09", 24 | "modified": "2019-02-18 04:56:09" 25 | }, 26 | { 27 | "comment": "Heh. Me too. So looking forward to afternoons on my yacht. :-)", 28 | "user_id": 3, 29 | "room_id": 1, 30 | "id": 4, 31 | "created": "2019-02-18 04:57:02", 32 | "modified": "2019-02-18 04:57:02" 33 | }, 34 | { 35 | "comment": "Wait... didn't we go live this week?", 36 | "user_id": 4, 37 | "room_id": 1, 38 | "id": 5, 39 | "created": "2019-02-18 04:57:08", 40 | "modified": "2019-02-18 04:57:08" 41 | }, 42 | { 43 | "comment": "Well, as the most interesting man in the world says, \"I don't always get apps tested, but when I do, I get it done in prod.\"", 44 | "user_id": 2, 45 | "room_id": 1, 46 | "id": 6, 47 | "created": "2019-02-18 04:57:20", 48 | "modified": "2019-02-18 04:57:20" 49 | }, 50 | { 51 | "comment": "LOL! So, yeah, did any of you guys fix those things I found during QA testing? I sent Cooper a link to them in a private message.", 52 | "user_id": 5, 53 | "room_id": 1, 54 | "id": 7, 55 | "created": "2019-02-18 04:57:32", 56 | "modified": "2019-02-18 04:57:32" 57 | }, 58 | { 59 | "comment": "No.", 60 | "user_id": 4, 61 | "room_id": 1, 62 | "id": 8, 63 | "created": "2019-02-18 04:57:37", 64 | "modified": "2019-02-18 04:57:37" 65 | }, 66 | { 67 | "comment": "My bad.", 68 | "user_id": 2, 69 | "room_id": 1, 70 | "id": 9, 71 | "created": "2019-02-18 04:57:41", 72 | "modified": "2019-02-18 04:57:41" 73 | }, 74 | { 75 | "comment": "Uh oh...", 76 | "user_id": 3, 77 | "room_id": 1, 78 | "id": 10, 79 | "created": "2019-02-18 04:57:46", 80 | "modified": "2019-02-18 04:57:46" 81 | }, 82 | { 83 | "comment": "Wow. We're totally going to end up on https://haveibeenpwned.com/PwnedWebsites.", 84 | "user_id": 5, 85 | "room_id": 1, 86 | "id": 11, 87 | "created": "2019-02-18 04:59:31", 88 | "modified": "2019-02-18 04:59:31" 89 | } 90 | ] 91 | -------------------------------------------------------------------------------- /pwnedapi/fixtures/base/rooms.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "general", 4 | "private": false, 5 | "id": 1, 6 | "created": "2019-02-16 01:51:59", 7 | "modified": "2019-02-16 01:51:59" 8 | }, 9 | { 10 | "name": "f9adeea0", 11 | "private": true, 12 | "id": 2, 13 | "created": "2023-07-17 04:58:05", 14 | "modified": "2023-07-17 04:58:05" 15 | }, 16 | { 17 | "name": "a28b1e3e", 18 | "private": true, 19 | "id": 3, 20 | "created": "2023-07-17 04:58:06", 21 | "modified": "2023-07-17 04:58:06" 22 | }, 23 | { 24 | "name": "2ce70a5f", 25 | "private": true, 26 | "id": 4, 27 | "created": "2023-07-17 04:58:08", 28 | "modified": "2023-07-17 04:58:08" 29 | }, 30 | { 31 | "name": "ae206386", 32 | "private": true, 33 | "id": 5, 34 | "created": "2023-07-17 04:58:09", 35 | "modified": "2023-07-17 04:58:09" 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /pwnedapi/fixtures/base/tools.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Dig", 4 | "path": "dig", 5 | "description": "(Domain Internet Groper) Network administration tool for Domain Name System (DNS) name server interrogation.", 6 | "id": 1, 7 | "created": "2019-02-16 02:09:59", 8 | "modified": "2019-02-16 02:09:59" 9 | }, 10 | { 11 | "name": "Nmap", 12 | "path": "nmap", 13 | "description": "(Network Mapper) Utility for network discovery and security auditing.", 14 | "id": 2, 15 | "created": "2019-02-16 02:10:29", 16 | "modified": "2019-02-16 02:10:29" 17 | }, 18 | { 19 | "name": "Nikto", 20 | "path": "nikto", 21 | "description": "Signature-based web server scanner.", 22 | "id": 3, 23 | "created": "2019-02-16 02:10:59", 24 | "modified": "2019-02-16 02:10:59" 25 | }, 26 | { 27 | "name": "SSLyze", 28 | "path": "sslyze", 29 | "description": "Fast and powerful SSL/TLS server scanning library.", 30 | "id": 4, 31 | "created": "2019-02-16 02:11:29", 32 | "modified": "2019-02-16 02:11:29" 33 | }, 34 | { 35 | "name": "SQLmap", 36 | "path": "sqlmap --batch", 37 | "description": "Penetration testing tool that automates the process of detecting and exploiting SQL injection flaws.", 38 | "id": 5, 39 | "created": "2019-02-16 02:11:59", 40 | "modified": "2019-02-16 02:11:59" 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /pwnedapi/fixtures/base/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "email": "admin@pwnedhub.com", 4 | "name": "Administrator", 5 | "avatar": "/static/common/images/avatars/admin.png", 6 | "signature": "All your base are belong to me.", 7 | "role": 0, 8 | "status": 1, 9 | "id": 1, 10 | "created": "2019-02-16 01:51:59", 11 | "modified": "2019-02-16 01:51:59" 12 | }, 13 | { 14 | "email": "cooper@pwnedhub.com", 15 | "name": "Cooper", 16 | "avatar": "/static/common/images/avatars/c-man.png", 17 | "signature": "Gamer, hacker, and basketball player. Energy sword FTW!", 18 | "role": 1, 19 | "status": 1, 20 | "id": 2, 21 | "created": "2019-02-16 04:46:27", 22 | "modified": "2019-02-16 04:46:27" 23 | }, 24 | { 25 | "email": "taylor@pwnedhub.com", 26 | "name": "Taylor", 27 | "avatar": "/static/common/images/avatars/wolf.jpg", 28 | "signature": "Wolf in a past life. Nerd in the current. Johnny 5 is indeed alive.", 29 | "role": 1, 30 | "status": 1, 31 | "id": 3, 32 | "created": "2019-02-16 04:47:14", 33 | "modified": "2019-02-16 04:47:14" 34 | }, 35 | { 36 | "email": "tanner@pwnedhub.com", 37 | "name": "Tanner", 38 | "avatar": "/static/common/images/avatars/kitty.jpg", 39 | "signature": "I might be small, cute, and cuddly, but remember... dynamite comes in small tightly wrapped packages that go boom.", 40 | "role": 1, 41 | "status": 1, 42 | "id": 4, 43 | "created": "2019-02-16 04:48:19", 44 | "modified": "2019-02-16 04:48:19" 45 | }, 46 | { 47 | "email": "emilee@pwnedhub.com", 48 | "name": "Emilee", 49 | "avatar": "/static/common/images/avatars/bacon.png", 50 | "signature": "Late to the party, but still the life of the party.", 51 | "role": 1, 52 | "status": 1, 53 | "id": 5, 54 | "created": "2019-02-16 04:49:34", 55 | "modified": "2019-02-16 04:49:34" 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /pwnedapi/routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/pwnedapi/routes/__init__.py -------------------------------------------------------------------------------- /pwnedapi/static/swaggerui/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/pwnedapi/static/swaggerui/favicon-16x16.png -------------------------------------------------------------------------------- /pwnedapi/static/swaggerui/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/pwnedapi/static/swaggerui/favicon-32x32.png -------------------------------------------------------------------------------- /pwnedapi/static/swaggerui/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | overflow: -moz-scrollbars-vertical; 4 | overflow-y: scroll; 5 | } 6 | 7 | *, 8 | *:before, 9 | *:after { 10 | box-sizing: inherit; 11 | } 12 | 13 | body { 14 | margin: 0; 15 | background: #fafafa; 16 | } 17 | -------------------------------------------------------------------------------- /pwnedapi/static/swaggerui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /pwnedapi/static/swaggerui/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swagger UI: OAuth2 Redirect 5 | 6 | 7 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /pwnedapi/static/swaggerui/swagger-initializer.js: -------------------------------------------------------------------------------- 1 | window.onload = function() { 2 | // 3 | 4 | // the following lines will be replaced by docker/configurator, when it runs in a docker-container 5 | window.ui = SwaggerUIBundle({ 6 | url: "http://api.pwnedhub.com/static/openapi.json", 7 | dom_id: '#swagger-ui', 8 | deepLinking: true, 9 | validatorUrl: null, 10 | presets: [ 11 | SwaggerUIBundle.presets.apis, 12 | SwaggerUIStandalonePreset 13 | ], 14 | plugins: [ 15 | SwaggerUIBundle.plugins.DownloadUrl 16 | ], 17 | layout: "StandaloneLayout" 18 | }); 19 | 20 | // 21 | }; 22 | -------------------------------------------------------------------------------- /pwnedapi/tasks.py: -------------------------------------------------------------------------------- 1 | from pwnedapi import create_app, db 2 | from pwnedapi.models import Scan 3 | from rq import get_current_job 4 | import os 5 | import subprocess 6 | import sys 7 | import traceback 8 | 9 | def execute_tool(cmd): 10 | app, socketio = create_app() 11 | with app.app_context(): 12 | try: 13 | output = '' 14 | env = os.environ.copy() 15 | env['PATH'] = os.pathsep.join(('/usr/bin', env['PATH'])) 16 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=env) 17 | out, err = p.communicate() 18 | output = (out + err).decode() 19 | except: 20 | app.logger.error('Unhandled exception', exc_info=sys.exc_info()) 21 | output = traceback.format_exc() 22 | finally: 23 | job = get_current_job() 24 | scan = Scan.query.get(job.get_id()) 25 | scan.complete = True 26 | scan.results = output 27 | db.session.commit() 28 | -------------------------------------------------------------------------------- /pwnedapi/validators.py: -------------------------------------------------------------------------------- 1 | from pwnedapi.models import Config 2 | import re 3 | 4 | def is_valid_command(cmd): 5 | pattern = r'[;&|]' 6 | if Config.get_value('OSCI_PROTECT'): 7 | pattern = r'[;&|<>`$(){}]' 8 | if re.search(pattern, cmd): 9 | return False 10 | return True 11 | -------------------------------------------------------------------------------- /pwnedapi/wsgi.py: -------------------------------------------------------------------------------- 1 | from pwnedapi import create_app 2 | 3 | app, socketio = create_app() 4 | if __name__ == '__main__': 5 | app.run() 6 | -------------------------------------------------------------------------------- /pwnedhub/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-alpine 2 | 3 | ENV BUILD_DEPS="build-base gcc libc-dev libxslt-dev mariadb-dev" 4 | ENV RUNTIME_DEPS="libxslt mariadb-connector-c-dev" 5 | 6 | ENV PYTHONDONTWRITEBYTECODE=1 7 | ENV PYTHONUNBUFFERED=1 8 | 9 | RUN mkdir -p /src 10 | 11 | WORKDIR /src 12 | 13 | ADD ./REQUIREMENTS.txt /src/REQUIREMENTS.txt 14 | 15 | RUN apk update &&\ 16 | apk add --no-cache $BUILD_DEPS $RUNTIME_DEPS &&\ 17 | pip install --no-cache-dir --upgrade pip &&\ 18 | pip install --no-cache-dir -r REQUIREMENTS.txt &&\ 19 | apk del $BUILD_DEPS &&\ 20 | rm -rf /var/cache/apk/* 21 | -------------------------------------------------------------------------------- /pwnedhub/REQUIREMENTS-base.txt: -------------------------------------------------------------------------------- 1 | flask 2 | flask-mysqldb 3 | flask-session 4 | flask-sqlalchemy 5 | gunicorn 6 | pyjwt 7 | lxml 8 | markdown 9 | mysqlclient 10 | requests 11 | redis 12 | rq 13 | -------------------------------------------------------------------------------- /pwnedhub/REQUIREMENTS.txt: -------------------------------------------------------------------------------- 1 | async-timeout==4.0.2 2 | blinker==1.6.2 3 | cachelib==0.13.0 4 | certifi==2023.5.7 5 | charset-normalizer==3.2.0 6 | click==8.1.4 7 | Flask==2.3.2 8 | Flask-MySQLdb==1.0.1 9 | Flask-Session==0.8.0 10 | Flask-SQLAlchemy==3.0.5 11 | greenlet==2.0.2 12 | gunicorn==20.1.0 13 | idna==3.4 14 | itsdangerous==2.1.2 15 | Jinja2==3.1.2 16 | lxml==4.9.3 17 | Markdown==3.4.3 18 | MarkupSafe==2.1.3 19 | msgspec==0.18.6 20 | mysqlclient==2.2.0 21 | PyJWT==2.7.0 22 | redis==4.6.0 23 | requests==2.31.0 24 | rq==1.15.1 25 | SQLAlchemy==2.0.18 26 | typing_extensions==4.7.1 27 | urllib3==2.0.3 28 | Werkzeug==2.3.6 29 | -------------------------------------------------------------------------------- /pwnedhub/config.py: -------------------------------------------------------------------------------- 1 | from cachelib.file import FileSystemCache 2 | import os 3 | 4 | 5 | class BaseConfig(object): 6 | 7 | # base 8 | DEBUG = False 9 | TESTING = False 10 | SECRET_KEY = os.getenv('SECRET_KEY', default='$ecretKey') 11 | # prevents connection pool exhaustion but disables interactive debugging 12 | PRESERVE_CONTEXT_ON_EXCEPTION = False 13 | MESSAGES_PER_PAGE = 5 14 | 15 | # database 16 | DATABASE_HOST = os.environ.get('DATABASE_HOST', 'localhost') 17 | SQLALCHEMY_DATABASE_URI = f"mysql://pwnedhub:dbconnectpass@{DATABASE_HOST}/pwnedhub" 18 | SQLALCHEMY_BINDS = { 19 | 'admin': f"mysql://pwnedhub:dbconnectpass@{DATABASE_HOST}/pwnedhub-admin" 20 | } 21 | SQLALCHEMY_TRACK_MODIFICATIONS = False 22 | 23 | # redis 24 | REDIS_URL = os.environ.get('REDIS_URL', 'redis://') 25 | 26 | # file upload 27 | UPLOAD_FOLDER = '/tmp/artifacts' 28 | ALLOWED_EXTENSIONS = set(['txt', 'xml', 'jpg', 'png', 'gif', 'pdf']) 29 | ALLOWED_MIMETYPES = set(['text/plain', 'application/xml', 'image/jpeg', 'image/png', 'image/gif', 'application/pdf']) 30 | 31 | # session 32 | SESSION_TYPE = 'cachelib' 33 | SESSION_SERIALIZATION_FORMAT = 'json' 34 | SESSION_CACHELIB = FileSystemCache(threshold=500, cache_dir='/tmp/sessions') 35 | SESSION_COOKIE_NAME = 'session' 36 | SESSION_COOKIE_HTTPONLY = False 37 | SESSION_REFRESH_EACH_REQUEST = False 38 | PERMANENT_SESSION_LIFETIME = 3600 # 1 hour 39 | 40 | # oidc 41 | OAUTH_PROVIDERS = { 42 | 'google': { 43 | 'CLIENT_ID': '1098478339188-pvi39gpsvclmmucvu16vhrh0179sd100.apps.googleusercontent.com', 44 | 'CLIENT_SECRET': '5LFAbNk7rLa00PZOHceQfudp', 45 | 'DISCOVERY_DOC': 'https://accounts.google.com/.well-known/openid-configuration', 46 | }, 47 | } 48 | 49 | # markdown 50 | MARKDOWN_EXTENSIONS = [ 51 | 'markdown.extensions.tables', 52 | 'markdown.extensions.extra', 53 | 'markdown.extensions.attr_list', 54 | 'markdown.extensions.fenced_code', 55 | ] 56 | 57 | 58 | class Development(BaseConfig): 59 | 60 | DEBUG = True 61 | 62 | 63 | class Test(BaseConfig): 64 | 65 | DEBUG = True 66 | TESTING = True 67 | 68 | 69 | class Production(BaseConfig): 70 | 71 | pass 72 | -------------------------------------------------------------------------------- /pwnedhub/constants.py: -------------------------------------------------------------------------------- 1 | ROLES = { 2 | 0: 'admin', 3 | 1: 'user', 4 | } 5 | 6 | USER_STATUSES = { 7 | 0: 'disabled', 8 | 1: 'enabled', 9 | } 10 | 11 | QUESTIONS = { 12 | 0: 'Favorite food?', 13 | 1: 'Pet\'s name?', 14 | 2: 'High school mascot?', 15 | 3: 'Birthplace?', 16 | 4: 'First employer?', 17 | } 18 | 19 | DEFAULT_NOTE = '''##### Welcome to PwnedHub! 20 | 21 | A collaborative space to conduct hosted security assessments. 22 | 23 | **Find flaws.** 24 | 25 | * This is your notes space. Keep your personal notes here. 26 | * Store artifacts from local and external tools in the artifacts space. 27 | * Leverage popular security testing tools right from your browser in the tools space. 28 | 29 | **Collaborate.** 30 | 31 | * Privately collaborate with coworkers in the PwnMail space. 32 | * Share public information and socialize in the messages space. 33 | 34 | Happy hunting! 35 | 36 | \\- The PwnedHub Team 37 | 38 | ''' 39 | 40 | ADMIN_RESPONSE = { 41 | 'default': 'I would be more than happy to help you with that. Unfortunately, the person responsible for that is unavailable at the moment. We\'ll get back with you soon. Thanks.', 42 | 'password': 'Hey no problem. We all forget our password every now and then. Your current password is {password}, but you can simply reset it using the Forgot Password link on the login page. I hope this helps. Have a great day!' 43 | } 44 | -------------------------------------------------------------------------------- /pwnedhub/decorators.py: -------------------------------------------------------------------------------- 1 | from flask import g, request, session, redirect, url_for, abort, make_response, flash 2 | from pwnedhub.constants import ROLES 3 | from pwnedhub.models import Config 4 | from functools import wraps 5 | from urllib.parse import urlparse 6 | 7 | def validate(params, method='POST'): 8 | def wrapper(func): 9 | @wraps(func) 10 | def wrapped(*args, **kwargs): 11 | if request.method == method: 12 | for param in params: 13 | valid = None 14 | # iterate through all request inputs 15 | for attr in ('args', 'form', 'files'): 16 | valid = getattr(request, attr).get(param) 17 | if valid: 18 | break 19 | if not valid: 20 | if request.referrer: 21 | flash('Required field(s) missing.') 22 | return redirect(request.referrer) 23 | abort(400) 24 | return func(*args, **kwargs) 25 | return wrapped 26 | return wrapper 27 | 28 | def login_required(func): 29 | @wraps(func) 30 | def wrapped(*args, **kwargs): 31 | if g.user: 32 | return func(*args, **kwargs) 33 | parsed_url = urlparse(request.url) 34 | location = parsed_url.path 35 | if parsed_url.query: 36 | location += '?{}'.format(parsed_url.query) 37 | return redirect(url_for('auth.login', next=location)) 38 | return wrapped 39 | 40 | def roles_required(*roles): 41 | def wrapper(func): 42 | @wraps(func) 43 | def wrapped(*args, **kwargs): 44 | if ROLES[g.user.role] not in roles: 45 | return abort(403) 46 | return func(*args, **kwargs) 47 | return wrapped 48 | return wrapper 49 | 50 | def csrf_protect(func): 51 | @wraps(func) 52 | def wrapped(*args, **kwargs): 53 | if Config.get_value('CSRF_PROTECT'): 54 | # only apply CSRF protection to POSTs 55 | if request.method == 'POST': 56 | csrf_token = session.pop('csrf_token', None) 57 | untrusted_token = request.values.get('csrf_token') 58 | if not csrf_token or untrusted_token != csrf_token: 59 | flash('CSRF detected!') 60 | return redirect(request.base_url) 61 | return func(*args, **kwargs) 62 | return wrapped 63 | 64 | def no_cache(func): 65 | @wraps(func) 66 | def wrapped(*args, **kwargs): 67 | response = make_response(func(*args, **kwargs)) 68 | response.headers['Pragma'] = 'no-cache' 69 | response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' 70 | response.headers['Expires'] = '0' 71 | return response 72 | return wrapped 73 | -------------------------------------------------------------------------------- /pwnedhub/extensions.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | from flask_session import Session 3 | 4 | db = SQLAlchemy() 5 | sess = Session() 6 | -------------------------------------------------------------------------------- /pwnedhub/fixtures/base/mail.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "subject": "QA Results", 4 | "content": "Hey Cooper,\r\n\r\nI just finished checking out the latest push of PwnedHub. I encountered a couple of errors as I was testing and placed them in a paste for you to check out at https://pastebin.com/F2mzJJJ0.", 5 | "sender_id": 5, 6 | "receiver_id": 2, 7 | "read": 1, 8 | "id": 1, 9 | "created": "2019-02-14 14:12:17", 10 | "modified": "2019-02-14 14:12:17" 11 | }, 12 | { 13 | "subject": "Training", 14 | "content": "Hey Cooper,\r\n\r\nHave you heard about that PWAPT class by Tim Tomes? Sounds like some top notch stuff. We should get him in here to do some training.", 15 | "sender_id": 4, 16 | "receiver_id": 2, 17 | "read": 1, 18 | "id": 2, 19 | "created": "2019-02-17 22:30:14", 20 | "modified": "2019-02-17 22:30:14" 21 | }, 22 | { 23 | "subject": "RE: Training", 24 | "content": "Tanner,\r\n\r\nSounds good to me. I'll put a request in to Taylor.", 25 | "sender_id": 2, 26 | "receiver_id": 4, 27 | "read": 1, 28 | "id": 3, 29 | "created": "2019-02-17 22:45:38", 30 | "modified": "2019-02-17 22:45:38" 31 | }, 32 | { 33 | "subject": "PWAPT Training", 34 | "content": "Taylor,\r\n\r\nTanner and some of the folks have been asking about some training. Specifically, the PWAPT class by Tim Tomes. You ever heard of it?", 35 | "sender_id": 2, 36 | "receiver_id": 3, 37 | "read": 1, 38 | "id": 4, 39 | "created": "2019-02-17 22:46:29", 40 | "modified": "2019-02-17 22:46:29" 41 | }, 42 | { 43 | "subject": "RE: PWAPT Training", 44 | "content": "Cooper,\r\n\r\nYeah, I've heard about that guy. He's a hack!", 45 | "sender_id": 3, 46 | "receiver_id": 2, 47 | "read": 1, 48 | "id": 5, 49 | "created": "2019-02-17 22:48:12", 50 | "modified": "2019-02-17 22:48:12" 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /pwnedhub/fixtures/base/messages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "comment": "Hey, did you guys hear that we're having a security assessment this week?", 4 | "user_id": 3, 5 | "id": 1, 6 | "created": "2019-02-18 04:55:11", 7 | "modified": "2019-02-18 04:55:11" 8 | }, 9 | { 10 | "comment": "No.", 11 | "user_id": 4, 12 | "id": 2, 13 | "created": "2019-02-18 04:55:19", 14 | "modified": "2019-02-18 04:55:19" 15 | }, 16 | { 17 | "comment": "First I'm hearing of it. I hope they don't find any bugs. This is my \"get rich quick\" scheme.", 18 | "user_id": 2, 19 | "id": 3, 20 | "created": "2019-02-18 04:56:09", 21 | "modified": "2019-02-18 04:56:09" 22 | }, 23 | { 24 | "comment": "Heh. Me too. So looking forward to afternoons on my yacht. :-)", 25 | "user_id": 3, 26 | "id": 4, 27 | "created": "2019-02-18 04:57:02", 28 | "modified": "2019-02-18 04:57:02" 29 | }, 30 | { 31 | "comment": "Wait... didn't we go live this week?", 32 | "user_id": 4, 33 | "id": 5, 34 | "created": "2019-02-18 04:57:08", 35 | "modified": "2019-02-18 04:57:08" 36 | }, 37 | { 38 | "comment": "Well, as the most interesting man in the world says, \"I don't always get apps tested, but when I do, I get it done in prod.\"", 39 | "user_id": 2, 40 | "id": 6, 41 | "created": "2019-02-18 04:57:20", 42 | "modified": "2019-02-18 04:57:20" 43 | }, 44 | { 45 | "comment": "LOL! So, yeah, did any of you guys fix those things I found during QA testing? I sent Cooper a link to them in a private message.", 46 | "user_id": 5, 47 | "id": 7, 48 | "created": "2019-02-18 04:57:32", 49 | "modified": "2019-02-18 04:57:32" 50 | }, 51 | { 52 | "comment": "No.", 53 | "user_id": 4, 54 | "id": 8, 55 | "created": "2019-02-18 04:57:37", 56 | "modified": "2019-02-18 04:57:37" 57 | }, 58 | { 59 | "comment": "My bad.", 60 | "user_id": 2, 61 | "id": 9, 62 | "created": "2019-02-18 04:57:41", 63 | "modified": "2019-02-18 04:57:41" 64 | }, 65 | { 66 | "comment": "Uh oh...", 67 | "user_id": 3, 68 | "id": 10, 69 | "created": "2019-02-18 04:57:46", 70 | "modified": "2019-02-18 04:57:46" 71 | }, 72 | { 73 | "comment": "Wow. We're totally going to end up on https://haveibeenpwned.com/PwnedWebsites.", 74 | "user_id": 5, 75 | "id": 11, 76 | "created": "2019-02-18 04:59:31", 77 | "modified": "2019-02-18 04:59:31" 78 | } 79 | ] 80 | -------------------------------------------------------------------------------- /pwnedhub/fixtures/base/tools.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Dig", 4 | "path": "dig", 5 | "description": "(Domain Internet Groper) Network administration tool for Domain Name System (DNS) name server interrogation.", 6 | "id": 1, 7 | "created": "2019-02-16 02:09:59", 8 | "modified": "2019-02-16 02:09:59" 9 | }, 10 | { 11 | "name": "Nmap", 12 | "path": "nmap", 13 | "description": "(Network Mapper) Utility for network discovery and security auditing.", 14 | "id": 2, 15 | "created": "2019-02-16 02:10:29", 16 | "modified": "2019-02-16 02:10:29" 17 | }, 18 | { 19 | "name": "Nikto", 20 | "path": "nikto", 21 | "description": "Signature-based web server scanner.", 22 | "id": 3, 23 | "created": "2019-02-16 02:10:59", 24 | "modified": "2019-02-16 02:10:59" 25 | }, 26 | { 27 | "name": "SSLyze", 28 | "path": "sslyze", 29 | "description": "Fast and powerful SSL/TLS server scanning library.", 30 | "id": 4, 31 | "created": "2019-02-16 02:11:29", 32 | "modified": "2019-02-16 02:11:29" 33 | }, 34 | { 35 | "name": "SQLmap", 36 | "path": "sqlmap --batch", 37 | "description": "Penetration testing tool that automates the process of detecting and exploiting SQL injection flaws.", 38 | "id": 5, 39 | "created": "2019-02-16 02:11:59", 40 | "modified": "2019-02-16 02:11:59" 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /pwnedhub/fixtures/base/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "username": "admin", 4 | "email": "admin@pwnedhub.com", 5 | "name": "Administrator", 6 | "avatar": "/static/common/images/avatars/admin.png", 7 | "signature": "All your base are belong to me.", 8 | "password_hash": "QQsEEwIRJgAXUBY=", 9 | "question": 1, 10 | "answer": "Diego", 11 | "role": 0, 12 | "status": 1, 13 | "id": 1, 14 | "created": "2019-02-16 01:51:59", 15 | "modified": "2019-02-16 01:51:59" 16 | }, 17 | { 18 | "username": "Cooperman", 19 | "email": "cooper@pwnedhub.com", 20 | "name": "Cooper", 21 | "avatar": "/static/common/images/avatars/c-man.png", 22 | "signature": "Gamer, hacker, and basketball player. Energy sword FTW!", 23 | "password_hash": "cBdTBwdALwoLAlY=", 24 | "question": 3, 25 | "answer": "Augusta", 26 | "role": 1, 27 | "status": 1, 28 | "id": 2, 29 | "created": "2019-02-16 04:46:27", 30 | "modified": "2019-02-16 04:46:27" 31 | }, 32 | { 33 | "username": "Babygirl#1", 34 | "email": "taylor@pwnedhub.com", 35 | "name": "Taylor", 36 | "avatar": "/static/common/images/avatars/wolf.jpg", 37 | "signature": "Wolf in a past life. Nerd in the current. Johnny 5 is indeed alive.", 38 | "password_hash": "RwoRAAAXPw0WVhYG", 39 | "question": 2, 40 | "answer": "Rocket", 41 | "role": 1, 42 | "status": 1, 43 | "id": 3, 44 | "created": "2019-02-16 04:47:14", 45 | "modified": "2019-02-16 04:47:14" 46 | }, 47 | { 48 | "username": "Hack3rPrincess", 49 | "email": "tanner@pwnedhub.com", 50 | "name": "Tanner", 51 | "avatar": "/static/common/images/avatars/kitty.jpg", 52 | "signature": "I might be small, cute, and cuddly, but remember... dynamite comes in small tightly wrapped packages that go boom.", 53 | "password_hash": "RgQXBgAGMhYNRRUPFw==", 54 | "question": 0, 55 | "answer": "Drumstick", 56 | "role": 1, 57 | "status": 1, 58 | "id": 4, 59 | "created": "2019-02-16 04:48:19", 60 | "modified": "2019-02-16 04:48:19" 61 | }, 62 | { 63 | "username": "Baconator", 64 | "email": "emilee@pwnedhub.com", 65 | "name": "Emilee", 66 | "avatar": "/static/common/images/avatars/bacon.png", 67 | "signature": "Late to the party, but still the life of the party.", 68 | "password_hash": "XA4AFksXJAhWHVZVXQ==", 69 | "question": 4, 70 | "answer": "Chick-fil-a", 71 | "role": 1, 72 | "status": 1, 73 | "id": 5, 74 | "created": "2019-02-16 04:49:34", 75 | "modified": "2019-02-16 04:49:34" 76 | } 77 | ] 78 | -------------------------------------------------------------------------------- /pwnedhub/routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/pwnedhub/routes/__init__.py -------------------------------------------------------------------------------- /pwnedhub/routes/errors.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, render_template, render_template_string, jsonify 2 | from pwnedhub import db 3 | from urllib.parse import unquote 4 | import traceback 5 | 6 | blp = Blueprint('errors', __name__) 7 | 8 | CONTENT_TYPE = 'application/json' 9 | 10 | # error handling controllers 11 | 12 | @blp.app_errorhandler(400) 13 | def bad_request(e): 14 | if request.content_type == CONTENT_TYPE: 15 | return jsonify(status=400, message=e.description), 400 16 | else: 17 | return e 18 | 19 | @blp.app_errorhandler(403) 20 | def forbidden(e): 21 | if request.content_type == CONTENT_TYPE: 22 | return jsonify(status=403, message="Resource forbidden."), 403 23 | else: 24 | return e 25 | 26 | # affected by werkzeug v0.15.0 27 | # https://github.com/pallets/werkzeug/pull/1433 28 | @blp.app_errorhandler(404) 29 | def not_found(e): 30 | if request.content_type == CONTENT_TYPE: 31 | return jsonify(status=404, message="Resource not found."), 404 32 | else: 33 | template = '''{% extends "layout.html" %} 34 | {% block body %} 35 |
36 |

Oops! That page doesn't exist.

37 |

'''+unquote(request.url)+'''

38 |
39 | {% endblock %}''' 40 | return render_template_string(template), 404 41 | 42 | @blp.app_errorhandler(405) 43 | def method_not_allowed(e): 44 | if request.content_type == CONTENT_TYPE: 45 | return jsonify(status=405, message="Method not allowed."), 405 46 | else: 47 | return e 48 | 49 | @blp.app_errorhandler(500) 50 | def internal_server_error(e): 51 | db.session.rollback() 52 | message = traceback.format_exc() 53 | if request.content_type == CONTENT_TYPE: 54 | return jsonify(status=500, message=message), 500 55 | else: 56 | return render_template('500.html', message=message), 500 57 | -------------------------------------------------------------------------------- /pwnedhub/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/pwnedhub/static/favicon.ico -------------------------------------------------------------------------------- /pwnedhub/static/js/pwnedhub.js: -------------------------------------------------------------------------------- 1 | // add the format method to the String object to add string formatting behavior 2 | String.prototype.format = function() { 3 | a = this; 4 | for (k in arguments) { 5 | a = a.replace("{" + k + "}", arguments[k]) 6 | } 7 | return a 8 | } 9 | 10 | function showFlash(msg) { 11 | var div = document.createElement("div"); 12 | div.className = "center-content rounded shaded"; 13 | div.innerHTML = msg; 14 | var id = "flash-" + Date.now(); 15 | div.id = id 16 | var flash = document.getElementById("flash"); 17 | flash.appendChild(div); 18 | setTimeout(function() { 19 | flash.removeChild(document.getElementById(id)); 20 | }, 5000); 21 | } 22 | 23 | function cleanRedirect(event, url) { 24 | event.preventDefault(); 25 | event.stopPropagation(); 26 | window.location = url; 27 | } 28 | 29 | function confirmRedirect(event, url) { 30 | if (confirm("Are you sure?")) { 31 | cleanRedirect(event, url); 32 | } 33 | } 34 | 35 | function cleanSubmit(event, form) { 36 | event.preventDefault(); 37 | event.stopPropagation(); 38 | form.submit(); 39 | } 40 | 41 | function confirmSubmit(event, form) { 42 | if (confirm("Are you sure?")) { 43 | cleanSubmit(event, form); 44 | } 45 | } 46 | 47 | function toggleShow() { 48 | var el = document.getElementById("password"); 49 | if (el.type =="password") { 50 | el.type = "text"; 51 | } else { 52 | el.type = "password"; 53 | } 54 | } 55 | 56 | window.addEventListener("load", function() { 57 | // flash on load if needed 58 | var queryString = window.location.search; 59 | var urlParams = new URLSearchParams(queryString); 60 | var error = urlParams.get('error') 61 | if (error !== null) { 62 | showFlash(error); 63 | } 64 | 65 | // event handler for tab navigation 66 | var tabs = document.querySelectorAll(".tabs > input[type='radio']") 67 | var panes = document.querySelectorAll(".tab-content > div") 68 | tabs.forEach(function(tab) { 69 | tab.addEventListener("click", function(evt) { 70 | panes.forEach(function(pane) { 71 | pane.classList.remove("active"); 72 | }); 73 | document.querySelector(evt.target.value).classList.add("active"); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /pwnedhub/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |

Oops! That page doesn't exist.

5 |

{{ message|safe }}

6 |
7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /pwnedhub/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |

Oops! Somethin' broke.

5 |
6 |
{{ message }}
7 |
8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /pwnedhub/templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |

Welcome to PwnedHub!

5 |

The ability to consolidate and organize testing tools and results during client engagements is key for consultants dealing with short timelines and high expectations. Unfortunately, today's options for cloud resourced security testing are poorly designed and fail to support even the most basic needs. PwnedHub attempts to solve this problem by providing a space to share knowledge, execute test cases, and store the results.

6 |

Developed by child prodigies Cooper ("Cooperman"), Taylor ("Babygirl#1"), and Tanner ("Hack3rPrincess"), PwnedHub was designed based on experience gained through months of security testing. The PwnedHub team is ambitions, talented, and so confident in their product, if you don't like it, they'll issue a full refund. No questions asked.

7 |

So what are you waiting for? Click here and get to work!

8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /pwnedhub/templates/admin_tools.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% for tool in tools %} 17 | 18 | 19 | 20 | 21 | 24 | 25 | {% endfor %} 26 | 27 |
namepathdescriptionaction
{{ tool.name }}{{ tool.path }}{{ tool.description }} 22 | 23 |
28 |
29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /pwnedhub/templates/admin_users.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | {% if users|length > 0 %} 4 |
5 | 6 | 7 | 8 | 9 | 10 | {% for user in users %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 29 | 30 | {% endfor %} 31 | 32 |
createddisplay nameusernamerolestatusaction
{{ user.created_as_string }}{{ user.name }}{{ user.username }}{{ user.role_as_string }}{{ user.status_as_string }} 18 | {% if user.is_admin %} 19 | 20 | {% else %} 21 | 22 | {% endif %} 23 | {% if user.is_enabled %} 24 | 25 | {% else %} 26 | 27 | {% endif %} 28 |
33 |
34 | {% endif %} 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /pwnedhub/templates/artifacts.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | {% if artifacts|length > 0 %} 15 | {% for artifact in artifacts %} 16 | 17 | 18 | 19 | 29 | 30 | {% endfor %} 31 | {% else %} 32 | 33 | {% endif %} 34 | 35 |
filecreatedaction
{{ artifact.filename }}{{ artifact.modified }} 20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 |
36 |
37 |
38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /pwnedhub/templates/diagnostics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PwnedHub 5 | 6 | 68 | 69 | 70 |

Platform Status

71 | 72 | 73 | {% for key, value in platform_stats.items() %} 74 | 75 | 76 | 77 | 78 | {% endfor %} 79 | 80 |
{{ key }}
{{ value }}
81 |

Log Status

82 | {% for log in log_stats %} 83 | 84 | 85 | {% for key, value in log.items() %} 86 | 87 | 88 | 89 | 90 | {% endfor %} 91 | 92 |
{{ key }}
{{ value }}
93 | {% endfor %} 94 | 95 | -------------------------------------------------------------------------------- /pwnedhub/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /pwnedhub/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /pwnedhub/templates/macros.html: -------------------------------------------------------------------------------- 1 | {% macro pagination(route, items) %} 2 | 20 | {% endmacro %} 21 | -------------------------------------------------------------------------------- /pwnedhub/templates/mail_compose.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 | 6 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /pwnedhub/templates/mail_inbox.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | {% if mail|length > 0 %} 11 | {% for letter in mail %} 12 | 13 | 14 | 15 | 16 | 17 | {% endfor %} 18 | {% else %} 19 | 20 | {% endif %} 21 | 22 |
fromsubjectdate
{{ letter.sender.name }}{{ letter.subject }}{{ letter.created_as_string }}
23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /pwnedhub/templates/mail_reply.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |
{{ letter.sender.name }}
6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 |
16 |
17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /pwnedhub/templates/mail_view.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
{{ letter.subject }}
5 |
6 |
7 |
{{ letter.sender.name }} → {{ letter.receiver.name }} @ {{ letter.created_as_string }}
8 |
9 |
{{ letter.content|safe }}
10 |
11 |
12 | 13 | 14 | 15 |
16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /pwnedhub/templates/mobile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PwnedHub 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |

PwnedHub

18 |

This application is not suited for use with mobile browsers. Download our mobile application for an enhanced mobile experience!

19 | 27 |
28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /pwnedhub/templates/notes.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 | 5 |
6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 | 14 |
15 |
16 |
17 |
18 | 76 | {% endblock %} 77 | -------------------------------------------------------------------------------- /pwnedhub/templates/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 |
21 | 22 | 27 | 28 | 29 | 30 | 31 |
32 |
33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /pwnedhub/templates/profile_view.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |

{{ user.name }}

6 |
Member since: {{ user.created_as_string[:-9] }}
7 |
{{ user.signature }}
8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /pwnedhub/templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |

Create an account.

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 |
21 | 22 | 27 | 28 | 29 | 30 |
31 |
32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /pwnedhub/templates/reset_init.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |

Password trouble?

6 | 7 | 8 | 9 |
10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /pwnedhub/templates/reset_password.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 | {% if app_config('OOB_RESET_ENABLE') %} 5 |
6 | {% else %} 7 | 8 | {% endif %} 9 |

Welcome back!

10 | 11 |
12 | 13 | 14 |
15 | 16 |
17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /pwnedhub/templates/reset_question.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |

Verify your identity.

6 | 7 | 8 | 9 |
10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /pwnedhub/templates/tools.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 | 10 | 11 | 12 |
13 |
14 |

15 |         
16 |         
17 |
18 |
19 | 75 | {% endblock %} 76 | -------------------------------------------------------------------------------- /pwnedhub/utils.py: -------------------------------------------------------------------------------- 1 | from flask import session 2 | from datetime import datetime, timezone 3 | from hashlib import md5 4 | from itertools import cycle 5 | from lxml import etree 6 | from uuid import uuid4 7 | import base64 8 | import hashlib 9 | import os 10 | import random 11 | import requests 12 | 13 | def get_current_utc_time(): 14 | return datetime.now(tz=timezone.utc) 15 | 16 | def get_local_from_utc(dtg): 17 | return dtg.replace(tzinfo=timezone.utc).astimezone(tz=None) 18 | 19 | def xor_encrypt(s, k): 20 | ciphertext = ''.join([ chr(ord(c)^ord(k)) for c,k in zip(s, cycle(k)) ]) 21 | return base64.b64encode(ciphertext.encode()).decode() 22 | 23 | def xor_decrypt(c, k): 24 | ciphertext = base64.b64decode(c.encode()).decode() 25 | return ''.join([ chr(ord(c)^ord(k)) for c,k in zip(ciphertext, cycle(k)) ]) 26 | 27 | def generate_state(length=1024): 28 | """Generates a random string of characters.""" 29 | return hashlib.sha256(os.urandom(length)).hexdigest() 30 | 31 | def generate_nonce(length=8): 32 | """Generates a pseudorandom number.""" 33 | return ''.join([str(random.randint(0, 9)) for i in range(length)]) 34 | 35 | def generate_token(): 36 | return str(uuid4()) 37 | 38 | def generate_timestamp_token(): 39 | return md5(str(int(get_current_utc_time().timestamp()*100)).encode()).hexdigest() 40 | 41 | def generate_csrf_token(): 42 | session['csrf_token'] = generate_token() 43 | return session['csrf_token'] 44 | 45 | def unfurl_url(url, headers={}): 46 | # request resource 47 | resp = requests.get(url, headers=headers) 48 | # parse meta tags 49 | html = etree.HTML(resp.content) 50 | data = {'url': url} 51 | for kw in ('site_name', 'title', 'description'): 52 | # standard 53 | prop = kw 54 | values = html.xpath('//meta[@property=\'{}\']/@content'.format(prop)) 55 | data[kw] = ' '.join(values) or None 56 | # OpenGraph 57 | prop = 'og:{}'.format(kw) 58 | values = html.xpath('//meta[@property=\'{}\']/@content'.format(prop)) 59 | data[kw] = ' '.join(values) or None 60 | return data 61 | -------------------------------------------------------------------------------- /pwnedhub/validators.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | from pwnedhub.models import Config 3 | from urllib.parse import urlparse 4 | import re 5 | 6 | def is_valid_command(cmd): 7 | pattern = r'[;&|]' 8 | if Config.get_value('OSCI_PROTECT'): 9 | pattern = r'[;&|<>`$(){}]' 10 | if re.search(pattern, cmd): 11 | return False 12 | return True 13 | 14 | def is_valid_filename(filename): 15 | # validate that the filename includes an allowed extension 16 | for ext in current_app.config['ALLOWED_EXTENSIONS']: 17 | if '.'+ext in filename: 18 | return True 19 | return False 20 | 21 | def is_valid_mimetype(mimetype): 22 | # validate that the mimetype is allowed 23 | if mimetype in current_app.config['ALLOWED_MIMETYPES']: 24 | return True 25 | return False 26 | 27 | # 6 or more characters 28 | PASSWORD_REGEX = r'.{6,}' 29 | 30 | def is_valid_password(password): 31 | if not re.match(r'^{}$'.format(PASSWORD_REGEX), password): 32 | return False 33 | return True 34 | 35 | EMAIL_REGEX = r'[^@]+@[a-zA-Z\d-]+(?:\.[a-zA-Z\d-]+)+' 36 | 37 | def is_valid_email(email): 38 | if not re.match(r'^{}$'.format(EMAIL_REGEX), email): 39 | return False 40 | return True 41 | 42 | def is_safe_url(url, origin): 43 | host = urlparse(origin).netloc 44 | proto = urlparse(origin).scheme 45 | # reject blank urls 46 | if not url: 47 | return False 48 | url = url.strip() 49 | url = url.replace('\\', '/') 50 | # simplify down to proto://, //, and / 51 | if url.startswith('///'): 52 | return False 53 | url_info = urlparse(url) 54 | # prevent browser manipulation via proto:///... 55 | if url_info.scheme and not url_info.netloc: 56 | return False 57 | # no proto for relative paths, or a matching proto for absolute paths 58 | if not url_info.scheme or url_info.scheme == proto: 59 | # no host for relative paths, or a matching host for absolute paths 60 | if not url_info.netloc or url_info.netloc == host: 61 | return True 62 | return False 63 | -------------------------------------------------------------------------------- /pwnedhub/wsgi.py: -------------------------------------------------------------------------------- 1 | from pwnedhub import create_app 2 | 3 | app = create_app() 4 | if __name__ == '__main__': 5 | app.run() 6 | -------------------------------------------------------------------------------- /pwnedspa/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-alpine 2 | 3 | ENV BUILD_DEPS="build-base gcc libc-dev" 4 | ENV RUNTIME_DEPS="" 5 | 6 | ENV PYTHONDONTWRITEBYTECODE=1 7 | ENV PYTHONUNBUFFERED=1 8 | 9 | RUN mkdir -p /src 10 | 11 | WORKDIR /src 12 | 13 | ADD ./REQUIREMENTS.txt /src/REQUIREMENTS.txt 14 | 15 | RUN apk update &&\ 16 | apk add --no-cache $BUILD_DEPS $RUNTIME_DEPS &&\ 17 | pip install --no-cache-dir --upgrade pip &&\ 18 | pip install --no-cache-dir -r REQUIREMENTS.txt &&\ 19 | apk del $BUILD_DEPS &&\ 20 | rm -rf /var/cache/apk/* 21 | -------------------------------------------------------------------------------- /pwnedspa/REQUIREMENTS-base.txt: -------------------------------------------------------------------------------- 1 | flask 2 | gunicorn 3 | -------------------------------------------------------------------------------- /pwnedspa/REQUIREMENTS.txt: -------------------------------------------------------------------------------- 1 | blinker==1.6.2 2 | click==8.1.4 3 | Flask==2.3.2 4 | gunicorn==20.1.0 5 | itsdangerous==2.1.2 6 | Jinja2==3.1.2 7 | MarkupSafe==2.1.3 8 | Werkzeug==2.3.6 9 | -------------------------------------------------------------------------------- /pwnedspa/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, Blueprint 2 | import os 3 | 4 | def create_app(): 5 | 6 | # create the Flask application 7 | app = Flask(__name__, static_url_path='/static') 8 | 9 | # configure the Flask application 10 | config_class = os.getenv('CONFIG', default='Development') 11 | app.config.from_object('pwnedspa.config.{}'.format(config_class.title())) 12 | 13 | # misc jinja configuration variables 14 | app.jinja_env.trim_blocks = True 15 | app.jinja_env.lstrip_blocks = True 16 | 17 | StaticBlueprint = Blueprint('common', __name__, static_url_path='/static/common', static_folder='../common/static') 18 | app.register_blueprint(StaticBlueprint) 19 | 20 | from pwnedspa.routes.core import blp as CoreBlueprint 21 | app.register_blueprint(CoreBlueprint) 22 | 23 | return app 24 | -------------------------------------------------------------------------------- /pwnedspa/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class BaseConfig(object): 5 | 6 | # base 7 | DEBUG = False 8 | TESTING = False 9 | SECRET_KEY = os.getenv('SECRET_KEY', default='$ecretKey') 10 | # prevents connection pool exhaustion but disables interactive debugging 11 | PRESERVE_CONTEXT_ON_EXCEPTION = False 12 | 13 | # oidc 14 | OAUTH_PROVIDERS = { 15 | 'google': { 16 | 'CLIENT_ID': '1098478339188-pvi39gpsvclmmucvu16vhrh0179sd100.apps.googleusercontent.com', 17 | 'CLIENT_SECRET': '5LFAbNk7rLa00PZOHceQfudp', 18 | 'DISCOVERY_DOC': 'https://accounts.google.com/.well-known/openid-configuration', 19 | }, 20 | } 21 | 22 | # csrf 23 | CSRF_TOKEN_NAME = 'X-Csrf-Token' 24 | 25 | # other 26 | API_BASE_URL = 'http://api.pwnedhub.com' 27 | 28 | 29 | class Development(BaseConfig): 30 | 31 | DEBUG = True 32 | 33 | 34 | class Test(BaseConfig): 35 | 36 | DEBUG = True 37 | TESTING = True 38 | 39 | 40 | class Production(BaseConfig): 41 | 42 | pass 43 | -------------------------------------------------------------------------------- /pwnedspa/routes/core.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template 2 | 3 | blp = Blueprint('core', __name__) 4 | 5 | @blp.route('/') 6 | def index(): 7 | return render_template('spa.html') 8 | -------------------------------------------------------------------------------- /pwnedspa/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/pwnedspa/static/favicon.ico -------------------------------------------------------------------------------- /pwnedspa/static/vue/app.js: -------------------------------------------------------------------------------- 1 | import Background from './components/background.js'; 2 | import Toasts from './components/toasts.js'; 3 | import Modal from './components/modal.js'; 4 | import Navigation from './components/navigation.js'; 5 | 6 | const template = ` 7 | 8 | 9 | 10 |
11 | 12 | 13 |
14 | `; 15 | 16 | export default { 17 | name: 'App', 18 | template, 19 | components: { 20 | 'background': Background, 21 | 'toasts': Toasts, 22 | 'modal': Modal, 23 | 'navigation': Navigation, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/components/background.css: -------------------------------------------------------------------------------- 1 | .background { 2 | position: fixed; 3 | z-index: -10; 4 | top: 0; 5 | left: 0; 6 | min-width: 100%; 7 | min-height: 100%; 8 | background-position: center center; 9 | background-repeat: no-repeat; 10 | background-attachment: fixed; 11 | background-size: cover; 12 | } 13 | 14 | .background-auth { 15 | background-image: linear-gradient(120deg, #fdfbfb 0%, #ebedee 100%); 16 | } 17 | 18 | .background-unauth { 19 | background-image: url(/static/common/images/background-dark.jpg); 20 | } 21 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/components/background.js: -------------------------------------------------------------------------------- 1 | import { useAuthStore } from '../stores/auth-store.js'; 2 | 3 | const { computed } = Vue; 4 | 5 | const template = ` 6 |
7 | `; 8 | 9 | export default { 10 | name: 'Background', 11 | template, 12 | setup () { 13 | const authStore = useAuthStore(); 14 | const backgroundClass = computed(() => { 15 | return authStore.isLoggedIn ? 'background-auth' : 'background-unauth'; 16 | }); 17 | return { 18 | backgroundClass, 19 | }; 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/components/google-login.js: -------------------------------------------------------------------------------- 1 | import { useAuthStore } from '../stores/auth-store.js'; 2 | import { useAppStore } from '../stores/app-store.js'; 3 | 4 | const { onMounted } = Vue; 5 | 6 | const template = ` 7 | 8 | `; 9 | 10 | export default { 11 | name: 'GoogleLogin', 12 | template, 13 | setup (props, context) { 14 | const authStore = useAuthStore(); 15 | const appStore = useAppStore(); 16 | 17 | onMounted(() => { 18 | if (window.hasOwnProperty("gapi")) { 19 | gapi.load('auth2', () => { 20 | const auth2 = window.gapi.auth2.init({ 21 | cookiepolicy: 'single_host_origin', 22 | }); 23 | auth2.attachClickHandler( 24 | 'signinBtn', 25 | {}, 26 | (googleUser) => { 27 | authStore.doLogin({id_token: googleUser.getAuthResponse().id_token}); 28 | }, 29 | (error) => { 30 | if (error.error === 'network_error') { 31 | appStore.createToast('OpenID Connect provider unreachable.'); 32 | } else if (error.error !== 'popup_closed_by_user') { 33 | appStore.createToast('OpenID Connect error ({0}).'.format(error.error)); 34 | }; 35 | }, 36 | ); 37 | }); 38 | } else { 39 | document.getElementById("signinBtn").addEventListener("click", (e) => { 40 | appStore.createToast('Google sign-in is not available. Check your internet connection and TLS configuration.'); 41 | }); 42 | } 43 | }); 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/components/link-preview.css: -------------------------------------------------------------------------------- 1 | .messages .message-container .message .link-preview a { 2 | color: inherit; 3 | text-decoration: inherit; 4 | font-weight: inherit; 5 | } 6 | 7 | .messages .message-container .message .link-preview p { 8 | font-size: 1.25rem; 9 | border-left: 2px solid red; 10 | padding-left: .5rem; 11 | margin-bottom: .5rem; 12 | } 13 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/components/link-preview.js: -------------------------------------------------------------------------------- 1 | import { LinkPreview } from '../services/api.js'; 2 | 3 | const { ref } = Vue; 4 | 5 | const template = ` 6 | 11 | `; 12 | 13 | export default { 14 | name: 'LinkPreview', 15 | template, 16 | props: { 17 | message: Object, 18 | }, 19 | setup (props) { 20 | const previews = ref([]); 21 | 22 | function parseUrls(message) { 23 | var pattern = /\w+:\/\/[^\s]+/gi; 24 | var matches = message.comment.match(pattern); 25 | return matches || []; 26 | }; 27 | 28 | async function doPreview(message) { 29 | const urls = parseUrls(message); 30 | for (let url of urls) { 31 | // remove punctuation from URLs ending a sentence 32 | const sanitizedUrl = url.replace(/[!.?]+$/g, ''); 33 | try { 34 | const json = await LinkPreview.create({url: sanitizedUrl}); 35 | const preview = { 36 | url: json.url, 37 | values: [] 38 | }; 39 | const keys = ['site_name', 'title', 'description']; 40 | for (let key of keys) { 41 | if (json[key] !== null) { 42 | preview.values.push(json[key]); 43 | }; 44 | }; 45 | if (preview.values.length > 0) { 46 | previews.value.push(preview); 47 | }; 48 | } catch (error) {}; 49 | }; 50 | }; 51 | 52 | doPreview(props.message); 53 | 54 | return { 55 | previews, 56 | }; 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/components/modal.css: -------------------------------------------------------------------------------- 1 | .modal-mask { 2 | position: fixed; 3 | z-index: 20; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | background-color: rgba(0, 0, 0, .5); 9 | transition: opacity .3s ease; 10 | overflow-x: auto; 11 | } 12 | 13 | .modal-mask .modal-container { 14 | width: 100%; 15 | height: 100%; 16 | position: relative; 17 | padding: 3rem 2rem 2rem; 18 | background-color: #fff; 19 | transition: all .3s ease; 20 | } 21 | 22 | .modal-mask .modal-container a.img-btn { 23 | position: absolute; 24 | top: 0.5rem; 25 | right: 0.5rem; 26 | } 27 | 28 | .modal-enter-active, 29 | .modal-leave-active { 30 | opacity: 0; 31 | } 32 | 33 | .modal-enter-active .modal-container, 34 | .modal-leave-active .modal-container { 35 | -webkit-transform: scale(1.1); 36 | transform: scale(1.1); 37 | } 38 | 39 | /* desktop modal */ 40 | @media all and (min-width: 960px) { 41 | 42 | .modal-mask .modal-container { 43 | width: 80%; 44 | height: 80%; 45 | border-radius: .5rem; 46 | box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/components/modal.js: -------------------------------------------------------------------------------- 1 | import { useAppStore } from '../stores/app-store.js'; 2 | 3 | const template = ` 4 | 5 | 13 | 14 | `; 15 | 16 | export default { 17 | name: 'Modal', 18 | template, 19 | setup () { 20 | const appStore = useAppStore(); 21 | return { 22 | appStore, 23 | }; 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/components/navigation.css: -------------------------------------------------------------------------------- 1 | .nav { 2 | background-color: #222;/*#fafafa;*/ 3 | color: #fff; 4 | padding: 0 2rem; 5 | box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5); 6 | } 7 | 8 | .nav a { 9 | color: inherit; 10 | text-decoration: inherit; 11 | } 12 | 13 | .nav a:hover { 14 | color: inherit; 15 | } 16 | 17 | .nav ul { 18 | list-style-type: none; 19 | } 20 | 21 | .nav ul, 22 | .nav li { 23 | margin: 0; 24 | padding: 0; 25 | } 26 | 27 | .nav .menu { 28 | display: flex; 29 | flex-wrap: wrap; 30 | align-items: center; 31 | } 32 | 33 | .nav .menu .brand { 34 | flex-grow: 1; 35 | } 36 | 37 | .nav .menu .brand img { 38 | height: 5rem; 39 | display: block; 40 | } 41 | 42 | .nav .menu li.item { 43 | width: 100%; 44 | text-align: right; 45 | display: none; 46 | } 47 | 48 | .nav .menu li.item a, 49 | .nav .menu li.item span { 50 | display: block; 51 | cursor: pointer; 52 | font-size: 1.25rem; 53 | letter-spacing: 0.1rem; 54 | text-transform: uppercase; 55 | font-weight: bold; 56 | padding: 1rem 0; 57 | } 58 | 59 | .nav .menu li.avatar img { 60 | display: block; 61 | object-fit: cover; 62 | width: 4rem; 63 | height: 4rem; 64 | margin: 0 auto; 65 | margin-right: 0; 66 | } 67 | 68 | .nav .active li.item { 69 | display: block; 70 | } 71 | 72 | /* desktop navigation */ 73 | @media all and (min-width: 960px) { 74 | 75 | .nav .menu { 76 | flex-wrap: nowrap; 77 | justify-content: center; 78 | } 79 | 80 | .nav .menu .brand { 81 | flex: 1; 82 | } 83 | 84 | .nav .menu li.item { 85 | display: block; 86 | width: auto; 87 | } 88 | 89 | .nav .menu li.item:hover { 90 | background: red; 91 | } 92 | 93 | .nav .menu li.item a, 94 | .nav .menu li.item span { 95 | padding: 1rem; 96 | } 97 | 98 | .nav .menu li.avatar { 99 | order: 1; 100 | } 101 | 102 | .nav .menu li.avatar:hover { 103 | background: inherit; 104 | } 105 | 106 | .nav .menu li.avatar a { 107 | padding: 0 1rem; 108 | } 109 | 110 | .nav .menu li.toggle { 111 | display: none; 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/components/navigation.js: -------------------------------------------------------------------------------- 1 | import { useAuthStore } from '../stores/auth-store.js'; 2 | 3 | const { ref, watch } = Vue; 4 | const { useRoute } = VueRouter; 5 | 6 | const template = ` 7 | 22 | `; 23 | 24 | export default { 25 | name: 'Navigation', 26 | template, 27 | setup () { 28 | const authStore = useAuthStore(); 29 | const route = useRoute(); 30 | 31 | const isOpen = ref(false); 32 | const permissions = { 33 | guest: [ 34 | { 35 | id: 0, 36 | text: 'Login', 37 | name: 'login', 38 | }, 39 | { 40 | id: 1, 41 | text: 'Signup', 42 | name: 'signup', 43 | }, 44 | ], 45 | admin: [ 46 | { 47 | id: 0, 48 | text: 'Users', 49 | name: 'users', 50 | }, 51 | { 52 | id: 1, 53 | text: 'Tools', 54 | name: 'tools', 55 | }, 56 | { 57 | id: 2, 58 | text: 'Messaging', 59 | name: 'messaging', 60 | }, 61 | ], 62 | user: [ 63 | { 64 | id: 0, 65 | text: 'Notes', 66 | name: 'notes', 67 | }, 68 | { 69 | id: 1, 70 | text: 'Scans', 71 | name: 'scans', 72 | }, 73 | { 74 | id: 2, 75 | text: 'Messaging', 76 | name: 'messaging', 77 | }, 78 | ], 79 | }; 80 | 81 | watch(() => route.name, () => { 82 | isOpen.value = false; 83 | }); 84 | 85 | function toggleMenu() { 86 | isOpen.value = !isOpen.value; 87 | }; 88 | 89 | return { 90 | authStore, 91 | isOpen, 92 | permissions, 93 | toggleMenu, 94 | }; 95 | }, 96 | }; 97 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/components/password-field.js: -------------------------------------------------------------------------------- 1 | const { ref } = Vue; 2 | 3 | const template = ` 4 |
5 | 6 | 7 |
8 | `; 9 | 10 | export default { 11 | name: 'PasswordField', 12 | template, 13 | props: { 14 | name: String, 15 | value: String, 16 | }, 17 | setup () { 18 | const showPassword = ref(false); 19 | return { 20 | showPassword, 21 | }; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/components/toasts.css: -------------------------------------------------------------------------------- 1 | .toasts { 2 | position: fixed; 3 | z-index: 10; 4 | width: 100%; 5 | top: 0; 6 | } 7 | 8 | .toast { 9 | width: 100%; 10 | text-align: center; 11 | padding: 1rem 0; 12 | font-size: 1.5rem; 13 | background-color: red; 14 | color: #fff; 15 | } 16 | 17 | .toast:last-child { 18 | box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5); 19 | } 20 | 21 | .toasts-move, 22 | .toasts-enter-active, 23 | .toasts-leave-active { 24 | transition: all 1s ease; 25 | /*transition-delay: .75s;*/ 26 | } 27 | 28 | .toasts-enter-from, 29 | .toasts-leave-to { 30 | transform: translateY(-100%); 31 | } 32 | 33 | .toasts-leave-active { 34 | position: absolute; 35 | } 36 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/components/toasts.js: -------------------------------------------------------------------------------- 1 | import { useAppStore } from '../stores/app-store.js'; 2 | 3 | const template = ` 4 | 5 |
{{ toast.text }}
6 |
7 | `; 8 | 9 | export default { 10 | name: 'Toasts', 11 | template, 12 | setup () { 13 | const appStore = useAppStore(); 14 | return { 15 | appStore, 16 | }; 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/helpers/fetch-wrapper.js: -------------------------------------------------------------------------------- 1 | import { useAuthStore } from '../stores/auth-store.js'; 2 | import { router } from '../router.js'; 3 | 4 | export const fetchWrapper = { 5 | get: request('GET'), 6 | post: request('POST'), 7 | put: request('PUT'), 8 | patch: request('PATCH'), 9 | delete: request('DELETE'), 10 | }; 11 | 12 | function request(method) { 13 | return async (url, body) => { 14 | const options = { 15 | method: method, 16 | credentials: 'include', 17 | headers: {}, 18 | }; 19 | const authStore = useAuthStore(); 20 | if (authStore.accessToken) { 21 | options.headers['Authorization'] = `Bearer ${authStore.accessToken}`; 22 | }; 23 | if (authStore.csrfToken) { 24 | options.headers[CSRF_TOKEN_NAME] = authStore.csrfToken; 25 | }; 26 | if (body) { 27 | options.headers['Content-Type'] = 'application/json'; 28 | options.body = JSON.stringify(body); 29 | }; 30 | const response = await fetch(url, options); 31 | return handleErrors(response); 32 | }; 33 | }; 34 | 35 | async function handleErrors(response) { 36 | // handle empty responses 37 | if (response.status === 204) { 38 | return {}; 39 | // handle good responses 40 | } else if (response.ok) { 41 | return await response.json(); 42 | // route unauthenticated users to login 43 | } else if (response.status === 401) { 44 | const authStore = useAuthStore(); 45 | authStore.unsetAuthInfo(); 46 | router.push('login'); 47 | throw new Error('Unauthenticated.'); 48 | // treat everything else like an error 49 | } else { 50 | const json = await response.json(); 51 | // handle Passwordless 52 | if (json.error === 'code_required') { 53 | const authStore = useAuthStore(); 54 | authStore.setCodeToken(json.code_token); 55 | router.push({ name: 'passwordless', params: { nextUrl: router.currentRoute.value.params.nextUrl } }); 56 | throw new Error('Code required for Passwordless Authentication.'); 57 | // raise an error to trigger the catch block 58 | } else { 59 | throw new Error(json.message || response.statusText); 60 | }; 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/helpers/socket.js: -------------------------------------------------------------------------------- 1 | import { io } from '../libs/socket.io.js'; // esm build 2 | 3 | export const socket = io(API_BASE_URL, { 4 | autoConnect: false, 5 | transports: ['websocket'], 6 | query: {}, 7 | }); 8 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/libs/vue-demi.js: -------------------------------------------------------------------------------- 1 | var VueDemi = (function (VueDemi, Vue, VueCompositionAPI) { 2 | if (VueDemi.install) { 3 | return VueDemi 4 | } 5 | if (!Vue) { 6 | console.error('[vue-demi] no Vue instance found, please be sure to import `vue` before `vue-demi`.') 7 | return VueDemi 8 | } 9 | 10 | // Vue 2.7 11 | if (Vue.version.slice(0, 4) === '2.7.') { 12 | for (var key in Vue) { 13 | VueDemi[key] = Vue[key] 14 | } 15 | VueDemi.isVue2 = true 16 | VueDemi.isVue3 = false 17 | VueDemi.install = function () {} 18 | VueDemi.Vue = Vue 19 | VueDemi.Vue2 = Vue 20 | VueDemi.version = Vue.version 21 | VueDemi.warn = Vue.util.warn 22 | VueDemi.hasInjectionContext = () => !!VueDemi.getCurrentInstance() 23 | function createApp(rootComponent, rootProps) { 24 | var vm 25 | var provide = {} 26 | var app = { 27 | config: Vue.config, 28 | use: Vue.use.bind(Vue), 29 | mixin: Vue.mixin.bind(Vue), 30 | component: Vue.component.bind(Vue), 31 | provide: function (key, value) { 32 | provide[key] = value 33 | return this 34 | }, 35 | directive: function (name, dir) { 36 | if (dir) { 37 | Vue.directive(name, dir) 38 | return app 39 | } else { 40 | return Vue.directive(name) 41 | } 42 | }, 43 | mount: function (el, hydrating) { 44 | if (!vm) { 45 | vm = new Vue(Object.assign({ propsData: rootProps }, rootComponent, { provide: Object.assign(provide, rootComponent.provide) })) 46 | vm.$mount(el, hydrating) 47 | return vm 48 | } else { 49 | return vm 50 | } 51 | }, 52 | unmount: function () { 53 | if (vm) { 54 | vm.$destroy() 55 | vm = undefined 56 | } 57 | }, 58 | } 59 | return app 60 | } 61 | VueDemi.createApp = createApp 62 | } 63 | // Vue 2.6.x 64 | else if (Vue.version.slice(0, 2) === '2.') { 65 | if (VueCompositionAPI) { 66 | for (var key in VueCompositionAPI) { 67 | VueDemi[key] = VueCompositionAPI[key] 68 | } 69 | VueDemi.isVue2 = true 70 | VueDemi.isVue3 = false 71 | VueDemi.install = function () {} 72 | VueDemi.Vue = Vue 73 | VueDemi.Vue2 = Vue 74 | VueDemi.version = Vue.version 75 | VueDemi.hasInjectionContext = () => !!VueDemi.getCurrentInstance() 76 | } else { 77 | console.error('[vue-demi] no VueCompositionAPI instance found, please be sure to import `@vue/composition-api` before `vue-demi`.') 78 | } 79 | } 80 | // Vue 3 81 | else if (Vue.version.slice(0, 2) === '3.') { 82 | for (var key in Vue) { 83 | VueDemi[key] = Vue[key] 84 | } 85 | VueDemi.isVue2 = false 86 | VueDemi.isVue3 = true 87 | VueDemi.install = function () {} 88 | VueDemi.Vue = Vue 89 | VueDemi.Vue2 = undefined 90 | VueDemi.version = Vue.version 91 | VueDemi.set = function (target, key, val) { 92 | if (Array.isArray(target)) { 93 | target.length = Math.max(target.length, key) 94 | target.splice(key, 1, val) 95 | return val 96 | } 97 | target[key] = val 98 | return val 99 | } 100 | VueDemi.del = function (target, key) { 101 | if (Array.isArray(target)) { 102 | target.splice(key, 1) 103 | return 104 | } 105 | delete target[key] 106 | } 107 | } else { 108 | console.error('[vue-demi] Vue version ' + Vue.version + ' is unsupported.') 109 | } 110 | return VueDemi 111 | })( 112 | (this.VueDemi = this.VueDemi || (typeof VueDemi !== 'undefined' ? VueDemi : {})), 113 | this.Vue || (typeof Vue !== 'undefined' ? Vue : undefined), 114 | this.VueCompositionAPI || (typeof VueCompositionAPI !== 'undefined' ? VueCompositionAPI : undefined) 115 | ); 116 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/libs/vue3-infinite-loading.js: -------------------------------------------------------------------------------- 1 | (function(s,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(s=typeof globalThis<"u"?globalThis:s||self,e(s.V3InfiniteLoading={},s.Vue))})(this,function(s,e){"use strict";function y(t,i){const o=t.getBoundingClientRect();if(!i)return o.top>=0&&o.bottom<=window.innerHeight;const n=i.getBoundingClientRect();return o.top>=n.top&&o.bottom<=n.bottom}async function E(t){return await e.nextTick(),t.value instanceof HTMLElement?t.value:t.value?document.querySelector(t.value):null}function f(t){let i=`0px 0px ${t.distance}px 0px`;t.top&&(i=`${t.distance}px 0px 0px 0px`);const o=new IntersectionObserver(n=>{n[0].isIntersecting&&(t.firstload&&t.emit(),t.firstload=!0)},{root:t.parentEl,rootMargin:i});return o.observe(t.infiniteLoading.value),o}const H="",u=(t,i)=>{const o=t.__vccOpts||t;for(const[n,d]of i)o[n]=d;return o},h={},S=t=>(e.pushScopeId("data-v-d3e37633"),t=t(),e.popScopeId(),t),x={class:"container"},w=[S(()=>e.createElementVNode("div",{class:"spinner"},null,-1))];function V(t,i){return e.openBlock(),e.createElementBlock("div",x,w)}const k=u(h,[["render",V],["__scopeId","data-v-d3e37633"]]),I={class:"state-error"},N=e.defineComponent({__name:"InfiniteLoading",props:{top:{type:Boolean,default:!1},target:{},distance:{default:0},identifier:{},firstload:{type:Boolean,default:!0},slots:{}},emits:["infinite"],setup(t,{emit:i}){const o=t;let n=null,d=0;const p=e.ref(null),c=e.ref(""),{top:_,firstload:L,distance:T}=o,{identifier:$,target:b}=e.toRefs(o),l={infiniteLoading:p,top:_,firstload:L,distance:T,parentEl:null,emit(){d=(l.parentEl||document.documentElement).scrollHeight,m.loading(),i("infinite",m)}},m={loading(){c.value="loading"},async loaded(){c.value="loaded";const r=l.parentEl||document.documentElement;await e.nextTick(),_&&(r.scrollTop=r.scrollHeight-d),y(p.value,l.parentEl)&&l.emit()},complete(){c.value="complete",n==null||n.disconnect()},error(){c.value="error"}};return e.watch($,()=>{n==null||n.disconnect(),n=f(l)}),e.onMounted(async()=>{l.parentEl=await E(b),n=f(l)}),e.onUnmounted(()=>{n==null||n.disconnect()}),(r,g)=>(e.openBlock(),e.createElementBlock("div",{ref_key:"infiniteLoading",ref:p,style:{"min-height":"1px"}},[e.withDirectives(e.createElementVNode("div",null,[e.renderSlot(r.$slots,"spinner",{},()=>[e.createVNode(k)],!0)],512),[[e.vShow,c.value=="loading"]]),c.value=="complete"?e.renderSlot(r.$slots,"complete",{key:0},()=>{var a;return[e.createElementVNode("span",null,e.toDisplayString(((a=r.slots)==null?void 0:a.complete)||"No more results!"),1)]},!0):e.createCommentVNode("",!0),c.value=="error"?e.renderSlot(r.$slots,"error",{key:1,retry:l.emit},()=>{var a;return[e.createElementVNode("span",I,[e.createElementVNode("span",null,e.toDisplayString(((a=r.slots)==null?void 0:a.error)||"Oops something went wrong!"),1),e.createElementVNode("button",{class:"retry",onClick:g[0]||(g[0]=(...C)=>l.emit&&l.emit(...C))},"retry")])]},!0):e.createCommentVNode("",!0)],512))}}),O="",B=u(N,[["__scopeId","data-v-a7077831"]]);s.default=B,Object.defineProperties(s,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}); 2 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/main.js: -------------------------------------------------------------------------------- 1 | import App from './app.js'; 2 | import { router } from './router.js'; 3 | 4 | const { createApp } = Vue; 5 | const { createPinia } = Pinia; 6 | const pinia = createPinia(); 7 | const app = createApp(App); 8 | 9 | app.use(router); 10 | app.use(pinia); 11 | app.mount('#app'); 12 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/modals/scans-modal.js: -------------------------------------------------------------------------------- 1 | const template = ` 2 |
3 |

 4 | 
5 | `; 6 | 7 | export default { 8 | name: 'ScansModal', 9 | template, 10 | props: { 11 | results: String, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/router.js: -------------------------------------------------------------------------------- 1 | import Signup from './views/signup.js'; 2 | import Activate from './views/activate.js'; 3 | import Login from './views/login.js'; 4 | import PasswordlessAuth from './views/passwordless.js'; 5 | import Account from './views/account.js'; 6 | import Profile from './views/profile.js'; 7 | import Notes from './views/notes.js'; 8 | import Scans from './views/scans.js'; 9 | import Messaging from './views/messages.js'; 10 | import Tools from './views/tools.js' 11 | import Users from './views/users.js' 12 | import { useAuthStore } from './stores/auth-store.js'; 13 | 14 | const { createRouter, createWebHashHistory } = VueRouter; 15 | const routes = [ 16 | { 17 | path: '/signup', 18 | name: 'signup', 19 | component: Signup, 20 | }, 21 | { 22 | path: '/signup/activate/:activateToken', 23 | name: 'activate', 24 | component: Activate, 25 | props: true, 26 | }, 27 | { 28 | path: '/login', 29 | name: 'login', 30 | component: Login, 31 | }, 32 | { 33 | path: '/login/passwordless', 34 | name: 'passwordless', 35 | component: PasswordlessAuth, 36 | }, 37 | { 38 | path: '/account', 39 | name: 'account', 40 | component: Account, 41 | meta: { 42 | authRequired: true, 43 | }, 44 | }, 45 | { 46 | path: '/profile/:userId', 47 | name: 'profile', 48 | component: Profile, 49 | props: true, 50 | meta: { 51 | authRequired: true, 52 | }, 53 | }, 54 | { 55 | path: '/notes', 56 | name: 'notes', 57 | component: Notes, 58 | meta: { 59 | authRequired: true, 60 | }, 61 | }, 62 | { 63 | path: '/scans', 64 | name: 'scans', 65 | component: Scans, 66 | meta: { 67 | authRequired: true, 68 | }, 69 | }, 70 | { 71 | path: '/messaging', 72 | name: 'messaging', 73 | component: Messaging, 74 | meta: { 75 | authRequired: true, 76 | }, 77 | }, 78 | { 79 | path: '/admin/tools', 80 | name: 'tools', 81 | component: Tools, 82 | meta: { 83 | authRequired: true, 84 | }, 85 | }, 86 | { 87 | path: '/admin/users', 88 | name: 'users', 89 | component: Users, 90 | meta: { 91 | authRequired: true, 92 | }, 93 | }, 94 | { 95 | path: '/:catchAll(.*)', 96 | redirect: '/login', 97 | }, 98 | ]; 99 | 100 | export const router = createRouter({ 101 | history: createWebHashHistory(), 102 | routes, 103 | }); 104 | 105 | router.beforeEach((to, from, next) => { 106 | const authStore = useAuthStore(); 107 | if (to.matched.some(record => record.meta.authRequired)) { 108 | if (!authStore.isLoggedIn) { 109 | next({ 110 | name: 'login', 111 | params: { nextUrl: to.fullPath }, 112 | }); 113 | } else { 114 | next(); 115 | }; 116 | } else { 117 | // the login/passwordless views use similar logic to handle routing of the nextUrl parameter 118 | // all must be updated if there is a change 119 | if (authStore.isLoggedIn) { 120 | if (authStore.isAdmin) { 121 | next({ name: 'users' }); 122 | }; 123 | next({ name: 'notes' }); 124 | } else { 125 | next(); 126 | }; 127 | }; 128 | }); 129 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/services/api.js: -------------------------------------------------------------------------------- 1 | import { fetchWrapper } from '../helpers/fetch-wrapper.js'; 2 | 3 | const AccessToken = { 4 | create(data) { 5 | return fetchWrapper.post(`${API_BASE_URL}/access-token`, data); 6 | }, 7 | delete() { 8 | return fetchWrapper.delete(`${API_BASE_URL}/access-token`); 9 | }, 10 | }; 11 | 12 | const User = { 13 | all() { 14 | return fetchWrapper.get(`${API_BASE_URL}/users`); 15 | }, 16 | get(uid) { 17 | return fetchWrapper.get(`${API_BASE_URL}/users/${uid}`); 18 | }, 19 | create(data) { 20 | return fetchWrapper.post(`${API_BASE_URL}/users`, data); 21 | }, 22 | update(uid, data) { 23 | return fetchWrapper.patch(`${API_BASE_URL}/users/${uid}`, data); 24 | }, 25 | }; 26 | 27 | const AdminUser = { 28 | update(uid, data) { 29 | return fetchWrapper.patch(`${API_BASE_URL}/admin/users/${uid}`, data); 30 | }, 31 | }; 32 | 33 | const Message = { 34 | all(rid, query) { 35 | return fetchWrapper.get(`${API_BASE_URL}/rooms/${rid}/messages${query}`); 36 | }, 37 | }; 38 | 39 | const LinkPreview = { 40 | create(data) { 41 | return fetchWrapper.post(`${API_BASE_URL}/unfurl`, data); 42 | }, 43 | }; 44 | 45 | const Note = { 46 | all() { 47 | return fetchWrapper.get(`${API_BASE_URL}/notes`); 48 | }, 49 | replace(data) { 50 | return fetchWrapper.put(`${API_BASE_URL}/notes`, data); 51 | }, 52 | }; 53 | 54 | const Tool = { 55 | all() { 56 | return fetchWrapper.get(`${API_BASE_URL}/tools`); 57 | }, 58 | create(data) { 59 | return fetchWrapper.post(`${API_BASE_URL}/tools`, data); 60 | }, 61 | delete(tid) { 62 | return fetchWrapper.delete(`${API_BASE_URL}/tools/${tid}`); 63 | }, 64 | }; 65 | 66 | const Scan = { 67 | all() { 68 | return fetchWrapper.get(`${API_BASE_URL}/scans`); 69 | }, 70 | get(sid) { 71 | return fetchWrapper.get(`${API_BASE_URL}/scans/${sid}/results`); 72 | }, 73 | create(data) { 74 | return fetchWrapper.post(`${API_BASE_URL}/scans`, data); 75 | }, 76 | delete(sid) { 77 | return fetchWrapper.delete(`${API_BASE_URL}/scans/${sid}`); 78 | }, 79 | }; 80 | 81 | 82 | export { 83 | AccessToken, 84 | User, 85 | AdminUser, 86 | Message, 87 | LinkPreview, 88 | Note, 89 | Tool, 90 | Scan, 91 | }; 92 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/stores/app-store.js: -------------------------------------------------------------------------------- 1 | const { defineStore } = Pinia; 2 | const { ref, shallowRef } = Vue; 3 | 4 | export const useAppStore = defineStore('app', () => { 5 | const toasts = ref([]); 6 | const modalVisible = ref(false); 7 | const modalComponent = shallowRef(null); 8 | const modalProps = ref({}); 9 | 10 | let maxToastId = 0; 11 | 12 | function createToast(message) { 13 | const id = ++maxToastId; 14 | toasts.value.push({id: id, text: message}); 15 | setTimeout(() => { 16 | toasts.value = toasts.value.filter(t => t.id !== id) 17 | }, 5000); 18 | }; 19 | 20 | function showModal(payload) { 21 | modalVisible.value = true; 22 | modalComponent.value = payload.componentName; 23 | modalProps.value = payload.props; 24 | }; 25 | 26 | function hideModal() { 27 | modalVisible.value = false; 28 | modalComponent.value = null; 29 | modalProps.value = {}; 30 | }; 31 | 32 | return { 33 | toasts, 34 | modalVisible, 35 | modalComponent, 36 | modalProps, 37 | createToast, 38 | showModal, 39 | hideModal, 40 | }; 41 | }); 42 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/style.css: -------------------------------------------------------------------------------- 1 | @import "/static/common/css/fontawesome.css"; 2 | @import "/static/common/css/normalize.css"; 3 | @import "/static/common/css/custom-flex.css"; 4 | @import "/static/common/css/custom-utility.css"; 5 | @import "/static/common/css/baseline.css"; 6 | @import "/static/common/css/pwnedspa.css"; 7 | @import "/static/vue/components/modal.css"; 8 | @import "/static/vue/components/toasts.css"; 9 | @import "/static/vue/components/navigation.css"; 10 | @import "/static/vue/components/background.css"; 11 | @import "/static/vue/views/users.css"; 12 | @import "/static/vue/views/tools.css"; 13 | @import "/static/vue/views/messages.css"; 14 | @import "/static/vue/components/link-preview.css"; 15 | @import "/static/vue/views/scans.css"; 16 | @import "/static/vue/views/notes.css"; 17 | @import "/static/vue/views/profile.css"; 18 | @import "/static/vue/views/account.css"; 19 | @import "/static/vue/views/reset.css"; 20 | @import "/static/vue/views/passwordless.css"; 21 | @import "/static/vue/views/login.css"; 22 | @import "/static/vue/views/signup.css"; 23 | 24 | .content-wrapper { 25 | position: relative; 26 | z-index: auto; 27 | } 28 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/account.css: -------------------------------------------------------------------------------- 1 | .account { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 1rem; 5 | } 6 | 7 | .account .avatar { 8 | width: 80%; 9 | padding-bottom: 80%; 10 | margin: 1.5rem auto; 11 | position: relative; 12 | } 13 | 14 | .account .avatar img { 15 | position: absolute; 16 | left: 0; 17 | top: 0; 18 | width: 100%; 19 | height: 100%; 20 | object-fit: cover; 21 | } 22 | 23 | .account .form { 24 | padding: 0.5rem 0; 25 | } 26 | 27 | /* desktop account */ 28 | @media all and (min-width: 960px) { 29 | 30 | .account { 31 | flex-direction: row; 32 | flex-wrap: wrap; 33 | justify-content: center; 34 | } 35 | 36 | .account > * { 37 | flex-basis: 40rem; 38 | } 39 | 40 | .account .form { 41 | margin: 0 auto; 42 | width: 30rem; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/account.js: -------------------------------------------------------------------------------- 1 | import { useAuthStore } from '../stores/auth-store.js'; 2 | import { useAppStore } from '../stores/app-store.js'; 3 | import { User } from '../services/api.js'; 4 | 5 | const { ref } = Vue; 6 | 7 | const template = ` 8 | 32 | `; 33 | 34 | export default { 35 | name: 'Account', 36 | template, 37 | setup () { 38 | const authStore = useAuthStore(); 39 | const appStore = useAppStore(); 40 | 41 | const userForm = ref({ 42 | email: '', 43 | name: '', 44 | avatar: '', 45 | signature: '', 46 | }); 47 | // intentionally not reactive to avoid re-rendering on logout 48 | const currentUser = authStore.userInfo; 49 | 50 | function setFormValues() { 51 | userForm.value.email = currentUser.email; 52 | userForm.value.name = currentUser.name; 53 | userForm.value.avatar = currentUser.avatar; 54 | userForm.value.signature = currentUser.signature; 55 | }; 56 | 57 | async function updateUser() { 58 | try { 59 | const json = await User.update(currentUser.id, userForm.value); 60 | authStore.setAuthUserInfo(json); 61 | appStore.createToast('Account updated.'); 62 | } catch (error) { 63 | appStore.createToast(error.message); 64 | }; 65 | }; 66 | 67 | setFormValues(); 68 | 69 | return { 70 | currentUser, 71 | userForm, 72 | updateUser, 73 | }; 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/activate.js: -------------------------------------------------------------------------------- 1 | import { useAppStore } from '../stores/app-store.js'; 2 | import { User } from '../services/api.js'; 3 | 4 | const { ref } = Vue; 5 | const { useRouter } = VueRouter; 6 | 7 | export default { 8 | name: 'Activate', 9 | props: { 10 | activateToken: String, 11 | }, 12 | setup (props) { 13 | const appStore = useAppStore(); 14 | const router = useRouter(); 15 | 16 | const activateForm = ref({ 17 | activate_token: props.activateToken, 18 | }); 19 | 20 | async function activateUser() { 21 | try { 22 | await User.create(activateForm.value); 23 | appStore.createToast('Account activated. Please log in.'); 24 | } catch (error) { 25 | appStore.createToast(error.message); 26 | }; 27 | router.push({ name: 'login' }); 28 | }; 29 | 30 | activateUser(); 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/login.css: -------------------------------------------------------------------------------- 1 | .login { 2 | display: flex; 3 | flex-direction: column; 4 | color: white; 5 | padding: 1rem; 6 | } 7 | 8 | .login .form { 9 | padding: 1rem 2rem; 10 | background-color: rgba(0,0,0,0.6); 11 | } 12 | 13 | .login .form .oidc-button { 14 | cursor: pointer; 15 | max-width: 100%; 16 | } 17 | 18 | .login .panels > * { 19 | flex-basis: 20rem; 20 | margin: 2rem 0; 21 | } 22 | 23 | /* desktop login */ 24 | @media all and (min-width: 960px) { 25 | 26 | .login { 27 | flex-direction: row; 28 | flex-wrap: wrap; 29 | justify-content: center; 30 | width: 960px; 31 | } 32 | 33 | .login > * { 34 | flex-basis: 40rem; 35 | } 36 | 37 | .login .form { 38 | margin: 0 auto; 39 | width: 30rem; 40 | } 41 | 42 | .login .panels { 43 | flex-basis: 100%; 44 | margin-top: 5rem; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/login.js: -------------------------------------------------------------------------------- 1 | import GoogleLogin from '../components/google-login.js'; 2 | import { useAuthStore } from '../stores/auth-store.js'; 3 | 4 | const { ref } = Vue; 5 | 6 | const template = ` 7 | 42 | `; 43 | 44 | export default { 45 | name: 'Login', 46 | template, 47 | components: { 48 | 'google-oidc': GoogleLogin, 49 | }, 50 | setup () { 51 | const authStore = useAuthStore(); 52 | 53 | const loginForm = ref({ 54 | email: '', 55 | }); 56 | 57 | function doFormLogin() { 58 | authStore.doLogin(loginForm.value); 59 | }; 60 | 61 | return { 62 | loginForm, 63 | doFormLogin, 64 | }; 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/messages.css: -------------------------------------------------------------------------------- 1 | .rooms { 2 | position: relative; 3 | } 4 | 5 | .rooms .closed { 6 | transform: translateX(-15rem); 7 | } 8 | 9 | .rooms .tab { 10 | position: absolute; 11 | top: calc(50vh - 2rem - 50px); 12 | left: 15rem; 13 | z-index: 10; 14 | background-color: #333; 15 | color: #eee; 16 | padding: 2rem 1rem; 17 | border-radius: 0 0.5rem 0.5rem 0; 18 | box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5); 19 | transition: all 0.3s ease-in; 20 | } 21 | 22 | .rooms .rooms-wrapper { 23 | position: absolute; 24 | top: 0; 25 | left: 0; 26 | width: 15rem; 27 | z-index: 20; 28 | box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5); 29 | transition: all 0.3s ease-in; 30 | } 31 | 32 | .rooms .rooms-wrapper { 33 | background-color: #333; 34 | color: #eee; 35 | height: calc(100vh - 50px); /* height of nav (50) */ 36 | overflow-y: auto; 37 | } 38 | 39 | .rooms .rooms-wrapper > * { 40 | padding: 1rem; 41 | border-bottom: 1px solid #444; 42 | } 43 | 44 | .rooms .rooms-wrapper .label { 45 | font-size: 1.25rem; 46 | letter-spacing: 0.1rem; 47 | text-transform: uppercase; 48 | } 49 | 50 | .rooms .rooms-wrapper .room { 51 | cursor: pointer; 52 | } 53 | 54 | .rooms .rooms-wrapper .room.active { 55 | font-weight: bold; 56 | } 57 | 58 | .rooms .rooms-wrapper .room.tagged:before { 59 | content:"• "; 60 | color: red; 61 | } 62 | 63 | /* desktop rooms */ 64 | @media all and (min-width: 960px) { 65 | 66 | .rooms .tab { 67 | display: none; 68 | } 69 | 70 | .rooms .rooms-wrapper { 71 | position: static; 72 | } 73 | 74 | .rooms .closed { 75 | transform: none; 76 | } 77 | 78 | } 79 | 80 | .messages { 81 | height: calc(100vh - 50px); /* height of nav (50) */ 82 | padding: 0 1rem 1rem 1rem; 83 | } 84 | 85 | .messages .message-container { 86 | overflow-y: scroll; 87 | } 88 | 89 | .messages .message-container .message { 90 | position: relative; 91 | padding: 1rem 0; 92 | } 93 | 94 | .messages .message-container .message .avatar { 95 | margin: 0 1rem; 96 | } 97 | 98 | .messages .message-container .message .avatar img { 99 | display: block; 100 | object-fit: cover; 101 | width: 4rem; 102 | height: 4rem; 103 | } 104 | 105 | .messages .message-container .message .name { 106 | font-size: 1.5rem; 107 | font-weight: bold; 108 | margin-bottom: .5rem; 109 | } 110 | 111 | .messages .message-container .message .comment { 112 | margin-bottom: .5rem; 113 | } 114 | 115 | .messages .message-container .message a.img-btn { 116 | position: absolute; 117 | top: 0.5rem; 118 | right: 0.5rem; 119 | } 120 | 121 | .messages .message-container .message .timestamp { 122 | font-size: 1rem; 123 | margin-bottom: 0; 124 | } 125 | 126 | .messages .message-form { 127 | position: relative; 128 | } 129 | 130 | .messages .message-form input[type=text] { 131 | margin: 0; 132 | } 133 | 134 | .messages .message-form button { 135 | line-height: 1em; 136 | border: none; 137 | background-color: transparent; 138 | position: absolute; 139 | right: 0; 140 | top: 0; 141 | border: 0; 142 | } 143 | 144 | /* desktop messages */ 145 | @media all and (min-width: 960px) { 146 | 147 | .messages .message-container .message:hover { 148 | background-color: #eee; 149 | } 150 | 151 | .messages .message-container .message a.img-btn { 152 | display: none; 153 | } 154 | 155 | .messages .message-container .message:hover a.img-btn { 156 | display: inline; 157 | } 158 | 159 | .messages .message-form button { 160 | display: none; 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/notes.css: -------------------------------------------------------------------------------- 1 | .notes { 2 | height: 100%; 3 | } 4 | 5 | .notes textarea { 6 | height: auto; /* skeleton override */ 7 | padding: 1rem 1.5rem; 8 | border: 1px solid #bbb; 9 | margin: 1rem; 10 | resize: none; 11 | } 12 | 13 | .markdown { 14 | padding: 2rem 3rem; 15 | border: 1px solid #e1e1e1; 16 | } 17 | 18 | .markdown li { 19 | margin-bottom: 0; 20 | } 21 | 22 | .markdown code { 23 | overflow: scroll; 24 | } 25 | 26 | /* tabs */ 27 | 28 | .tabs { 29 | } 30 | 31 | .tabs > input[type=radio] { 32 | display: none; 33 | } 34 | 35 | .tabs > label { 36 | background-color: #eee; 37 | border: 1px solid #e1e1e1; 38 | padding: 0.5rem 1rem; 39 | margin: 0; 40 | cursor: pointer; 41 | z-index: 1; 42 | } 43 | 44 | .tabs > input[type=radio]:checked + label { 45 | background: #fff; 46 | border-bottom: 1px solid #fff; 47 | } 48 | 49 | .tab-content > div { 50 | margin-top: -1px; /* overlaps the border of the lab */ 51 | background-color: #fff; 52 | border: 1px solid #e1e1e1; 53 | } 54 | 55 | .tab-content > div:not(.active) { 56 | position: absolute; 57 | top: -9999px; 58 | left: -9999px; 59 | height: 1px; 60 | width: 1px; 61 | } 62 | 63 | /* tabs end */ 64 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/notes.js: -------------------------------------------------------------------------------- 1 | import { useAppStore } from '../stores/app-store.js'; 2 | import { Note } from '../services/api.js'; 3 | import { marked } from '../libs/marked.js'; // esm build 4 | 5 | const { ref } = Vue; 6 | 7 | const template = ` 8 |
9 |
10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 |
19 |
20 |
21 |
22 | `; 23 | 24 | export default { 25 | name: 'Notes', 26 | template, 27 | setup () { 28 | const appStore = useAppStore(); 29 | 30 | const note = ref(''); 31 | const markdown = ref(''); 32 | const activePane = ref('view'); 33 | 34 | async function getNote() { 35 | try { 36 | const json = await Note.all(); 37 | note.value = json.content; 38 | renderNote(); 39 | } catch (error) { 40 | appStore.createToast(error.message); 41 | }; 42 | }; 43 | 44 | function renderNote() { 45 | if (note.value != null) { 46 | markdown.value = marked.parse(note.value); 47 | }; 48 | }; 49 | 50 | async function updateNote() { 51 | try { 52 | await Note.replace({content: note.value}); 53 | } catch (error) { 54 | appStore.createToast(error.message); 55 | }; 56 | }; 57 | 58 | function isActive(tab) { 59 | return activePane.value === tab; 60 | }; 61 | 62 | function setActive(tab) { 63 | activePane.value = tab; 64 | }; 65 | 66 | getNote(); 67 | 68 | return { 69 | note, 70 | markdown, 71 | isActive, 72 | setActive, 73 | renderNote, 74 | updateNote, 75 | }; 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/passwordless.css: -------------------------------------------------------------------------------- 1 | .passwordless { 2 | color: white; 3 | padding: 1rem; 4 | } 5 | 6 | .passwordless .form { 7 | padding: 1rem 2rem; 8 | background-color: rgba(0,0,0,0.6); 9 | } 10 | 11 | /* desktop reset */ 12 | @media all and (min-width: 960px) { 13 | 14 | .passwordless .form { 15 | margin: 0 auto; 16 | width: 30rem; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/passwordless.js: -------------------------------------------------------------------------------- 1 | import { useAuthStore } from '../stores/auth-store.js'; 2 | 3 | const { ref, onBeforeUnmount } = Vue; 4 | 5 | const template = ` 6 |
7 |
8 |

Check your email.

9 |

A Passwordless Authentication code has been emailed to you.

10 | 11 | 12 | 13 |
14 |
15 | `; 16 | 17 | export default { 18 | name: 'PasswordlessAuth', 19 | template, 20 | setup () { 21 | const authStore = useAuthStore(); 22 | 23 | const codeForm = ref({ 24 | code: '', 25 | code_token: '', 26 | }); 27 | 28 | function doSubmitCode() { 29 | codeForm.value.code_token = authStore.codeToken; 30 | authStore.doLogin(codeForm.value); 31 | }; 32 | 33 | onBeforeUnmount(() => { 34 | authStore.unsetCodeToken(); 35 | }); 36 | 37 | return { 38 | codeForm, 39 | doSubmitCode, 40 | }; 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/profile.css: -------------------------------------------------------------------------------- 1 | .profile { 2 | padding: 1rem; 3 | } 4 | 5 | .profile .avatar { 6 | width: 80%; 7 | padding-bottom: 80%; 8 | margin: 2rem auto; 9 | position: relative; 10 | } 11 | 12 | .profile .avatar img { 13 | position: absolute; 14 | left: 0; 15 | top: 0; 16 | width: 100%; 17 | height: 100%; 18 | object-fit: cover; 19 | } 20 | 21 | .profile blockquote { 22 | font-style: italic; 23 | margin-left: 0; 24 | margin-right: 0; 25 | quotes: "\201C""\201D""\2018""\2019"; 26 | } 27 | 28 | .profile blockquote:before { 29 | color: #bbb; 30 | font-family: Arial; 31 | content: open-quote; 32 | font-size: 4em; 33 | line-height: 0.1em; 34 | margin-right: 0.25em; 35 | vertical-align: -0.4em; 36 | } 37 | 38 | /* desktop profile */ 39 | @media all and (min-width: 960px) { 40 | 41 | .profile { 42 | margin: 0 auto; 43 | width: 30rem; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/profile.js: -------------------------------------------------------------------------------- 1 | import { useAppStore } from '../stores/app-store.js'; 2 | import { User } from '../services/api.js'; 3 | 4 | const { ref } = Vue; 5 | 6 | const template = ` 7 |
8 |
9 |

{{ user.name }}

10 |
Member since: {{ user.created }}
11 |
{{ user.signature }}
12 |
13 | `; 14 | 15 | export default { 16 | name: 'Profile', 17 | template, 18 | props: { 19 | userId: [Number, String], 20 | }, 21 | setup (props) { 22 | const appStore = useAppStore(); 23 | 24 | const user = ref(null); 25 | 26 | async function getUser() { 27 | try { 28 | const json = await User.get(props.userId); 29 | user.value = json; 30 | } catch (error) { 31 | appStore.createToast(error.message); 32 | }; 33 | }; 34 | 35 | getUser(); 36 | 37 | return { 38 | user, 39 | }; 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/reset.css: -------------------------------------------------------------------------------- 1 | .reset { 2 | color: white; 3 | padding: 1rem; 4 | } 5 | 6 | .reset .form { 7 | padding: 1rem 2rem; 8 | background-color: rgba(0,0,0,0.6); 9 | } 10 | 11 | /* desktop reset */ 12 | @media all and (min-width: 960px) { 13 | 14 | .reset .form { 15 | margin: 0 auto; 16 | width: 30rem; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/scans.css: -------------------------------------------------------------------------------- 1 | .scans { 2 | padding: 1rem; 3 | } 4 | 5 | .scans .scan-form { 6 | display: flex; 7 | flex-direction: column; 8 | margin-bottom: 1rem; 9 | } 10 | 11 | .scans .scan-form .scan-form-args { 12 | position: relative; 13 | } 14 | 15 | .scans .scan-form .scan-form-args button { 16 | line-height: 1em; 17 | border: none; 18 | background-color: transparent; 19 | position: absolute; 20 | right: 0; 21 | top: 0; 22 | border: 0; 23 | } 24 | 25 | .scans .scans-table pre { 26 | white-space: pre-wrap; 27 | } 28 | 29 | .scans .scans-table .scans-table-row:hover { 30 | cursor: pointer; 31 | } 32 | 33 | /* desktop scans */ 34 | @media all and (min-width: 960px) { 35 | 36 | /*flex-row flex-wrap flex-align-center */ 37 | .scans .scan-form { 38 | flex-direction: row; 39 | flex-wrap: wrap; 40 | } 41 | 42 | .scans .scan-form select { 43 | margin-right: 1rem; 44 | } 45 | 46 | .scans .scan-form .scan-form-args { 47 | flex-grow: 1; 48 | } 49 | 50 | .scans .scan-form .scan-form-meta { 51 | flex-basis: 100%; 52 | } 53 | 54 | .scans .scan-form .scan-form-args button { 55 | display: none; 56 | } 57 | 58 | .scans .scans-table .scans-table-row .actions-cell { 59 | width: 100%; 60 | text-align: center; 61 | } 62 | 63 | } 64 | 65 | /* needed for scrolling overflow */ 66 | .scans-modal { 67 | position: absolute; 68 | top: 3rem; 69 | left: 2rem; 70 | right: 2rem; 71 | bottom: 2rem; 72 | overflow: auto; 73 | } 74 | 75 | .scans-modal pre { 76 | margin: 0; 77 | } 78 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/signup.css: -------------------------------------------------------------------------------- 1 | .signup { 2 | display: flex; 3 | flex-direction: column; 4 | color: white; 5 | padding: 1rem; 6 | } 7 | 8 | .signup .form { 9 | padding: 1rem 2rem; 10 | background-color: rgba(0,0,0,0.6); 11 | } 12 | 13 | .signup .about { 14 | text-align: justify; 15 | } 16 | 17 | /* desktop signup */ 18 | @media all and (min-width: 960px) { 19 | 20 | .signup { 21 | flex-direction: row; 22 | flex-wrap: wrap; 23 | justify-content: center; 24 | } 25 | 26 | .signup > * { 27 | flex-basis: 40rem; 28 | } 29 | 30 | .signup .form { 31 | margin: 0 auto; 32 | width: 30rem; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/signup.js: -------------------------------------------------------------------------------- 1 | import { useAppStore } from '../stores/app-store.js'; 2 | import { User } from '../services/api.js'; 3 | 4 | const { ref } = Vue; 5 | const { useRouter } = VueRouter; 6 | 7 | const template = ` 8 | 29 | `; 30 | 31 | export default { 32 | name: 'Signup', 33 | template, 34 | setup () { 35 | const appStore = useAppStore(); 36 | const router = useRouter(); 37 | 38 | const signupForm = ref({ 39 | email: '', 40 | name: '', 41 | avatar: '', 42 | signature: '', 43 | }); 44 | 45 | async function doSignup() { 46 | try { 47 | await User.create(signupForm.value); 48 | appStore.createToast('Account activation email sent. Please activate your account to log in.'); 49 | router.push({ name: 'login' }); 50 | } catch (error) { 51 | appStore.createToast(error.message); 52 | }; 53 | }; 54 | 55 | return { 56 | signupForm, 57 | doSignup, 58 | }; 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/tools.css: -------------------------------------------------------------------------------- 1 | .tools { 2 | padding: 1rem; 3 | } 4 | 5 | .tools .tool-form { 6 | display: flex; 7 | flex-direction: column; 8 | margin-bottom: 1rem; 9 | } 10 | 11 | .tools .tools-table pre { 12 | white-space: pre-wrap; 13 | } 14 | 15 | /* desktop tools */ 16 | @media all and (min-width: 960px) { 17 | 18 | /*flex-row flex-wrap flex-align-center */ 19 | .tools .tool-form { 20 | flex-direction: row; 21 | flex-wrap: wrap; 22 | } 23 | 24 | .tools .tool-form input { 25 | margin-right: 1rem; 26 | } 27 | 28 | .tools .tools-table .tools-table-row .actions-cell { 29 | width: 100%; 30 | text-align: center; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /pwnedspa/static/vue/views/users.css: -------------------------------------------------------------------------------- 1 | .users { 2 | padding: 1rem; 3 | } 4 | 5 | .users .avatar { 6 | margin-right: 1rem; 7 | } 8 | 9 | .users .avatar img { 10 | display: block; 11 | object-fit: cover; 12 | width: 4rem; 13 | height: 4rem; 14 | } 15 | 16 | /* desktop users */ 17 | @media all and (min-width: 960px) { 18 | 19 | .users .users-table .users-table-row .actions-cell { 20 | width: 100%; 21 | text-align: center; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /pwnedspa/templates/spa.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PwnedHub 2.0 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /pwnedspa/wsgi.py: -------------------------------------------------------------------------------- 1 | from pwnedspa import create_app 2 | 3 | app = create_app() 4 | if __name__ == '__main__': 5 | app.run() 6 | -------------------------------------------------------------------------------- /pwnedsso/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-alpine 2 | 3 | ENV BUILD_DEPS="build-base gcc libc-dev mariadb-dev" 4 | ENV RUNTIME_DEPS="mariadb-connector-c-dev" 5 | 6 | ENV PYTHONDONTWRITEBYTECODE=1 7 | ENV PYTHONUNBUFFERED=1 8 | 9 | RUN mkdir -p /src 10 | 11 | WORKDIR /src 12 | 13 | ADD ./REQUIREMENTS.txt /src/REQUIREMENTS.txt 14 | 15 | RUN apk update &&\ 16 | apk add --no-cache $BUILD_DEPS $RUNTIME_DEPS &&\ 17 | pip install --no-cache-dir --upgrade pip &&\ 18 | pip install --no-cache-dir -r REQUIREMENTS.txt &&\ 19 | apk del $BUILD_DEPS &&\ 20 | rm -rf /var/cache/apk/* 21 | -------------------------------------------------------------------------------- /pwnedsso/REQUIREMENTS-base.txt: -------------------------------------------------------------------------------- 1 | flask 2 | flask-mysqldb 3 | flask-sqlalchemy 4 | gunicorn 5 | mysqlclient 6 | pyjwt 7 | -------------------------------------------------------------------------------- /pwnedsso/REQUIREMENTS.txt: -------------------------------------------------------------------------------- 1 | blinker==1.6.2 2 | click==8.1.4 3 | Flask==2.3.2 4 | Flask-MySQLdb==1.0.1 5 | Flask-SQLAlchemy==3.0.5 6 | greenlet==2.0.2 7 | gunicorn==20.1.0 8 | itsdangerous==2.1.2 9 | Jinja2==3.1.2 10 | MarkupSafe==2.1.3 11 | mysqlclient==2.2.0 12 | PyJWT==2.7.0 13 | SQLAlchemy==2.0.18 14 | typing_extensions==4.7.1 15 | Werkzeug==2.3.6 16 | -------------------------------------------------------------------------------- /pwnedsso/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from pwnedsso.extensions import db 3 | import os 4 | 5 | def create_app(): 6 | 7 | # create the Flask application 8 | app = Flask(__name__, static_url_path='/static') 9 | 10 | # configure the Flask application 11 | config_class = os.getenv('CONFIG', default='Development') 12 | app.config.from_object('pwnedsso.config.{}'.format(config_class.title())) 13 | 14 | db.init_app(app) 15 | 16 | from pwnedsso.routes.sso import blp as SsoBlueprint 17 | app.register_blueprint(SsoBlueprint) 18 | 19 | return app 20 | -------------------------------------------------------------------------------- /pwnedsso/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class BaseConfig(object): 5 | 6 | # base 7 | DEBUG = False 8 | TESTING = False 9 | SECRET_KEY = os.getenv('SECRET_KEY', default='$ecretKey') 10 | # prevents connection pool exhaustion but disables interactive debugging 11 | PRESERVE_CONTEXT_ON_EXCEPTION = False 12 | 13 | # database 14 | DATABASE_HOST = os.environ.get('DATABASE_HOST', 'localhost') 15 | SQLALCHEMY_DATABASE_URI = f"mysql://pwnedhub:dbconnectpass@{DATABASE_HOST}/pwnedhub" 16 | SQLALCHEMY_TRACK_MODIFICATIONS = False 17 | 18 | 19 | class Development(BaseConfig): 20 | 21 | DEBUG = True 22 | 23 | 24 | class Test(BaseConfig): 25 | 26 | DEBUG = True 27 | TESTING = True 28 | 29 | 30 | class Production(BaseConfig): 31 | 32 | pass 33 | -------------------------------------------------------------------------------- /pwnedsso/extensions.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | db = SQLAlchemy() 4 | -------------------------------------------------------------------------------- /pwnedsso/models.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | from pwnedsso import db 3 | from pwnedsso.utils import get_current_utc_time, get_local_from_utc, xor_encrypt 4 | from secrets import token_urlsafe 5 | 6 | 7 | class BaseModel(db.Model): 8 | __abstract__ = True 9 | id = db.Column(db.Integer, primary_key=True) 10 | created = db.Column(db.DateTime, nullable=False, default=get_current_utc_time) 11 | modified = db.Column(db.DateTime, nullable=False, default=get_current_utc_time, onupdate=get_current_utc_time) 12 | 13 | @property 14 | def _name(self): 15 | return self.__class__.__name__.lower() 16 | 17 | @property 18 | def created_as_string(self): 19 | return get_local_from_utc(self.created).strftime("%Y-%m-%d %H:%M:%S") 20 | 21 | @property 22 | def modified_as_string(self): 23 | return get_local_from_utc(self.modified).strftime("%Y-%m-%d %H:%M:%S") 24 | 25 | 26 | class User(BaseModel): 27 | __tablename__ = 'users' 28 | username = db.Column(db.String(255), nullable=False, unique=True) 29 | email = db.Column(db.String(255), nullable=False, unique=True) 30 | name = db.Column(db.String(255), nullable=False) 31 | avatar = db.Column(db.Text) 32 | signature = db.Column(db.Text) 33 | password_hash = db.Column(db.String(255)) 34 | question = db.Column(db.Integer, nullable=False, default=0) 35 | answer = db.Column(db.String(255), nullable=False, default=token_urlsafe(10)) 36 | role = db.Column(db.Integer, nullable=False, default=1) 37 | status = db.Column(db.Integer, nullable=False, default=1) 38 | 39 | @property 40 | def is_enabled(self): 41 | if self.status == 1: 42 | return True 43 | return False 44 | 45 | def check_password(self, password): 46 | if self.password_hash == xor_encrypt(password, current_app.config['SECRET_KEY']): 47 | return True 48 | return False 49 | 50 | @staticmethod 51 | def get_by_username(username): 52 | return User.query.filter_by(username=username).first() 53 | 54 | def __repr__(self): 55 | return "".format(self.username) 56 | -------------------------------------------------------------------------------- /pwnedsso/routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/practisec/pwnedhub/e1111ae4bfa43f0cd6a3fe473d114e8c0a9e2e8a/pwnedsso/routes/__init__.py -------------------------------------------------------------------------------- /pwnedsso/routes/sso.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, redirect 2 | from pwnedsso.models import User 3 | from pwnedsso.utils import encode_jwt 4 | from urllib.parse import urlencode 5 | 6 | blp = Blueprint('sso', __name__) 7 | 8 | # sso controllers 9 | 10 | @blp.route('/authenticate', methods=['POST']) 11 | def authenticate(): 12 | username = request.form.get('username') 13 | password = request.form.get('password') 14 | user = None 15 | if username and password: 16 | untrusted_user = User.get_by_username(username) 17 | if untrusted_user and untrusted_user.check_password(password): 18 | user = untrusted_user 19 | params = {} 20 | params['id_token'] = encode_jwt(user.username if user else None) 21 | if 'next' in request.args: 22 | params['next'] = request.args.get('next') 23 | redirect_url = '?'.join(['http://www.pwnedhub.com/sso/login', urlencode(params)]) 24 | return redirect(redirect_url) 25 | -------------------------------------------------------------------------------- /pwnedsso/utils.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | from datetime import datetime, timezone, timedelta 3 | from itertools import cycle 4 | import base64 5 | import jwt 6 | 7 | def get_current_utc_time(): 8 | return datetime.now(tz=timezone.utc) 9 | 10 | def get_local_from_utc(dtg): 11 | return dtg.replace(tzinfo=timezone.utc).astimezone(tz=None) 12 | 13 | def xor_encrypt(s, k): 14 | ciphertext = ''.join([ chr(ord(c)^ord(k)) for c,k in zip(s, cycle(k)) ]) 15 | return base64.b64encode(ciphertext.encode()).decode() 16 | 17 | def encode_jwt(user_id, claims={}, expire_delta={'days': 1, 'seconds': 0}): 18 | payload = { 19 | 'exp': get_current_utc_time() + timedelta(**expire_delta), 20 | 'iat': get_current_utc_time(), 21 | 'sub': user_id 22 | } 23 | for claim, value in claims.items(): 24 | payload[claim] = value 25 | return jwt.encode( 26 | payload, 27 | current_app.config['SECRET_KEY'], 28 | algorithm='HS256' 29 | ) 30 | -------------------------------------------------------------------------------- /pwnedsso/wsgi.py: -------------------------------------------------------------------------------- 1 | from pwnedsso import create_app 2 | 3 | app = create_app() 4 | if __name__ == '__main__': 5 | app.run() 6 | --------------------------------------------------------------------------------