".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 |
23 |
30 | {% endblock %}
31 |
--------------------------------------------------------------------------------
/pwnedadmin/templates/emails.html:
--------------------------------------------------------------------------------
1 | {% extends 'layout.html' %}
2 | {% block title %}PwnedAdmin | Inbox{% endblock %}
3 | {% block content %}
4 |
5 |
6 | Empty
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 |
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 |
30 | {% endblock %}
31 |
--------------------------------------------------------------------------------
/pwnedhub/templates/admin_users.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 | {% if users|length > 0 %}
4 |
5 |
6 |
7 | created display name username role status action
8 |
9 |
10 | {% for user in users %}
11 |
12 | {{ user.created_as_string }}
13 | {{ user.name }}
14 | {{ user.username }}
15 | {{ user.role_as_string }}
16 | {{ user.status_as_string }}
17 |
18 | {% if user.is_admin %}
19 |
20 | {% else %}
21 |
22 | {% endif %}
23 | {% if user.is_enabled %}
24 |
25 | {% else %}
26 |
27 | {% endif %}
28 |
29 |
30 | {% endfor %}
31 |
32 |
33 |
34 | {% endif %}
35 | {% endblock %}
36 |
--------------------------------------------------------------------------------
/pwnedhub/templates/artifacts.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
4 |
8 |
9 |
10 |
11 | file created action
12 |
13 |
14 | {% if artifacts|length > 0 %}
15 | {% for artifact in artifacts %}
16 |
17 | {{ artifact.filename }}
18 | {{ artifact.modified }}
19 |
20 |
24 |
28 |
29 |
30 | {% endfor %}
31 | {% else %}
32 |
33 | {% endif %}
34 |
35 |
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 | {{ key }}
76 | {{ value }}
77 |
78 | {% endfor %}
79 |
80 |
81 | Log Status
82 | {% for log in log_stats %}
83 |
84 |
85 | {% for key, value in log.items() %}
86 |
87 | {{ key }}
88 | {{ value }}
89 |
90 | {% endfor %}
91 |
92 |
93 | {% endfor %}
94 |
95 |
--------------------------------------------------------------------------------
/pwnedhub/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
4 |
5 |
A collaborative space to conduct hosted security assessments.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Scan.
13 |
14 |
15 |
16 |
Find.
17 |
18 |
19 |
20 |
Win.
21 |
22 |
23 |
24 |
25 | {% endblock %}
26 |
--------------------------------------------------------------------------------
/pwnedhub/templates/login.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
4 | {% if app_config('OIDC_ENABLE') %}
5 |
6 |
7 |
Login with your Google Account
8 |
9 |
No need to register or remember another pesky password! Just click the button below to register and/or sign-in and get started.
10 |
11 |
12 |
13 |
14 | {% else %}
15 | {% if app_config('SSO_ENABLE') %}
16 |
31 | {% endif %}
32 |
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 |
23 | {% endblock %}
24 |
--------------------------------------------------------------------------------
/pwnedhub/templates/mail_inbox.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
24 | {% endblock %}
25 |
--------------------------------------------------------------------------------
/pwnedhub/templates/mail_reply.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
4 |
5 |
{{ letter.sender.name }}
6 |
7 | Subject: *
8 |
9 | Content: *
10 |
11 |
12 |
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 |
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 | Edit
8 |
9 | View
10 |
11 |
12 |
13 | {{ notes }}
14 |
15 |
16 |
17 |
18 |
76 | {% endblock %}
77 |
--------------------------------------------------------------------------------
/pwnedhub/templates/profile.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
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 |
8 |
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/pwnedhub/templates/register.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
32 | {% endblock %}
33 |
--------------------------------------------------------------------------------
/pwnedhub/templates/reset_init.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
4 |
5 | Password trouble?
6 | Username:
7 |
8 |
9 |
10 |
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/pwnedhub/templates/reset_password.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
18 | {% endblock %}
19 |
--------------------------------------------------------------------------------
/pwnedhub/templates/reset_question.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
4 |
5 | Verify your identity.
6 | {{ question }}
7 |
8 |
9 |
10 |
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/pwnedhub/templates/tools.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
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 |
8 |
21 |
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 |
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 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
31 |
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 |
8 |
9 |
10 |
11 |
12 |
13 |
A collaborative space to conduct hosted security assessments.
14 |
15 |
16 |
27 |
28 |
29 |
Scan.
30 |
31 |
32 |
33 |
Find.
34 |
35 |
36 |
37 |
Win.
38 |
39 |
40 |
41 |
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 |
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 |
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 |
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 |
9 |
10 |
Welcome to PwnedHub !
11 |
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.
12 |
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.
13 |
So what are you waiting for? Signup today!
14 |
15 |
28 |
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 |
--------------------------------------------------------------------------------