├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── dependabot.yml └── workflows │ ├── changelog_reminder.yml │ ├── python-pypi-publish.yml │ ├── stylecheck.yml │ └── stylecheck_pr.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── docker-compose.yml ├── documentation ├── pastepwn-detail-architecture.png └── pastepwn_logo.png ├── examples ├── example.py ├── json_example.py └── plaintext_cc.py ├── pastepwn ├── __init__.py ├── actions │ ├── __init__.py │ ├── basicaction.py │ ├── databaseaction.py │ ├── discordaction.py │ ├── emailaction.py │ ├── genericaction.py │ ├── ircaction.py │ ├── logaction.py │ ├── mispaction.py │ ├── savefileaction.py │ ├── savejsonaction.py │ ├── syslogaction.py │ ├── telegramaction.py │ ├── tests │ │ ├── __init__.py │ │ ├── discordaction_test.py │ │ ├── savefileaction_test.py │ │ └── savejsonaction_test.py │ ├── twitteraction.py │ └── webhookaction.py ├── analyzers │ ├── __init__.py │ ├── adobekeyanalyzer.py │ ├── alwaystrueanalyzer.py │ ├── awsaccesskeyanalyzer.py │ ├── awssecretkeyanalyzer.py │ ├── awssessiontokenanalyzer.py │ ├── azuresubscriptionkeyanalyzer.py │ ├── base64analyzer.py │ ├── base64asciianalyzer.py │ ├── basicanalyzer.py │ ├── battlenetkeyanalyzer.py │ ├── bcrypthashanalyzer.py │ ├── creditcardanalyzer.py │ ├── databasedumpanalyzer.py │ ├── dbconnstringanalyzer.py │ ├── emailpasswordpairanalyzer.py │ ├── epickeyanalyzer.py │ ├── exactwordanalyzer.py │ ├── facebookaccesstokenanalyzer.py │ ├── genericanalyzer.py │ ├── googleapikeyanalyzer.py │ ├── googleoauthkeyanalyzer.py │ ├── hashanalyzer.py │ ├── ibananalyzer.py │ ├── ipv4addressanalyzer.py │ ├── logicalanalyzers │ │ ├── __init__.py │ │ ├── andanalyzer.py │ │ ├── logicalbaseanalyzer.py │ │ ├── oranalyzer.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── andanalyzer_test.py │ │ │ ├── logicalbaseanalyzer_test.py │ │ │ └── oranalyzer_test.py │ ├── mailanalyzer.py │ ├── mailchimpapikeyanalyzer.py │ ├── md5hashanalyzer.py │ ├── megalinkanalyzer.py │ ├── microsoftkeyanalyzer.py │ ├── originkeyanalyzer.py │ ├── pastebinurlanalyzer.py │ ├── pastetitleanalyzer.py │ ├── phonenumberanalyzer.py │ ├── privatekeyanalyzer.py │ ├── regexanalyzer.py │ ├── shahashanalyzer.py │ ├── slacktokenanalyzer.py │ ├── slackwebhookanalyzer.py │ ├── steamkeyanalyzer.py │ ├── stripeapikeyanalyzer.py │ ├── tests │ │ ├── __init__.py │ │ ├── adobekeyanalyzer_test.py │ │ ├── alwaystrueanalyzer_test.py │ │ ├── awsaccesskeyanalyzer_test.py │ │ ├── awssecretkeyanalyzer_test.py │ │ ├── awssessiontokenanalyzer_test.py │ │ ├── azuresubscriptionkeyanalyzer_test.py │ │ ├── base64analyzer_test.py │ │ ├── base64asciianalyzer_test.py │ │ ├── basicanalyzer_test.py │ │ ├── battlenetkeyanalyzer_test.py │ │ ├── bcrypthashanalyzer_test.py │ │ ├── creditcardanalyzer_test.py │ │ ├── databasedumpanalyzer_test.py │ │ ├── dbconnstringanalyzer_test.py │ │ ├── emailpasswordpairanalyzer_test.py │ │ ├── epickeyanalyzer_test.py │ │ ├── exactwordanalyzer_test.py │ │ ├── facebookaccesstokenanalyzer_test.py │ │ ├── genericanalyzer_test.py │ │ ├── googleapikeyanalyzer_test.py │ │ ├── googleoauthkeyanalyzer_test.py │ │ ├── hashanalyzer_test.py │ │ ├── ibananalyzer_test.py │ │ ├── ipv4addressanalyzer_test.py │ │ ├── mailanalyzer_test.py │ │ ├── mailchimpapikeyanalyzer_test.py │ │ ├── md5hashanalyzer_test.py │ │ ├── megalinkanalyzer_test.py │ │ ├── mergedanalyzer_test.py │ │ ├── microsoftkeyanalyzer_test.py │ │ ├── originkeyanalyzer_test.py │ │ ├── pastebinurlanalyzer_test.py │ │ ├── pastetitleanalyzer_test.py │ │ ├── phonenumberanalyzer_test.py │ │ ├── privatekeyanalyzer_test.py │ │ ├── regexanalyzer_test.py │ │ ├── shahashanalyzer_test.py │ │ ├── slacktokenanalyzer_test.py │ │ ├── slackwebhookanalyzer_test.py │ │ ├── steamkeyanalyzer_test.py │ │ ├── stripeapikeyanalyzer_test.py │ │ ├── uplaykeyanalyzer_test.py │ │ └── wordanalyzer_test.py │ ├── uplaykeyanalyzer.py │ ├── urlanalyzer.py │ └── wordanalyzer.py ├── core │ ├── __init__.py │ ├── actionhandler.py │ ├── paste.py │ ├── pastedispatcher.py │ ├── pastepwn.py │ ├── scrapinghandler.py │ └── tests │ │ ├── __init__.py │ │ └── paste_test.py ├── database │ ├── __init__.py │ ├── abstractdb.py │ ├── mongodb.py │ ├── mysqldb.py │ ├── sqlitedb.py │ └── tests │ │ ├── __init__.py │ │ ├── mongodb_test.py │ │ └── sqlite_test.py ├── errors │ ├── __init__.py │ ├── errors.py │ └── tests │ │ ├── __init__.py │ │ └── errors_test.py ├── scraping │ ├── __init__.py │ ├── basicscraper.py │ └── pastebin │ │ ├── __init__.py │ │ ├── exceptions │ │ ├── __init__.py │ │ ├── ipnotregisterederror.py │ │ ├── pastedeletedexception.py │ │ ├── pasteemptyexception.py │ │ └── pastenotreadyexception.py │ │ ├── pastebinscraper.py │ │ └── tests │ │ ├── __init__.py │ │ └── pastebinscraper_test.py ├── util │ ├── __init__.py │ ├── dictwrapper.py │ ├── listify.py │ ├── network.py │ ├── request.py │ ├── templatingengine.py │ ├── tests │ │ ├── __init__.py │ │ ├── dictwrapper_test.py │ │ ├── listify_test.py │ │ ├── network_test.py │ │ └── templatingengine_test.py │ └── threadingutils.py └── version.py ├── pyproject.toml ├── requirements.txt ├── requirements_minimum.txt └── start.sh /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Description** 8 | A clear and concise description of what the bug is. 9 | 10 | **Steps To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **System** 24 | - OS: 25 | - Python Version: 26 | - PastePwn Version 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **User Story / Use Case** 8 | Is your feature request related to a problem? Please describe. 9 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 10 | 11 | **Possible solution** 12 | A clear and concise description of what you want to happen. 13 | 14 | **Alternative solution** 15 | A clear and concise description of any alternative solutions or features you've considered. 16 | 17 | **Additional context** 18 | Add any other context or screenshots about the feature request here. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask questions about this project or about how to use it 4 | 5 | --- 6 | 7 | > Please provide a clear description of your question and include all (technical) details, in case they are relevant for the question. 8 | > Please remove this line before creating the issue! 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | target-branch: dev 10 | ignore: 11 | - dependency-name: mysql-connector-python 12 | versions: 13 | - 8.0.23 14 | -------------------------------------------------------------------------------- /.github/workflows/changelog_reminder.yml: -------------------------------------------------------------------------------- 1 | name: Changelog Reminder 2 | on: pull_request 3 | permissions: 4 | pull-requests: write 5 | contents: read 6 | 7 | jobs: 8 | remind: 9 | name: Changelog Reminder 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Changelog Reminder 15 | uses: mskelton/changelog-reminder-action@v3 16 | with: 17 | message: "@${{ github.actor }} We couldn't find any modification to the CHANGELOG.md file. If your changes are not suitable for the changelog, that's fine. Otherwise please add them to the changelog!" 18 | -------------------------------------------------------------------------------- /.github/workflows/python-pypi-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [ created ] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.10' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install build wheel twine pytest 25 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 26 | - name: Lint with ruff 27 | uses: astral-sh/ruff-action@v2 28 | with: 29 | src: "./pastepwn" 30 | - name: Test with pytest 31 | run: | 32 | pytest 33 | - name: Build and publish 34 | env: 35 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 36 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 37 | run: | 38 | python -m build 39 | twine upload dist/* 40 | -------------------------------------------------------------------------------- /.github/workflows/stylecheck.yml: -------------------------------------------------------------------------------- 1 | name: Push Stylecheck 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Check out source code 12 | uses: actions/checkout@v3 13 | - name: Set up Python 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: '3.10' 17 | - name: Lint with ruff 18 | uses: astral-sh/ruff-action@v2 19 | with: 20 | src: "./pastepwn" 21 | -------------------------------------------------------------------------------- /.github/workflows/stylecheck_pr.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Stylecheck 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Check out source code 12 | uses: actions/checkout@v3 13 | - name: Set up Python 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: '3.10' 17 | - name: Lint with ruff 18 | uses: astral-sh/ruff-action@v2 19 | with: 20 | src: "./pastepwn" 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | LABEL maintainer="d-Rickyy-b " 4 | LABEL site="https://github.com/d-Rickyy-b/pastepwn" 5 | 6 | # Create base & log directory 7 | RUN mkdir -p /pastepwn/logs /pastepwn/src 8 | WORKDIR /pastepwn 9 | 10 | # Copy the source code to the container 11 | COPY . /pastepwn/src 12 | COPY ./start.sh /pastepwn 13 | 14 | # Installation of the pastepwn package 15 | RUN python3 /pastepwn/src/setup.py install 16 | # && pip3 install --no-cache pastepwn 17 | 18 | # Start the main file when the container is started 19 | ENTRYPOINT ["/bin/sh", "start.sh"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 d-Rickyy-b 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE requirements.txt requirements_minimum.txt 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | pastepwn: 4 | build: 5 | context: . 6 | env_file: 7 | - .env 8 | environment: 9 | - HTTP_PROXY=${HTTP_PROXY} 10 | - HTTPS_PROXY=${HTTPS_PROXY} 11 | - NO_PROXY=${NO_PROXY} 12 | - PATH_TO_START_PY=${PATH_TO_START_PY} 13 | volumes: 14 | - "${PATH_TO_START_PY}:/pastepwn/start.py:ro" 15 | depends_on: 16 | - db 17 | links: 18 | - db 19 | db: 20 | image: mysql 21 | env_file: 22 | - .env 23 | ports: 24 | - "3306:3306" 25 | 26 | -------------------------------------------------------------------------------- /documentation/pastepwn-detail-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-Rickyy-b/pastepwn/6832d70cb1eb9e141e49a776b69cf2844906025b/documentation/pastepwn-detail-architecture.png -------------------------------------------------------------------------------- /documentation/pastepwn_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-Rickyy-b/pastepwn/6832d70cb1eb9e141e49a776b69cf2844906025b/documentation/pastepwn_logo.png -------------------------------------------------------------------------------- /examples/example.py: -------------------------------------------------------------------------------- 1 | 2 | import logging.handlers 3 | import pathlib 4 | 5 | from pastepwn import PastePwn 6 | from pastepwn.actions import TelegramAction 7 | from pastepwn.analyzers import MailAnalyzer, WordAnalyzer 8 | from pastepwn.database import MongoDB 9 | 10 | # Setting up the logging to a file in ./logs/ 11 | logdir_path = pathlib.Path(__file__).parent.joinpath("logs").absolute() 12 | logfile_path = logdir_path.joinpath("pastepwn.log") 13 | 14 | # Creates ./logs/ if it doesn't exist 15 | if not logdir_path.exists(): 16 | logdir_path.mkdir() 17 | 18 | logfile_handler = logging.handlers.WatchedFileHandler(logfile_path, "a", "utf-8") 19 | 20 | logger = logging.getLogger(__name__) 21 | logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO, handlers=[logfile_handler, logging.StreamHandler()]) 22 | 23 | # Framework code 24 | database = MongoDB(ip="192.168.240.128") 25 | 26 | pastepwn = PastePwn(database) 27 | 28 | # Generic action to send Telegram messages on new matched pastes 29 | telegram_action = TelegramAction(token="token", receiver="-1001348376474") 30 | 31 | mail_analyzer = MailAnalyzer(telegram_action) 32 | premium_analyzer = WordAnalyzer(telegram_action, "premium") 33 | 34 | pastepwn.add_analyzer(mail_analyzer) 35 | pastepwn.add_analyzer(premium_analyzer) 36 | 37 | pastepwn.start() 38 | -------------------------------------------------------------------------------- /examples/json_example.py: -------------------------------------------------------------------------------- 1 | import logging.handlers 2 | import pathlib 3 | 4 | from pastepwn import PastePwn 5 | from pastepwn.actions import SaveJSONAction 6 | from pastepwn.analyzers import GoogleApiKeyAnalyzer 7 | 8 | # Setting up the logging to a file in ./logs/ 9 | logdir_path = pathlib.Path(__file__).parent.joinpath("logs").absolute() 10 | logfile_path = logdir_path.joinpath("pastepwn.log") 11 | 12 | # Creates ./logs/ if it doesn't exist 13 | if not logdir_path.exists(): 14 | logdir_path.mkdir() 15 | 16 | logfile_handler = logging.handlers.WatchedFileHandler(logfile_path, "a", "utf-8") 17 | 18 | logger = logging.getLogger(__name__) 19 | logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO, handlers=[logfile_handler, logging.StreamHandler()]) 20 | 21 | # A database can be provided to store pastes. This example does not implement a database. 22 | pastepwn = PastePwn() 23 | 24 | # PastePWN has 3 components: Scraper, Analyzer, Action. 25 | # This example implements the default Scraper, Google API Key Analyzer, and JSON File Save Action. 26 | # Generic Action to save Pastes to JSON File. 27 | json_path = "./json_pastes" 28 | save_json_action = SaveJSONAction(json_path) 29 | 30 | # Google API Key Analyzer with save JSON action. 31 | google_api_key_analyzer = GoogleApiKeyAnalyzer(save_json_action) 32 | 33 | # Add the analyzer to the pastepwn instance. 34 | pastepwn.add_analyzer(google_api_key_analyzer) 35 | 36 | # Start scraping/saving. 37 | pastepwn.start() 38 | -------------------------------------------------------------------------------- /examples/plaintext_cc.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging.handlers 4 | import typing 5 | from pathlib import Path 6 | 7 | from pastepwn import PastePwn 8 | from pastepwn.actions import BasicAction 9 | from pastepwn.analyzers import CreditCardAnalyzer 10 | 11 | if typing.TYPE_CHECKING: 12 | import re 13 | 14 | from pastepwn.core import Paste 15 | 16 | 17 | # Setup logging 18 | logdir_path = Path(__file__).parent.joinpath("logs").absolute() 19 | logfile_path = logdir_path.joinpath("pastepwn.log") 20 | 21 | if not logdir_path.exists(): 22 | logdir_path.mkdir() 23 | 24 | logfile_handler = logging.handlers.WatchedFileHandler(logfile_path, "a", "utf-8") 25 | 26 | logger = logging.getLogger(__name__) 27 | logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO, handlers=[logfile_handler, logging.StreamHandler()]) 28 | 29 | 30 | # Subclass BasicAction to append matches to file 31 | class AppendCCMatchFileAction(BasicAction): 32 | def __init__(self, path: Path): 33 | super().__init__() 34 | if not path.exists(): 35 | msg = f"{path.resolve()} does not exist." 36 | raise FileNotFoundError(msg) 37 | self.path = path 38 | 39 | def perform(self, paste: Paste, analyzer_name: str | None = None, matches: list[re.Match] | None = None): 40 | if matches: 41 | # Append matches to file joined by newline 42 | with open(self.path, "+a", errors="ignore") as result_fp: 43 | result_fp.write("\n".join(matches) + "\n") 44 | 45 | 46 | # Create PastePwn object 47 | pastepwn = PastePwn() 48 | 49 | # Check if result file exists and create if it doesnt 50 | cc_path = Path("./credit_cards.txt") 51 | if not cc_path.exists(): 52 | cc_path.touch() 53 | 54 | # Create analyzer object with action 55 | cc_analyzer = CreditCardAnalyzer(AppendCCMatchFileAction(cc_path)) 56 | 57 | # Add analyzer to PastePwn 58 | pastepwn.add_analyzer(cc_analyzer) 59 | 60 | # Start PastePwn 61 | pastepwn.start() 62 | -------------------------------------------------------------------------------- /pastepwn/__init__.py: -------------------------------------------------------------------------------- 1 | # Do not mess with the order of the imports 2 | # Otherwise there will be circular imports -> bad 3 | 4 | from .core.actionhandler import ActionHandler 5 | from .core.paste import Paste 6 | from .core.pastedispatcher import PasteDispatcher 7 | from .core.pastepwn import PastePwn 8 | from .core.scrapinghandler import ScrapingHandler 9 | 10 | __author__ = "d-Rickyy-b (pastepwn@rico-j.de)" 11 | 12 | __all__ = ["ActionHandler", "Paste", "PasteDispatcher", "PastePwn", "ScrapingHandler"] 13 | -------------------------------------------------------------------------------- /pastepwn/actions/__init__.py: -------------------------------------------------------------------------------- 1 | from .basicaction import BasicAction 2 | from .databaseaction import DatabaseAction 3 | from .discordaction import DiscordAction 4 | from .emailaction import EmailAction 5 | from .genericaction import GenericAction 6 | from .ircaction import IrcAction 7 | from .logaction import LogAction 8 | from .mispaction import MISPAction 9 | from .savefileaction import SaveFileAction 10 | from .savejsonaction import SaveJSONAction 11 | from .telegramaction import TelegramAction 12 | from .twitteraction import TwitterAction 13 | 14 | __all__ = [ 15 | "BasicAction", 16 | "DatabaseAction", 17 | "DiscordAction", 18 | "EmailAction", 19 | "GenericAction", 20 | "IrcAction", 21 | "LogAction", 22 | "MISPAction", 23 | "SaveFileAction", 24 | "SaveJSONAction", 25 | "TelegramAction", 26 | "TwitterAction", 27 | ] 28 | -------------------------------------------------------------------------------- /pastepwn/actions/basicaction.py: -------------------------------------------------------------------------------- 1 | class BasicAction: 2 | """Base class for actions which can be performed on pastes""" 3 | 4 | name = "BasicAction" 5 | 6 | def __init__(self): 7 | pass 8 | 9 | def perform(self, paste, analyzer_name=None, matches=None): 10 | """Perform the action on the passed paste""" 11 | raise NotImplementedError 12 | -------------------------------------------------------------------------------- /pastepwn/actions/databaseaction.py: -------------------------------------------------------------------------------- 1 | from .basicaction import BasicAction 2 | 3 | 4 | class DatabaseAction(BasicAction): 5 | """Action to save a paste to a database""" 6 | 7 | name = "DatabaseAction" 8 | 9 | def __init__(self, database): 10 | super().__init__() 11 | self.database = database 12 | 13 | def perform(self, paste, analyzer_name=None, matches=None): 14 | """Store an incoming paste in the database""" 15 | self.database.store(paste) 16 | -------------------------------------------------------------------------------- /pastepwn/actions/emailaction.py: -------------------------------------------------------------------------------- 1 | import re 2 | import smtplib 3 | from email.mime.multipart import MIMEMultipart 4 | from email.mime.text import MIMEText 5 | 6 | from pastepwn.util import TemplatingEngine 7 | 8 | from .basicaction import BasicAction 9 | 10 | 11 | class EmailAction(BasicAction): 12 | """This action sends out an e-mail to the receiver containing the paste, when executed""" 13 | 14 | name = "EmailAction" 15 | 16 | def __init__(self, username, password, receiver, hostname, port=465, template=None): 17 | super().__init__() 18 | mail_regex = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)" 19 | if username is None or not re.match(mail_regex, username): 20 | raise ValueError("Invalid username !") 21 | 22 | self.username = username 23 | 24 | if receiver is None or not re.match(mail_regex, receiver): 25 | raise ValueError("Invalid reciever address !") 26 | 27 | self.receiver = receiver 28 | self.password = password 29 | self.hostname = hostname 30 | self.port = port 31 | self.template = template 32 | 33 | def perform(self, paste, analyzer_name=None, matches=None): 34 | """Sends an email to the specified receiver with the paste's content. 35 | :param paste: The paste passed by the ActionHandler 36 | :param analyzer_name: The name of the analyzer which matched the paste 37 | :param matches: A list of matches, on which the analyzer matched on 38 | :return: None 39 | """ 40 | text = TemplatingEngine.fill_template(paste, analyzer_name, template_string=self.template, matches=matches) 41 | 42 | email = MIMEMultipart() 43 | email["From"] = self.username 44 | email["To"] = self.receiver 45 | email["Subject"] = f"Paste matched by pastepwn via analyzer '{analyzer_name}'" 46 | email.attach(MIMEText(text, "plain")) 47 | 48 | # TODO there should be a way to use starttls - check https://realpython.com/python-send-email/ 49 | with smtplib.SMTP_SSL(self.hostname, self.port) as smtp: 50 | smtp.login(self.username, self.password) 51 | text = email.as_string() 52 | smtp.sendmail(self.username, self.receiver, text) 53 | -------------------------------------------------------------------------------- /pastepwn/actions/genericaction.py: -------------------------------------------------------------------------------- 1 | from .basicaction import BasicAction 2 | 3 | 4 | class GenericAction(BasicAction): 5 | """Action to execute a custom function""" 6 | 7 | name = "GenericAction" 8 | 9 | def __init__(self, func): 10 | super().__init__() 11 | self.func = func 12 | 13 | def perform(self, paste, analyzer_name=None, matches=None): 14 | self.func(paste) 15 | -------------------------------------------------------------------------------- /pastepwn/actions/logaction.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from .basicaction import BasicAction 4 | 5 | 6 | class LogAction(BasicAction): 7 | """Action to log a paste to console""" 8 | 9 | name = "LogAction" 10 | 11 | def __init__(self): 12 | super().__init__() 13 | self.logger = logging.getLogger(__name__) 14 | 15 | def perform(self, paste, analyzer_name=None, matches=None): 16 | self.logger.debug(f"New Paste matched: {paste}") 17 | -------------------------------------------------------------------------------- /pastepwn/actions/savefileaction.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | from pastepwn.util import TemplatingEngine 4 | 5 | from .basicaction import BasicAction 6 | 7 | 8 | class SaveFileAction(BasicAction): 9 | """Action to save each paste as a file named '.txt'""" 10 | 11 | name = "SaveFileAction" 12 | 13 | def __init__(self, path, file_ending=".txt", template=None): 14 | """ 15 | Action to save each paste as a file named '.txt' 16 | If you want to store metadata within the file, use template strings 17 | > https://github.com/d-Rickyy-b/pastepwn/wiki/Templating-in-actions 18 | :param path: The directory in which the file(s) should be stored 19 | :param template: A template string describing how the paste variables should be filled in 20 | """ 21 | super().__init__() 22 | self.path = pathlib.Path(path) 23 | self.file_ending = file_ending 24 | self.template = template or "${body}" 25 | 26 | @staticmethod 27 | def _remove_prefix(input_string, prefix): 28 | """Remove a prefix from a certain string (e.g. remove '.' as prefix from '.txt')""" 29 | if input_string.startswith(prefix): 30 | return input_string[len(prefix) :] 31 | return input_string 32 | 33 | def get_file_content(self, paste, analyzer_name, matches): 34 | """Returns the content to be written to the file""" 35 | return TemplatingEngine.fill_template(paste, analyzer_name, template_string=self.template, matches=matches) 36 | 37 | def perform(self, paste, analyzer_name=None, matches=None): 38 | """ 39 | Stores the paste as a file 40 | :param paste: The paste passed by the ActionHandler 41 | :param analyzer_name: The name of the analyzer which matched the paste 42 | :param matches: List of matches returned by the analyzer 43 | :return: None 44 | """ 45 | if not self.path.exists(): 46 | self.path.mkdir(parents=True, exist_ok=True) 47 | 48 | self.file_ending = self._remove_prefix(self.file_ending, ".") 49 | 50 | if self.file_ending == "": 51 | file_name = str(paste.key) 52 | else: 53 | file_name = f"{paste.key}.{self.file_ending}" 54 | 55 | file_path = self.path / file_name 56 | content = self.get_file_content(paste, analyzer_name, matches) 57 | with open(file_path, "w", encoding="utf-8") as file: 58 | file.write(content) 59 | -------------------------------------------------------------------------------- /pastepwn/actions/savejsonaction.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from .savefileaction import SaveFileAction 4 | 5 | 6 | class SaveJSONAction(SaveFileAction): 7 | """Action to save a json formatted file to the disk""" 8 | 9 | name = "SaveJSONAction" 10 | 11 | def __init__(self, path): 12 | super().__init__(path, file_ending=".json") 13 | 14 | def get_file_content(self, paste, analyzer_name, matches): 15 | """Returns the content to be written to the file""" 16 | return json.dumps(paste.to_dict()) 17 | -------------------------------------------------------------------------------- /pastepwn/actions/syslogaction.py: -------------------------------------------------------------------------------- 1 | import logging.handlers 2 | 3 | from pastepwn.util import TemplatingEngine 4 | 5 | from .basicaction import BasicAction 6 | 7 | 8 | class SyslogAction(BasicAction): 9 | """Action to log a paste to the syslog""" 10 | 11 | name = "SyslogAction" 12 | 13 | def __init__(self, syslog_address="/dev/log", template=None): 14 | """ 15 | This sets up a syslogger, which defaults to /dev/log. 16 | That means that it will work on most linux systems, 17 | but will not work on for example OSX. 18 | 19 | For OSX, the correct syslog address would be 20 | /var/run/syslog 21 | """ 22 | super().__init__() 23 | self.template = template 24 | self.logger = logging.getLogger("SyslogLogger") 25 | self.logger.setLevel(logging.DEBUG) 26 | 27 | handler = logging.handlers.SysLogHandler(address=syslog_address) 28 | self.logger.addHandler(handler) 29 | 30 | def perform(self, paste, analyzer_name=None, matches=None): 31 | """ 32 | Logs a paste to the syslog 33 | :param paste: The paste passed by the ActionHandler 34 | :param analyzer_name: The name of the analyzer which matched the paste 35 | :param matches: List of matches returned by the analyzer 36 | :return: None 37 | """ 38 | text = TemplatingEngine.fill_template(paste, analyzer_name, template_string=self.template, matches=matches) 39 | self.logger.debug(text) 40 | -------------------------------------------------------------------------------- /pastepwn/actions/telegramaction.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | 4 | from pastepwn.util import Request, TemplatingEngine 5 | 6 | from .basicaction import BasicAction 7 | 8 | 9 | class TelegramAction(BasicAction): 10 | """Action to send a Telegram message to a certain user or group/channel""" 11 | 12 | name = "TelegramAction" 13 | 14 | def __init__(self, token, receiver, template=None): 15 | """Action to send a Telegram message to a certain user or group/channel. 16 | :param token: The Telegram API token for your bot obtained by @BotFather 17 | :param receiver: The userID/groupID or channelID of the receiving entity 18 | :param template: A template string describing how the paste variables should be filled in 19 | """ 20 | super().__init__() 21 | self.logger = logging.getLogger(__name__) 22 | 23 | if not re.match(r"[0-9]+:[a-zA-Z0-9\-_]+", token) or token is None: 24 | raise ValueError("Bot token not correct or None!") 25 | 26 | self.token = token 27 | self.receiver = receiver 28 | self.template = template 29 | 30 | def perform(self, paste, analyzer_name=None, matches=None): 31 | """Send a message via a Telegram bot to a specified user, without checking for errors""" 32 | r = Request() 33 | text = TemplatingEngine.fill_template(paste, analyzer_name, template_string=self.template, matches=matches) 34 | 35 | api_url = f"https://api.telegram.org/bot{self.token}/sendMessage?chat_id={self.receiver}&text={text}" 36 | r.get(api_url) 37 | -------------------------------------------------------------------------------- /pastepwn/actions/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-Rickyy-b/pastepwn/6832d70cb1eb9e141e49a776b69cf2844906025b/pastepwn/actions/tests/__init__.py -------------------------------------------------------------------------------- /pastepwn/actions/tests/savejsonaction_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock, patch 3 | 4 | from pastepwn.actions import SaveJSONAction 5 | 6 | 7 | class TestSaveJSONAction(unittest.TestCase): 8 | def setUp(self): 9 | """Sets up the test case""" 10 | self.action = SaveJSONAction(path="") 11 | self.paste = Mock() 12 | self.path_mock = Mock() 13 | self.path_mock.__truediv__ = Mock(side_effect=self.side_effect) 14 | self.path_mock.exists = Mock(return_value=True) 15 | 16 | @staticmethod 17 | def side_effect(path): 18 | return path 19 | 20 | def test_get_file_content(self): 21 | """Check if the content of the file is returned correctly""" 22 | self.paste.to_dict = Mock(return_value={"test": "content", "another": "item"}) 23 | content_string = """{"test": "content", "another": "item"}""" 24 | content = self.action.get_file_content(self.paste, "", []) 25 | self.assertEqual(content_string, content) 26 | 27 | @patch("builtins.open") 28 | @patch("pastepwn.actions.savejsonaction.json") 29 | def test_file_ending(self, json_mock, open_mock): 30 | """Check that the file ending is actually json, not txt""" 31 | json_mock.dumps = Mock(return_value="json content!") 32 | 33 | self.action.path = self.path_mock 34 | 35 | self.paste.key = Mock() 36 | self.paste.key.__repr__ = Mock(return_value="123456") 37 | self.action.perform(self.paste) 38 | 39 | open_mock.assert_called_with("123456.json", "w", encoding="utf-8") 40 | open_mock().__enter__().write.assert_called_with("json content!") 41 | 42 | 43 | if __name__ == "__main__": 44 | unittest.main() 45 | -------------------------------------------------------------------------------- /pastepwn/actions/twitteraction.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import twitter 4 | 5 | from pastepwn.util import TemplatingEngine 6 | 7 | from .basicaction import BasicAction 8 | 9 | 10 | class TwitterAction(BasicAction): 11 | """Action to tweet a message to a given account""" 12 | 13 | name = "TwitterAction" 14 | 15 | def __init__( 16 | self, 17 | consumer_key=None, 18 | consumer_secret=None, 19 | access_token_key=None, 20 | access_token_secret=None, 21 | template=None, 22 | ): 23 | super().__init__() 24 | 25 | self.logger = logging.getLogger(__name__) 26 | 27 | self.twitter_api = twitter.Api( 28 | consumer_key=consumer_key, 29 | consumer_secret=consumer_secret, 30 | access_token_key=access_token_key, 31 | access_token_secret=access_token_secret, 32 | ) 33 | self.template = template 34 | 35 | def perform(self, paste, analyzer_name=None, matches=None): 36 | """Tweet a message""" 37 | text = TemplatingEngine.fill_template(paste, analyzer_name, template_string=self.template, matches=matches) 38 | self.twitter_api.PostUpdate(text) 39 | -------------------------------------------------------------------------------- /pastepwn/actions/webhookaction.py: -------------------------------------------------------------------------------- 1 | from pastepwn.util import Request 2 | 3 | from .basicaction import BasicAction 4 | 5 | 6 | class WebhookAction(BasicAction): 7 | """Action to perform a Webhook on a matched paste""" 8 | 9 | name = "WebhookAction" 10 | 11 | def __init__(self, url, post_data=True): 12 | """ 13 | Init method for the WebhookAction 14 | :param url: string, URL to POST against 15 | :param post_data: boolean, to decide wheather a paste should be sent in the body 16 | """ 17 | super().__init__() 18 | self.url = url 19 | self.post_data = post_data 20 | 21 | def perform(self, paste, analyzer_name=None, matches=None): 22 | """ 23 | Trigger the webhook 24 | :param paste: The paste passed by the ActionHandler 25 | :param analyzer_name: The name of the analyzer which matched the paste 26 | :param matches: List of matches returned by the analyzer 27 | :return: None 28 | """ 29 | if self.post_data is None: 30 | paste_dict = None 31 | else: 32 | paste_dict = paste.to_dict() 33 | 34 | r = Request() 35 | r.post(url=self.url, data=paste_dict) 36 | -------------------------------------------------------------------------------- /pastepwn/analyzers/__init__.py: -------------------------------------------------------------------------------- 1 | from .adobekeyanalyzer import AdobeKeyAnalyzer 2 | from .alwaystrueanalyzer import AlwaysTrueAnalyzer 3 | from .awsaccesskeyanalyzer import AWSAccessKeyAnalyzer 4 | from .awssecretkeyanalyzer import AWSSecretKeyAnalyzer 5 | from .awssessiontokenanalyzer import AWSSessionTokenAnalyzer 6 | from .azuresubscriptionkeyanalyzer import AzureSubscriptionKeyAnalyzer 7 | from .base64analyzer import Base64Analyzer 8 | from .base64asciianalyzer import Base64AsciiAnalyzer 9 | from .basicanalyzer import BasicAnalyzer 10 | from .battlenetkeyanalyzer import BattleNetKeyAnalyzer 11 | from .bcrypthashanalyzer import BcryptHashAnalyzer 12 | from .creditcardanalyzer import CreditCardAnalyzer 13 | from .databasedumpanalyzer import DatabaseDumpAnalyzer 14 | from .dbconnstringanalyzer import DBConnAnalyzer 15 | from .emailpasswordpairanalyzer import EmailPasswordPairAnalyzer 16 | from .facebookaccesstokenanalyzer import FacebookAccessTokenAnalyzer 17 | from .genericanalyzer import GenericAnalyzer 18 | from .googleapikeyanalyzer import GoogleApiKeyAnalyzer 19 | from .googleoauthkeyanalyzer import GoogleOAuthKeyAnalyzer 20 | from .hashanalyzer import HashAnalyzer 21 | from .ibananalyzer import IBANAnalyzer 22 | from .logicalanalyzers import AndAnalyzer, LogicalBaseAnalyzer, OrAnalyzer 23 | from .mailanalyzer import MailAnalyzer 24 | from .mailchimpapikeyanalyzer import MailChimpApiKeyAnalyzer 25 | from .md5hashanalyzer import MD5HashAnalyzer 26 | from .megalinkanalyzer import MegaLinkAnalyzer 27 | from .microsoftkeyanalyzer import MicrosoftKeyAnalyzer 28 | from .originkeyanalyzer import OriginKeyAnalyzer 29 | from .pastebinurlanalyzer import PastebinURLAnalyzer 30 | from .phonenumberanalyzer import PhoneNumberAnalyzer 31 | from .privatekeyanalyzer import PrivateKeyAnalyzer 32 | from .regexanalyzer import RegexAnalyzer 33 | from .shahashanalyzer import SHAHashAnalyzer 34 | from .slacktokenanalyzer import SlackTokenAnalyzer 35 | from .slackwebhookanalyzer import SlackWebhookAnalyzer 36 | from .steamkeyanalyzer import SteamKeyAnalyzer 37 | from .stripeapikeyanalyzer import StripeApiKeyAnalyzer 38 | from .uplaykeyanalyzer import UplayKeyAnalyzer 39 | from .urlanalyzer import URLAnalyzer 40 | from .wordanalyzer import WordAnalyzer 41 | 42 | __all__ = [ 43 | "AWSAccessKeyAnalyzer", 44 | "AWSSecretKeyAnalyzer", 45 | "AWSSessionTokenAnalyzer", 46 | "AdobeKeyAnalyzer", 47 | "AlwaysTrueAnalyzer", 48 | "AndAnalyzer", 49 | "AzureSubscriptionKeyAnalyzer", 50 | "Base64Analyzer", 51 | "Base64AsciiAnalyzer", 52 | "BasicAnalyzer", 53 | "BattleNetKeyAnalyzer", 54 | "BcryptHashAnalyzer", 55 | "CreditCardAnalyzer", 56 | "DBConnAnalyzer", 57 | "DatabaseDumpAnalyzer", 58 | "EmailPasswordPairAnalyzer", 59 | "FacebookAccessTokenAnalyzer", 60 | "GenericAnalyzer", 61 | "GoogleApiKeyAnalyzer", 62 | "GoogleOAuthKeyAnalyzer", 63 | "HashAnalyzer", 64 | "IBANAnalyzer", 65 | "LogicalBaseAnalyzer", 66 | "MD5HashAnalyzer", 67 | "MailAnalyzer", 68 | "MailChimpApiKeyAnalyzer", 69 | "MegaLinkAnalyzer", 70 | "MicrosoftKeyAnalyzer", 71 | "OrAnalyzer", 72 | "OriginKeyAnalyzer", 73 | "PastebinURLAnalyzer", 74 | "PhoneNumberAnalyzer", 75 | "PrivateKeyAnalyzer", 76 | "RegexAnalyzer", 77 | "SHAHashAnalyzer", 78 | "SlackTokenAnalyzer", 79 | "SlackWebhookAnalyzer", 80 | "SteamKeyAnalyzer", 81 | "StripeApiKeyAnalyzer", 82 | "URLAnalyzer", 83 | "UplayKeyAnalyzer", 84 | "WordAnalyzer", 85 | ] 86 | -------------------------------------------------------------------------------- /pastepwn/analyzers/adobekeyanalyzer.py: -------------------------------------------------------------------------------- 1 | from .regexanalyzer import RegexAnalyzer 2 | 3 | 4 | class AdobeKeyAnalyzer(RegexAnalyzer): 5 | """Analyzer to match Adobe License Keys""" 6 | 7 | def __init__(self, action): 8 | """Analyzer to match Adobe License Keys. 9 | :param action: Single action or list of actions to be executed when a paste matches 10 | """ 11 | # Tested Regex against https://pastebin.com/fxWBGf8t 12 | regex = r"\b(?|=)\\\\s{0,50}(?:\\\"|'|`)?[A-Za-z0-9/+=]{16,}(?:\\\"|'|`)?)" 11 | super().__init__(actions, regex) 12 | -------------------------------------------------------------------------------- /pastepwn/analyzers/azuresubscriptionkeyanalyzer.py: -------------------------------------------------------------------------------- 1 | from .regexanalyzer import RegexAnalyzer 2 | 3 | 4 | class AzureSubscriptionKeyAnalyzer(RegexAnalyzer): 5 | """ 6 | Analyzer to match Azure subscription key via regex 7 | 8 | Keys are 32 character alphanumeric (lower case) 9 | """ 10 | 11 | name = "AzureSubscriptionKeyAnalyzer" 12 | 13 | def __init__(self, actions): 14 | # https://docs.microsoft.com/en-us/azure/api-management/api-management-subscriptions 15 | regex = r"\b[a-f0-9]{32}\b" 16 | super().__init__(actions, regex) 17 | -------------------------------------------------------------------------------- /pastepwn/analyzers/base64analyzer.py: -------------------------------------------------------------------------------- 1 | from .regexanalyzer import RegexAnalyzer 2 | 3 | 4 | class Base64Analyzer(RegexAnalyzer): 5 | """Analyzer to match base64 encoding via regex""" 6 | 7 | name = "Base64Analyzer" 8 | 9 | def __init__(self, actions, min_len=1): 10 | regex = r"(?= self.min_len] 17 | -------------------------------------------------------------------------------- /pastepwn/analyzers/base64asciianalyzer.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | from base64 import b64decode 3 | 4 | from .base64analyzer import Base64Analyzer 5 | 6 | 7 | class Base64AsciiAnalyzer(Base64Analyzer): 8 | """Analyzer to match base64 strings which decode to valid ASCII""" 9 | 10 | name = "Base64AsciiAnalyzer" 11 | 12 | def __init__(self, actions, min_len=1, decode=False): 13 | super().__init__(actions, min_len) 14 | self.decode = decode 15 | 16 | def verify(self, results): 17 | """Method to determine if found base64 decodes to valid ASCII""" 18 | # find valid base64 strings with the parent class 19 | validated_strings = super().verify(results) 20 | 21 | # go through each base64 string and attempt to decode 22 | base64_ascii_strings = [] 23 | 24 | for validated_string in validated_strings: 25 | # Check if the string is valid base64 26 | try: 27 | decoded_string = b64decode(validated_string) 28 | except binascii.Error: 29 | # The string is no valid base64 30 | continue 31 | 32 | # Check if the valid base64 decodes to plain ascii 33 | try: 34 | b64_ascii_string = decoded_string.decode("ascii") 35 | except UnicodeDecodeError: 36 | continue 37 | 38 | if self.decode: 39 | base64_ascii_strings.append(b64_ascii_string) 40 | else: 41 | base64_ascii_strings.append(validated_string) 42 | 43 | return base64_ascii_strings 44 | -------------------------------------------------------------------------------- /pastepwn/analyzers/battlenetkeyanalyzer.py: -------------------------------------------------------------------------------- 1 | from .regexanalyzer import RegexAnalyzer 2 | 3 | 4 | class BattleNetKeyAnalyzer(RegexAnalyzer): 5 | """Analyzer to match battle.net keys via regex.""" 6 | 7 | name = "BattleNetKeyAnalyzer" 8 | 9 | def __init__(self, actions): 10 | # battle.net activation page shows 4-4-4-4 format, but there are also codes in the 4-4-5-4-4 format which work 11 | regex = r"\b(?\;\"\:\|\,\~]+" 4 | 5 | 6 | class EmailPasswordPairAnalyzer(RegexAnalyzer): 7 | """Analyzer to match username:password pairs""" 8 | 9 | name = "EmailPasswordPairAnalyzer" 10 | 11 | def __init__(self, actions, min_amount=0): 12 | super().__init__(actions, _EMAIL_PASSWORD_REGEX) 13 | self.min_amount = min_amount 14 | 15 | def verify(self, results): 16 | """Method to perform additional checks to test if the matches are actually valid""" 17 | if len(results) >= self.min_amount: 18 | return results 19 | return False 20 | -------------------------------------------------------------------------------- /pastepwn/analyzers/epickeyanalyzer.py: -------------------------------------------------------------------------------- 1 | from .regexanalyzer import RegexAnalyzer 2 | 3 | 4 | class EpicKeyAnalyzer(RegexAnalyzer): 5 | """Analyzer to match Epic Licensing Keys""" 6 | 7 | def __init__(self, action): 8 | """ 9 | Analyzer to match Epic Licensing Keys 10 | :param action: Single action or list of actions to be executed when a paste matches 11 | """ 12 | # Applied general A-Z or 0-9 based on example provided 13 | # Regex can be adjusted if certain characters are not valid 14 | regex = r"\b(? 0..35 44 | return integer_iban % 97 == 1 45 | 46 | _iban_length_by_country = dict( 47 | AL=28, AD=24, AT=20, AZ=28, BE=16, BH=22, BA=20, BR=29, 48 | BG=22, CR=21, HR=21, CY=28, CZ=24, DK=18, DO=28, EE=20, 49 | FO=18, FI=18, FR=27, GE=22, DE=22, GI=23, GR=27, GL=18, 50 | GT=28, HU=28, IS=26, IE=22, IL=23, IT=27, KZ=20, KW=30, 51 | LV=21, LB=28, LI=21, LT=20, LU=20, MK=19, MT=31, MR=27, 52 | MU=30, MC=27, MD=24, ME=22, NL=18, NO=15, PK=24, PS=29, 53 | PL=28, PT=25, RO=24, SM=27, SA=24, RS=22, SK=24, SI=19, 54 | ES=24, SE=24, CH=21, TN=24, TR=26, AE=23, GB=22, VG=24 55 | ) 56 | -------------------------------------------------------------------------------- /pastepwn/analyzers/ipv4addressanalyzer.py: -------------------------------------------------------------------------------- 1 | from .regexanalyzer import RegexAnalyzer 2 | 3 | 4 | class IPv4AddressAnalyzer(RegexAnalyzer): 5 | """Analyzer to match on ip addresses via regex""" 6 | 7 | name = "IPv4AddressAnalyzer" 8 | 9 | def __init__(self, actions): 10 | regex = r"\b\d{1,3}(?:\.\d{1,3}){3}\b" 11 | super().__init__(actions, regex) 12 | 13 | def verify(self, results): 14 | """Verify results to get only real IP adresses""" 15 | verified_ips = [] 16 | for result in results: 17 | # Check each IP 18 | octet_max_value = 255 19 | for octet in result.split("."): 20 | if int(octet) > octet_max_value: 21 | break 22 | else: 23 | # IP is valid 24 | verified_ips.append(result) 25 | 26 | return verified_ips 27 | -------------------------------------------------------------------------------- /pastepwn/analyzers/logicalanalyzers/__init__.py: -------------------------------------------------------------------------------- 1 | from .andanalyzer import AndAnalyzer 2 | from .logicalbaseanalyzer import LogicalBaseAnalyzer 3 | from .oranalyzer import OrAnalyzer 4 | 5 | __all__ = ["AndAnalyzer", "LogicalBaseAnalyzer", "OrAnalyzer"] 6 | -------------------------------------------------------------------------------- /pastepwn/analyzers/logicalanalyzers/andanalyzer.py: -------------------------------------------------------------------------------- 1 | from .logicalbaseanalyzer import LogicalBaseAnalyzer 2 | 3 | 4 | class AndAnalyzer(LogicalBaseAnalyzer): 5 | """Meta analyzer which matches a paste if all of the passed analyzers match that paste""" 6 | 7 | name = "AndAnalyzer" 8 | 9 | def match(self, paste): 10 | """Returns True if all of the passed analyzers matched""" 11 | if not self.analyzers: 12 | return False 13 | 14 | return all(analyzer.match(paste) for analyzer in self.analyzers) 15 | -------------------------------------------------------------------------------- /pastepwn/analyzers/logicalanalyzers/logicalbaseanalyzer.py: -------------------------------------------------------------------------------- 1 | from pastepwn.analyzers.basicanalyzer import BasicAnalyzer 2 | from pastepwn.util import listify 3 | 4 | 5 | class LogicalBaseAnalyzer(BasicAnalyzer): 6 | """Meta analyzer used to combine analyzers via a logical operator""" 7 | 8 | name = "LogicalBaseAnalyzer" 9 | 10 | def __init__(self, actions, analyzers, merge_actions=False): 11 | """ 12 | Meta analyzer used to combine analyzers via a logical operator 13 | :param actions: A single action or a list of actions to be executed on every paste 14 | :param analyzers: A single analyzer or a list of analyzers used to match pastes 15 | """ 16 | super().__init__(actions) 17 | self.analyzers = listify(analyzers) 18 | 19 | if not self.analyzers: 20 | self.logger.warning(f"You have not specified any analyzers inside '{self.name}'") 21 | 22 | if merge_actions: 23 | self._merge_actions() 24 | 25 | def _merge_actions(self): 26 | """ 27 | Merges the actions of the passed analyzers into this meta analyzer 28 | If merged, the actions will be executed if this meta analyzer matches 29 | :return: None 30 | """ 31 | for analyzer in self.analyzers: 32 | for action in analyzer.actions: 33 | self.actions.append(action) 34 | 35 | def add_analyzer(self, analyzer): 36 | """ 37 | Add a new analyzer to the list of analyzers 38 | :param analyzer: A single analyzer used to match pastes 39 | :return: 40 | """ 41 | self.analyzers.append(analyzer) 42 | 43 | def match(self, paste): 44 | """Must be overridden by the subclasses""" 45 | raise NotImplementedError("match() function must be overridden in the subclasses!") 46 | -------------------------------------------------------------------------------- /pastepwn/analyzers/logicalanalyzers/oranalyzer.py: -------------------------------------------------------------------------------- 1 | from .logicalbaseanalyzer import LogicalBaseAnalyzer 2 | 3 | 4 | class OrAnalyzer(LogicalBaseAnalyzer): 5 | """Meta analyzer which matches a paste if any of the passed analyzers match that paste""" 6 | 7 | name = "OrAnalyzer" 8 | 9 | def match(self, paste): 10 | """Returns True if all of the passed analyzers matched""" 11 | return any(analyzer.match(paste) for analyzer in self.analyzers) 12 | -------------------------------------------------------------------------------- /pastepwn/analyzers/logicalanalyzers/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-Rickyy-b/pastepwn/6832d70cb1eb9e141e49a776b69cf2844906025b/pastepwn/analyzers/logicalanalyzers/tests/__init__.py -------------------------------------------------------------------------------- /pastepwn/analyzers/logicalanalyzers/tests/andanalyzer_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from pastepwn.actions.basicaction import BasicAction 5 | from pastepwn.analyzers import AndAnalyzer 6 | 7 | 8 | class TestAndAnalyzer(unittest.TestCase): 9 | def setUp(self): 10 | self.paste = mock.Mock() 11 | self.gt = mock.Mock() 12 | self.gt.match = mock.Mock(return_value=True) 13 | 14 | self.gf = mock.Mock() 15 | self.gf.match = mock.Mock(return_value=False) 16 | 17 | def test_match_positive(self): 18 | self.paste.body = "Test" 19 | analyzer = AndAnalyzer(None, [self.gt]) 20 | self.assertTrue(analyzer.match(self.paste)) 21 | 22 | analyzer = AndAnalyzer(None, [self.gt, self.gt, self.gt]) 23 | self.assertTrue(analyzer.match(self.paste)) 24 | 25 | # Check if the analyzer returns false if there is at least one false result 26 | analyzer = AndAnalyzer([], [self.gt, self.gf]) 27 | self.assertFalse(analyzer.match(self.paste)) 28 | 29 | analyzer = AndAnalyzer([], [self.gf, self.gf]) 30 | self.assertFalse(analyzer.match(self.paste)) 31 | 32 | analyzer = AndAnalyzer([], [self.gf, self.gf, self.gf, self.gt]) 33 | self.assertFalse(analyzer.match(self.paste)) 34 | 35 | def test_negative(self): 36 | self.paste.body = "" 37 | 38 | analyzer = AndAnalyzer([], None) 39 | self.assertFalse(analyzer.match(self.paste)) 40 | 41 | analyzer = AndAnalyzer([], []) 42 | self.assertFalse(analyzer.match(self.paste)) 43 | 44 | def test_actions_present(self): 45 | action = mock.MagicMock(spec=BasicAction) 46 | analyzer = AndAnalyzer(action, None) 47 | self.assertEqual([action], analyzer.actions) 48 | 49 | def test_analyzers_present(self): 50 | analyzer = AndAnalyzer(None, self.paste) 51 | self.assertEqual([self.paste], analyzer.analyzers) 52 | 53 | 54 | if __name__ == "__main__": 55 | unittest.main() 56 | -------------------------------------------------------------------------------- /pastepwn/analyzers/logicalanalyzers/tests/logicalbaseanalyzer_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from pastepwn.actions.basicaction import BasicAction 5 | from pastepwn.analyzers.logicalanalyzers import LogicalBaseAnalyzer 6 | 7 | 8 | class TestLogicalBaseAnalyzer(unittest.TestCase): 9 | def setUp(self): 10 | self.paste = mock.Mock() 11 | 12 | def test_exception(self): 13 | analyzer = LogicalBaseAnalyzer([], []) 14 | self.assertRaises(NotImplementedError, analyzer.match, mock.Mock()) 15 | 16 | def test_actions_present(self): 17 | action = mock.MagicMock(spec=BasicAction) 18 | analyzer = LogicalBaseAnalyzer(action, None) 19 | self.assertEqual([action], analyzer.actions) 20 | 21 | def test_analyzers_present(self): 22 | analyzer = LogicalBaseAnalyzer(None, self.paste) 23 | self.assertEqual([self.paste], analyzer.analyzers) 24 | 25 | def test_merge_actions(self): 26 | action1 = mock.Mock() 27 | action2 = mock.Mock() 28 | action3 = mock.Mock() 29 | 30 | analyzer1 = mock.Mock() 31 | analyzer1.actions = [action1, action2] 32 | analyzer2 = mock.Mock() 33 | analyzer2.actions = [action3] 34 | 35 | analyzer = LogicalBaseAnalyzer(analyzers=[analyzer1, analyzer2], actions=[], merge_actions=True) 36 | self.assertEqual(3, len(analyzer.actions), "Wrong amount of actions in LogicalBaseAnalyzer!") 37 | self.assertEqual([action1, action2, action3], analyzer.actions, "Actions do not match!") 38 | 39 | 40 | if __name__ == "__main__": 41 | unittest.main() 42 | -------------------------------------------------------------------------------- /pastepwn/analyzers/logicalanalyzers/tests/oranalyzer_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from pastepwn.actions.basicaction import BasicAction 5 | from pastepwn.analyzers.logicalanalyzers import OrAnalyzer 6 | 7 | 8 | class TestOrAnalyzer(unittest.TestCase): 9 | def setUp(self): 10 | self.paste = mock.Mock() 11 | self.gt = mock.Mock() 12 | self.gt.match = mock.Mock(return_value=True) 13 | 14 | self.gf = mock.Mock() 15 | self.gf.match = mock.Mock(return_value=False) 16 | 17 | def test_match_positive(self): 18 | self.paste.body = "Test" 19 | analyzer = OrAnalyzer(None, [self.gt]) 20 | self.assertTrue(analyzer.match(self.paste)) 21 | 22 | analyzer = OrAnalyzer(None, [self.gt, self.gt, self.gt]) 23 | self.assertTrue(analyzer.match(self.paste)) 24 | 25 | analyzer = OrAnalyzer([], [self.gf, self.gf]) 26 | self.assertFalse(analyzer.match(self.paste)) 27 | 28 | analyzer = OrAnalyzer([], [self.gf, self.gf, self.gf]) 29 | self.assertFalse(analyzer.match(self.paste)) 30 | 31 | analyzer = OrAnalyzer([], [self.gf, self.gf, self.gf, self.gt]) 32 | self.assertTrue(analyzer.match(self.paste)) 33 | 34 | def test_negative(self): 35 | self.paste.body = "" 36 | 37 | analyzer = OrAnalyzer([], None) 38 | self.assertFalse(analyzer.match(self.paste)) 39 | 40 | analyzer = OrAnalyzer([], []) 41 | self.assertFalse(analyzer.match(self.paste)) 42 | 43 | def test_actions_present(self): 44 | action = mock.MagicMock(spec=BasicAction) 45 | analyzer = OrAnalyzer(action, None) 46 | self.assertEqual([action], analyzer.actions) 47 | 48 | def test_analyzers_present(self): 49 | analyzer = OrAnalyzer(None, self.paste) 50 | self.assertEqual([self.paste], analyzer.analyzers) 51 | 52 | 53 | if __name__ == "__main__": 54 | unittest.main() 55 | -------------------------------------------------------------------------------- /pastepwn/analyzers/mailanalyzer.py: -------------------------------------------------------------------------------- 1 | from .regexanalyzer import RegexAnalyzer 2 | 3 | 4 | class MailAnalyzer(RegexAnalyzer): 5 | """Analyzer to match on email addresses via regex""" 6 | 7 | name = "MailAnalyzer" 8 | 9 | def __init__(self, actions): 10 | # Regex taken from http://emailregex.com/ 11 | regex = r"([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)" 12 | super().__init__(actions, regex) 13 | -------------------------------------------------------------------------------- /pastepwn/analyzers/mailchimpapikeyanalyzer.py: -------------------------------------------------------------------------------- 1 | from .regexanalyzer import RegexAnalyzer 2 | 3 | 4 | class MailChimpApiKeyAnalyzer(RegexAnalyzer): 5 | def __init__(self, actions): 6 | regex = r"[0-9a-f]{32}-us[0-9]{12}" 7 | super().__init__(actions, regex) 8 | -------------------------------------------------------------------------------- /pastepwn/analyzers/md5hashanalyzer.py: -------------------------------------------------------------------------------- 1 | from .regexanalyzer import RegexAnalyzer 2 | 3 | 4 | class MD5HashAnalyzer(RegexAnalyzer): 5 | """Analyzer to match MD5 password hashes via regex""" 6 | 7 | name = "MD5HashAnalyzer" 8 | 9 | def __init__(self, actions): 10 | regex = r"\b(?#Gn@sYn``5Tt6X`h'Hn_Y-"F>DkGr'\/:4kLF:D" 15 | self.paste.body = "5812c57a01b5efa30af09d4f9388e50d" 16 | self.assertTrue(self.analyzer.match(self.paste)) 17 | 18 | # md5 hash of 64 character string: sa.tpMvv#J{q)tZfz,W[5Hq*Yz%kN(,8j)p>'g["d^mSLHkD)gZLVxk/,}#aVxv* 19 | self.paste.body = "372f202bdf0f16dfa41c101fc6a41695" 20 | self.assertTrue(self.analyzer.match(self.paste)) 21 | 22 | # md5 hash of 32 character string: zw8hspDAZ(\w]K/v~yZaa_m$6awgF4rj 23 | self.paste.body = "2287c6975240f865a9945ec9bcc0eead" 24 | self.assertTrue(self.analyzer.match(self.paste)) 25 | 26 | # md5 hash of 16 character string: cY4>szK'>(?48tz= 27 | self.paste.body = "f2195cea00cc796676f77b9d19473f7a" 28 | self.assertTrue(self.analyzer.match(self.paste)) 29 | 30 | self.paste.body = "my super cool hash is 2287c6975240f865a9945ec9bcc0eead and here's some more text" 31 | self.assertTrue(self.analyzer.match(self.paste)) 32 | 33 | # Newline-separated valid hashes 34 | self.paste.body = "2287c6975240f865a9945ec9bcc0eead\n372f202bdf0f16dfa41c101fc6a41695" 35 | self.assertTrue(self.analyzer.match(self.paste)) 36 | 37 | def test_match_negative(self): 38 | """Test if negatives are not recognized""" 39 | self.paste.body = "" 40 | self.assertFalse(self.analyzer.match(self.paste)) 41 | 42 | self.paste.body = None 43 | self.assertFalse(self.analyzer.match(self.paste)) 44 | 45 | # Invalid character 'g' 46 | self.paste.body = "f2195cea00cc796676f77b9d19473f7g" 47 | self.assertFalse(self.analyzer.match(self.paste)) 48 | 49 | # SHA Hash 50 | self.paste.body = "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15" 51 | self.assertFalse(self.analyzer.match(self.paste)) 52 | 53 | # Invalid MD5 hash length 54 | self.paste.body = "f2195cea00cc796676f77b9d19473f7" 55 | self.assertFalse(self.analyzer.match(self.paste)) 56 | 57 | 58 | if __name__ == "__main__": 59 | unittest.main() 60 | -------------------------------------------------------------------------------- /pastepwn/analyzers/tests/megalinkanalyzer_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from pastepwn.analyzers.megalinkanalyzer import MegaLinkAnalyzer 5 | 6 | 7 | class TestMegaLinkAnalyzer(unittest.TestCase): 8 | def setUp(self): 9 | self.analyzer = MegaLinkAnalyzer(None) 10 | self.paste = mock.Mock() 11 | 12 | def test_match_positive(self): 13 | """Test if positives are recognized""" 14 | # working mega link (short version) 15 | self.paste.body = "https://mega.nz/#F!XTQVEAZZ" 16 | self.assertTrue(self.analyzer.match(self.paste)) 17 | 18 | # working mega link (medium version) 19 | self.paste.body = "https://mega.nz/#F!XTQVEAZZ!eqxlOvTxJKnvAkYvjC0O8g" 20 | self.assertTrue(self.analyzer.match(self.paste)) 21 | 22 | # working mega link (long version) 23 | self.paste.body = "https://mega.nz/#F!PB8SSawR!SUokSlF2Zy8CR004DNFfNw!LQtniCoa" 24 | self.assertTrue(self.analyzer.match(self.paste)) 25 | 26 | # without https header 27 | self.paste.body = "mega.nz/#F!XTQVEAZZ!eqxlOvTxJKnvAkYvjC0O8g" 28 | self.assertTrue(self.analyzer.match(self.paste)) 29 | 30 | # http header 31 | self.paste.body = "http://mega.nz/#F!XTQVEAZZ!eqxlOvTxJKnvAkYvjC0O8g" 32 | self.assertTrue(self.analyzer.match(self.paste)) 33 | 34 | # in a sentence 35 | self.paste.body = "check out this file:https://mega.nz/#F!PB8SSawR!SUokSlF2Zy8CR004DNFfNw!LQtniCoa" 36 | self.assertTrue(self.analyzer.match(self.paste)) 37 | 38 | # multiple in one paste 39 | self.paste.body = ( 40 | "check out this file:https://mega.nz/#F!PB8SSawR!SUokSlF2Zy8CR004DNFfNw!LQtniCoa also use this link for other stuff:mega.nz/#F!XTQVEAZZ!eqxlOvTxJKnvAkYvjC0O8g" 41 | ) 42 | match = self.analyzer.match(self.paste) 43 | self.assertTrue(match) 44 | self.assertEqual("https://mega.nz/#F!PB8SSawR!SUokSlF2Zy8CR004DNFfNw!LQtniCoa", match[0]) 45 | self.assertEqual("mega.nz/#F!XTQVEAZZ!eqxlOvTxJKnvAkYvjC0O8g", match[1]) 46 | 47 | def test_match_negative(self): 48 | """Test if negatives are not recognized""" 49 | self.paste.body = "" 50 | self.assertFalse(self.analyzer.match(self.paste)) 51 | 52 | self.paste.body = None 53 | self.assertFalse(self.analyzer.match(self.paste)) 54 | 55 | # Invalid segment length 56 | self.paste.body = "https://mega.nz/#F!XTQVEAZZ1!eqxlOvTxJKnvAkYvjC0O8g" 57 | self.assertFalse(self.analyzer.match(self.paste)) 58 | 59 | 60 | if __name__ == "__main__": 61 | unittest.main() 62 | -------------------------------------------------------------------------------- /pastepwn/analyzers/tests/microsoftkeyanalyzer_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from pastepwn.analyzers.microsoftkeyanalyzer import MicrosoftKeyAnalyzer 5 | 6 | 7 | class TestMicrosoftKeyAnalyzer(unittest.TestCase): 8 | def setUp(self): 9 | self.analyzer = MicrosoftKeyAnalyzer(None) 10 | self.paste = mock.Mock() 11 | 12 | def test_match_positive(self): 13 | """Test if positives are recognized""" 14 | # microsoft key dump 15 | self.paste.body = "2YM6W-NXCWB-RX6F2-QFVDW-J8C9V" 16 | self.assertTrue(self.analyzer.match(self.paste)) 17 | 18 | # microsoft key dump 19 | self.paste.body = "6N667-BMRDR-T2WMM-2RMQ9-DYF3H" 20 | self.assertTrue(self.analyzer.match(self.paste)) 21 | 22 | # microsoft key dump 23 | self.paste.body = "88PNQ-KF99B-VJG64-R28RW-D9JQH" 24 | self.assertTrue(self.analyzer.match(self.paste)) 25 | 26 | # microsoft key dump 27 | self.paste.body = "BNQWW-X4422-FCXD8-JPT37-PWC9V" 28 | self.assertTrue(self.analyzer.match(self.paste)) 29 | 30 | # part of a sentence 31 | self.paste.body = "Hey, I have your key right here: GC288-NVPMT-GWF2M-76228-W42DH!" 32 | self.assertTrue(self.analyzer.match(self.paste)) 33 | 34 | # Newline seperated microsoft key 35 | self.paste.body = "N3B2Q-F9GQF-PTDBG-KKRR7-7QTXV\nN6C37-P8DW3-YVG9X-BRW77-9BQ67" 36 | match = self.analyzer.match(self.paste) 37 | self.assertTrue(match) 38 | self.assertTrue("N3B2Q-F9GQF-PTDBG-KKRR7-7QTXV", match[0]) 39 | self.assertTrue("N6C37-P8DW3-YVG9X-BRW77-9BQ67", match[1]) 40 | 41 | def test_match_negative(self): 42 | """Test if negatives are not recognized""" 43 | self.paste.body = "" 44 | self.assertFalse(self.analyzer.match(self.paste)) 45 | 46 | self.paste.body = None 47 | self.assertFalse(self.analyzer.match(self.paste)) 48 | 49 | # Invalid length 50 | self.paste.body = "PNT6B-DKH2Q-GW4J2-DDT6T-PDHT76" 51 | self.assertFalse(self.analyzer.match(self.paste)) 52 | 53 | # Invalid length 54 | self.paste.body = "PNT6B-DKH2Q-GW4J2-DDT6T-PDHT" 55 | self.assertFalse(self.analyzer.match(self.paste)) 56 | 57 | # Invalid Characters 58 | self.paste.body = "A78QN-Y8XXR-MKFK7-Q4R6C-Q3TXV" 59 | self.assertFalse(self.analyzer.match(self.paste)) 60 | 61 | 62 | if __name__ == "__main__": 63 | unittest.main() 64 | -------------------------------------------------------------------------------- /pastepwn/analyzers/tests/originkeyanalyzer_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from pastepwn.analyzers.originkeyanalyzer import OriginKeyAnalyzer 5 | 6 | 7 | class TestOriginKeyAnalyzer(unittest.TestCase): 8 | def setUp(self): 9 | self.analyzer = OriginKeyAnalyzer(None) 10 | self.paste = mock.Mock() 11 | 12 | def test_match_positive(self): 13 | """Test if positives are recognized""" 14 | # origin key dump 15 | self.paste.body = "87NN-Z277-KTHD-EDJL-7MVB" 16 | self.assertTrue(self.analyzer.match(self.paste)) 17 | 18 | # origin key dump 19 | self.paste.body = "DPMV-FZXD-GD2G-7U28-6S3D" 20 | self.assertTrue(self.analyzer.match(self.paste)) 21 | 22 | # origin key dump 23 | self.paste.body = "ACT3-3TD6-ZDYB-BCGP-TTDM" 24 | self.assertTrue(self.analyzer.match(self.paste)) 25 | 26 | # origin key dump 27 | self.paste.body = "63WW-8VQE-7VEA-HFCD-LVYT" 28 | self.assertTrue(self.analyzer.match(self.paste)) 29 | 30 | # part of a sentence 31 | self.paste.body = "Hey, I have your key right here: 87NN-Z277-KTHD-EDJL-7MVB!" 32 | self.assertTrue(self.analyzer.match(self.paste)) 33 | 34 | # Newline seperated origin key 35 | self.paste.body = "87NN-Z277-KTHD-EDJL-7MVB\nDPMV-FZXD-GD2G-7U28-6S3D" 36 | match = self.analyzer.match(self.paste) 37 | self.assertTrue(match) 38 | self.assertEqual("87NN-Z277-KTHD-EDJL-7MVB", match[0]) 39 | self.assertEqual("DPMV-FZXD-GD2G-7U28-6S3D", match[1]) 40 | 41 | def test_match_negative(self): 42 | """Test if negatives are not recognized""" 43 | self.paste.body = "" 44 | self.assertFalse(self.analyzer.match(self.paste)) 45 | 46 | self.paste.body = None 47 | self.assertFalse(self.analyzer.match(self.paste)) 48 | 49 | # Invalid length 50 | self.paste.body = "A63WW-8VQE-7VEA-HFCD-LVYT" 51 | self.assertFalse(self.analyzer.match(self.paste)) 52 | 53 | # Invalid Characters 54 | self.paste.body = "63W_-8VQE-7VEA-HFCD-LVYT" 55 | self.assertFalse(self.analyzer.match(self.paste)) 56 | 57 | 58 | if __name__ == "__main__": 59 | unittest.main() 60 | -------------------------------------------------------------------------------- /pastepwn/analyzers/tests/pastebinurlanalyzer_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from pastepwn.analyzers.pastebinurlanalyzer import PastebinURLAnalyzer 5 | 6 | 7 | class TestPastebinURLAnalyzer(unittest.TestCase): 8 | def setUp(self): 9 | self.analyzer = PastebinURLAnalyzer(None) 10 | self.paste = mock.Mock() 11 | 12 | def test_match_positive(self): 13 | self.paste.body = "https://pastebin.com/xyz" 14 | self.assertTrue(self.analyzer.match(self.paste)) 15 | self.paste.body = "https://pastebin.com/xyz/" 16 | self.assertTrue(self.analyzer.match(self.paste)) 17 | self.paste.body = "http://pastebin.com/xyz" 18 | self.assertTrue(self.analyzer.match(self.paste)) 19 | self.paste.body = "http://pastebin.com/xyz/" 20 | self.assertTrue(self.analyzer.match(self.paste)) 21 | self.paste.body = "https://pastebin.com/xyz " 22 | self.assertTrue(self.analyzer.match(self.paste)) 23 | self.paste.body = "www.pastebin.com/xyz" 24 | self.assertTrue(self.analyzer.match(self.paste)) 25 | self.paste.body = "pastebin.com/xyx" 26 | self.assertTrue(self.analyzer.match(self.paste)) 27 | self.paste.body = "This is a pastebin URL: pastebin.com/ya249asd - and this is a test" 28 | 29 | def test_match_negative(self): 30 | self.paste.body = "" 31 | self.assertFalse(self.analyzer.match(self.paste)) 32 | self.paste.body = None 33 | self.assertFalse(self.analyzer.match(self.paste)) 34 | self.paste.body = "https://google.com/xyz" 35 | self.assertFalse(self.analyzer.match(self.paste)) 36 | self.paste.body = "xyzpastebin.com/k" 37 | self.assertFalse(self.analyzer.match(self.paste)) 38 | self.paste.body = "https://pastebin.com/" 39 | self.assertFalse(self.analyzer.match(self.paste)) 40 | 41 | if __name__ == "__main__": 42 | unittest.main() 43 | -------------------------------------------------------------------------------- /pastepwn/analyzers/tests/pastetitleanalyzer_test.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unittest 3 | from unittest import mock 4 | 5 | from pastepwn.analyzers.pastetitleanalyzer import PasteTitleAnalyzer 6 | 7 | 8 | class TestPasteTitleAnalyzer(unittest.TestCase): 9 | def setUp(self): 10 | """Set's up a paste mock object""" 11 | self.paste = mock.Mock() 12 | 13 | def test_matchWord(self): 14 | analyzer = PasteTitleAnalyzer(None, regex=r"word") 15 | self.paste.title = "This text contains the word 'word'!" 16 | self.assertTrue(analyzer.match(self.paste), "Title does not match, although it should!") 17 | 18 | def test_matchPattern(self): 19 | analyzer = PasteTitleAnalyzer(None, regex=r"\d{2}-\d{2}-\d{4}") 20 | self.paste.title = "This is a date 24-01-2020 inside the title" 21 | self.assertTrue(analyzer.match(self.paste), "Title does not match pattern!") 22 | 23 | def test_matchPattern2(self): 24 | analyzer = PasteTitleAnalyzer(None, regex=r"^The title") 25 | self.paste.title = "The title is cool!" 26 | self.assertTrue(analyzer.match(self.paste), "Title does not match pattern!") 27 | 28 | def test_flags(self): 29 | """We only test a few flags, because as long as we use regex this should work fine""" 30 | analyzer = PasteTitleAnalyzer(None, regex=r"ThIs iS mIxEd") 31 | self.paste.title = "this is mixed case (not)" 32 | self.assertFalse(analyzer.match(self.paste), "The regex does match, although it shouldn't!") 33 | 34 | # Test case independend flag / ignorecase 35 | analyzer = PasteTitleAnalyzer(None, regex=r"ThIs iS mIxEd", flags=re.IGNORECASE) 36 | self.paste.title = "this is mixed case (not)" 37 | self.assertTrue(analyzer.match(self.paste), "The regex does not match, although it should!") 38 | 39 | # Test multiline 40 | analyzer = PasteTitleAnalyzer(None, regex=r"^regexiscool", flags=re.MULTILINE) 41 | self.paste.title = "this is a multiline string\nregexiscool is right at the start of the line\nand another line!" 42 | self.assertTrue(analyzer.match(self.paste), "The regex does not match in multiline strings, although it should!") 43 | 44 | def test_emptyTitle(self): 45 | analyzer = PasteTitleAnalyzer(None, regex=r"Any \d pattern") 46 | self.paste.title = "" 47 | self.assertFalse(analyzer.match(self.paste), "Empty title does match pattern, although it shouldn't!") 48 | 49 | 50 | if __name__ == "__main__": 51 | unittest.main() 52 | -------------------------------------------------------------------------------- /pastepwn/analyzers/tests/phonenumberanalyzer_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from pastepwn.analyzers.phonenumberanalyzer import PhoneNumberAnalyzer 5 | 6 | 7 | class TestPhoneNumberAnalyzer(unittest.TestCase): 8 | def setUp(self): 9 | self.analyzer = PhoneNumberAnalyzer(None) 10 | self.paste = mock.Mock() 11 | 12 | def test_match_positive(self): 13 | """Test if positives are recognized""" 14 | self.paste.body = "+14155552671" 15 | self.assertTrue(self.analyzer.match(self.paste)) 16 | self.paste.body = "+1 4155552671" 17 | self.assertTrue(self.analyzer.match(self.paste)) 18 | self.paste.body = "+49 89-636-48018" 19 | self.assertTrue(self.analyzer.match(self.paste)) 20 | self.paste.body = "+357 81-127 29103" 21 | self.assertTrue(self.analyzer.match(self.paste)) 22 | self.paste.body = "+1 (811) 1272 103" 23 | self.assertTrue(self.analyzer.match(self.paste)) 24 | 25 | def test_intext(self): 26 | """Test if matches inside text are recognized""" 27 | self.paste.body = "Check out my new number: +1 (811) 1272 103 this is it" 28 | match = self.analyzer.match(self.paste) 29 | self.assertTrue(match) 30 | self.assertEqual("+1 (811) 1272 103", match[0]) 31 | 32 | def test_multiple(self): 33 | """Test if multiple matches are recognized""" 34 | self.paste.body = "Check out all those numbers: +1 (811) 1272 103\n+357 81-127 29103 and many more" 35 | match = self.analyzer.match(self.paste) 36 | self.assertTrue(match) 37 | self.assertEqual("+1 (811) 1272 103", match[0]) 38 | self.assertEqual("+357 81-127 29103", match[1]) 39 | 40 | def test_match_negative(self): 41 | """Test if negatives are not recognized""" 42 | self.paste.body = "" 43 | self.assertFalse(self.analyzer.match(self.paste)) 44 | self.paste.body = None 45 | self.assertFalse(self.analyzer.match(self.paste)) 46 | self.paste.body = "https://www.google.com" 47 | self.assertFalse(self.analyzer.match(self.paste)) 48 | self.paste.body = "123456789" 49 | self.assertFalse(self.analyzer.match(self.paste)) 50 | self.paste.body = "123 456 789" 51 | self.assertFalse(self.analyzer.match(self.paste)) 52 | self.paste.body = "+42" 53 | self.assertFalse(self.analyzer.match(self.paste)) 54 | self.paste.body = "+42 1" 55 | self.assertFalse(self.analyzer.match(self.paste)) 56 | self.paste.body = "+123456789012345678901234567890" 57 | self.assertFalse(self.analyzer.match(self.paste)) 58 | 59 | 60 | if __name__ == "__main__": 61 | unittest.main() 62 | -------------------------------------------------------------------------------- /pastepwn/analyzers/tests/privatekeyanalyzer_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from pastepwn.analyzers.privatekeyanalyzer import PrivateKeyAnalyzer 5 | 6 | 7 | class TestPrivateKeyAnalyzer(unittest.TestCase): 8 | def setUp(self): 9 | self.analyzer = PrivateKeyAnalyzer(None) 10 | self.paste = mock.Mock() 11 | 12 | def test_match_positive(self): 13 | """Test if positives are recognized""" 14 | self.paste.body = "-----BEGIN PRIVATE KEY-----asdasdasdfgsdfgdfgs-----END PRIVATE KEY-----" 15 | self.assertTrue(self.analyzer.match(self.paste)) 16 | self.paste.body = "-----BEGIN PGP PRIVATE KEY BLOCK-----ZzEF0RDWWYb1vPTIeNrKrMEOZ+LesRil6TXCn5/4I0VinlrnVpRbF4xtsVt4eIRkomN9eN-----END PGP PRIVATE KEY BLOCK-----" 17 | self.assertTrue(self.analyzer.match(self.paste)) 18 | self.paste.body = "-----BEGIN RSA PRIVATE KEY-----sRefW7LzPMmTgqHgbNv/gTtT4V/Tc1CiYGVtG9NPlP9tAAAIAI/WvZ3Clh+B0X9mkZ9-----END RSA PRIVATE KEY-----" 19 | self.assertTrue(self.analyzer.match(self.paste)) 20 | self.paste.body = "-----BEGIN ENCRYPTED PRIVATE KEY-----sRefW7LzPMmTgqHgbNv/gTtT4V/Tc1CiYGVtG9NPlP9tAAAIAI/WvZ3Clh+B0X9mkZ9-----END ENCRYPTED PRIVATE KEY-----" 21 | self.assertTrue(self.analyzer.match(self.paste)) 22 | self.paste.body = "-----BEGIN OPENSSH PRIVATE KEY-----sRefW7LzPMmTgqHgbNv/gTtT4V/Tc1CiYGVtG9NPlP9tAAAIAI/WvZ3Clh+B0X9mkZ9-----END OPENSSH PRIVATE KEY-----" 23 | self.assertTrue(self.analyzer.match(self.paste)) 24 | 25 | def test_match_negative(self): 26 | """Test if negatives are not recognized""" 27 | self.paste.body = "" 28 | self.assertFalse(self.analyzer.match(self.paste)) 29 | self.paste.body = None 30 | self.assertFalse(self.analyzer.match(self.paste)) 31 | self.paste.body = "-----BEGIN PUBLIC KEY-----" 32 | self.assertFalse(self.analyzer.match(self.paste)) 33 | 34 | 35 | if __name__ == "__main__": 36 | unittest.main() 37 | -------------------------------------------------------------------------------- /pastepwn/analyzers/tests/shahashanalyzer_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from pastepwn.analyzers.shahashanalyzer import SHAHashAnalyzer 5 | 6 | 7 | class TestSHAHashAnalyzer(unittest.TestCase): 8 | def setUp(self): 9 | self.analyzer = SHAHashAnalyzer(None) 10 | self.paste = mock.Mock() 11 | 12 | def test_match_positive(self): 13 | """Test if positives are recognized""" 14 | # obtained by: $ echo "foo" | shasum 15 | self.paste.body = "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15" 16 | self.assertTrue(self.analyzer.match(self.paste)) 17 | 18 | # obtained by: $ echo "foo" | shasum -a 224 19 | self.paste.body = "e7d5e36e8d470c3e5103fedd2e4f2aa5c30ab27f6629bdc3286f9dd2" 20 | self.assertTrue(self.analyzer.match(self.paste)) 21 | 22 | # obtained by: $ echo "foo" | shasum -a 256 23 | self.paste.body = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c" 24 | self.assertTrue(self.analyzer.match(self.paste)) 25 | 26 | # obtained by: $ echo "foo" | shasum -a 384 27 | self.paste.body = "8effdabfe14416214a250f935505250bd991f106065d899db6e19bdc8bf648f3ac0f1935c4f65fe8f798289b1a0d1e06" 28 | self.assertTrue(self.analyzer.match(self.paste)) 29 | 30 | # obtained by: $ echo "foo" | shasum -a 512 31 | self.paste.body = "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6" 32 | self.assertTrue(self.analyzer.match(self.paste)) 33 | 34 | self.paste.body = "my super cool hash is f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 and here's some more text" 35 | self.assertTrue(self.analyzer.match(self.paste)) 36 | 37 | # Newline-separated valid hashes 38 | self.paste.body = "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15\ne7d5e36e8d470c3e5103fedd2e4f2aa5c30ab27f6629bdc3286f9dd2" 39 | self.assertTrue(self.analyzer.match(self.paste)) 40 | 41 | def test_match_negative(self): 42 | """Test if negatives are not recognized""" 43 | self.paste.body = "" 44 | self.assertFalse(self.analyzer.match(self.paste)) 45 | 46 | self.paste.body = None 47 | self.assertFalse(self.analyzer.match(self.paste)) 48 | 49 | # Invalid character 'g' 50 | self.paste.body = "1234567890abcdefg" 51 | self.assertFalse(self.analyzer.match(self.paste)) 52 | 53 | # MD5 Hash 54 | self.paste.body = "9e107d9d372bb6826bd81d3542a419d6" 55 | self.assertFalse(self.analyzer.match(self.paste)) 56 | 57 | # Invalid SHA hash length 58 | self.paste.body = 41 * "a" 59 | self.assertFalse(self.analyzer.match(self.paste)) 60 | self.paste.body = 513 * "a" 61 | self.assertFalse(self.analyzer.match(self.paste)) 62 | 63 | 64 | if __name__ == "__main__": 65 | unittest.main() 66 | -------------------------------------------------------------------------------- /pastepwn/analyzers/tests/slacktokenanalyzer_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from pastepwn.analyzers.slacktokenanalyzer import SlackTokenAnalyzer 5 | 6 | 7 | class TestSlackTokenAnalyzer(unittest.TestCase): 8 | def setUp(self): 9 | self.analyzer = SlackTokenAnalyzer(None) 10 | self.paste = mock.Mock() 11 | 12 | def test_match_positive(self): 13 | """Test if positives are recognized""" 14 | # slack key dump 15 | self.paste.body = "xoxb-999999999999-999999999999-999999999999-9999999999999999999999999999999a" 16 | self.assertTrue(self.analyzer.match(self.paste)) 17 | 18 | # slack key dump 19 | self.paste.body = "xoxb-999999999999-999999999999-999999999999-99999999999999999999999999999999" 20 | self.assertTrue(self.analyzer.match(self.paste)) 21 | 22 | # slack key dump 23 | self.paste.body = "xoxp-999999999999-999999999999-999999999999-99999999999999999999999999999999" 24 | self.assertTrue(self.analyzer.match(self.paste)) 25 | 26 | # slack key dump 27 | self.paste.body = "xoxa-999999999999-999999999999-999999999999-99999999999999999999999999999999" 28 | self.assertTrue(self.analyzer.match(self.paste)) 29 | 30 | def test_intext(self): 31 | """Test if matches inside text are recognized""" 32 | self.paste.body = "my token is: xoxo-999999999999-999999999999-999999999999-99999999999999abc999999999999999" 33 | match = self.analyzer.match(self.paste) 34 | self.assertTrue(match) 35 | self.assertEqual("xoxo-999999999999-999999999999-999999999999-99999999999999abc999999999999999", match[0]) 36 | 37 | def test_multiple(self): 38 | """Test if multiple matches are recognized""" 39 | self.paste.body = ( 40 | "my token is: xoxo-999999999999-999999999999-999999999999-99999999999999abc999999999999999. Please also" 41 | "take xoxa-999999999999-999999999999-999999999999-99999999999999999999999999999999!" 42 | ) 43 | match = self.analyzer.match(self.paste) 44 | self.assertTrue(match) 45 | self.assertEqual(2, len(match)) 46 | self.assertEqual("xoxo-999999999999-999999999999-999999999999-99999999999999abc999999999999999", match[0]) 47 | self.assertEqual("xoxa-999999999999-999999999999-999999999999-99999999999999999999999999999999", match[1]) 48 | 49 | def test_match_negative(self): 50 | """Test if negatives are not recognized""" 51 | self.paste.body = "" 52 | self.assertFalse(self.analyzer.match(self.paste)) 53 | 54 | self.paste.body = "my bike isn't a number" 55 | self.assertFalse(self.analyzer.match(self.paste)) 56 | 57 | # Invalid length 58 | self.paste.body = "xoxa-999999999999-999999999999-99999999999-99999999999999999999999999999999" 59 | self.assertFalse(self.analyzer.match(self.paste)) 60 | 61 | # Upper-case 62 | self.paste.body = "my token is: xoxo-999999999999-999999999999-999999999999-99999999999999Abc999999999999999" 63 | self.assertFalse(self.analyzer.match(self.paste)) 64 | # too short and Upper-case 65 | self.paste.body = "my token is: xoxo-999999999999-999999999999-99999999999-99999999999999Abc999999999999999" 66 | self.assertFalse(self.analyzer.match(self.paste)) 67 | 68 | 69 | if __name__ == "__main__": 70 | unittest.main() 71 | -------------------------------------------------------------------------------- /pastepwn/analyzers/tests/slackwebhookanalyzer_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from pastepwn.analyzers.slackwebhookanalyzer import SlackWebhookAnalyzer 5 | 6 | 7 | class TestSlackWebhookAnalyzer(unittest.TestCase): 8 | def setUp(self): 9 | self.analyzer = SlackWebhookAnalyzer(None) 10 | self.paste = mock.Mock() 11 | 12 | def test_match_positive(self): 13 | """Test if positives are recognized""" 14 | # slack webhook url (sample) 15 | self.paste.body = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" 16 | self.assertTrue(self.analyzer.match(self.paste)) 17 | 18 | # slack webhook url (manually generated) 19 | self.paste.body = "https://hooks.slack.com/services/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" 20 | self.assertTrue(self.analyzer.match(self.paste)) 21 | 22 | # slack webhook url (randomly generated) 23 | self.paste.body = "https://hooks.slack.com/services/TwLj3Aeic/B2RnzBQQp/7JkqKP9XxuqN3WFDn3tUA8NJ" 24 | self.assertTrue(self.analyzer.match(self.paste)) 25 | 26 | # slack webhook url (randomly generated) 27 | self.paste.body = "https://hooks.slack.com/services/TafdGEj9a/B9BdR2SLM/yJAk3gcguM8YzFEpaPnSvZ4Q" 28 | self.assertTrue(self.analyzer.match(self.paste)) 29 | 30 | def test_intext(self): 31 | """Test if matches inside text are recognized""" 32 | self.paste.body = "here is the webhook url: The slack webhook key is https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX! how about that!" 33 | match = self.analyzer.match(self.paste) 34 | self.assertTrue(match) 35 | self.assertEqual("https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX", match[0]) 36 | 37 | def test_multiple(self): 38 | """Test if multiple matches are recognized""" 39 | self.paste.body = ( 40 | "here is the webhook url: The slack webhook key is " 41 | "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX! how about that! and now" 42 | "there is another one https://hooks.slack.com/services/T00000000/B00000000/YYYYYYYYYYYYYYYYYYYYYYYY right here!" 43 | "And an invalid url: https://hooks.slack.com/services/T00000000/B00000000/ZZZZZZZZZZ there!" 44 | ) 45 | match = self.analyzer.match(self.paste) 46 | self.assertTrue(match) 47 | self.assertEqual(2, len(match)) 48 | self.assertEqual("https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX", match[0]) 49 | self.assertEqual("https://hooks.slack.com/services/T00000000/B00000000/YYYYYYYYYYYYYYYYYYYYYYYY", match[1]) 50 | 51 | def test_match_negative(self): 52 | """Test if negatives are not recognized""" 53 | self.paste.body = "" 54 | self.assertFalse(self.analyzer.match(self.paste)) 55 | 56 | self.paste.body = None 57 | self.assertFalse(self.analyzer.match(self.paste)) 58 | 59 | # Other Slack URL (api docs) 60 | self.paste.body = "https://api.slack.com/incoming-webhooks" 61 | self.assertFalse(self.analyzer.match(self.paste)) 62 | 63 | # Invalid Character 64 | self.paste.body = "https://hooks.slack.com/services/T00!00000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" 65 | self.assertFalse(self.analyzer.match(self.paste)) 66 | 67 | # Invalid Length 68 | self.paste.body = "https://hooks.slack.com/services/T00000000/B0000000/XXXXXXXXXXXXXXXXXXXXXXXX" 69 | self.assertFalse(self.analyzer.match(self.paste)) 70 | 71 | # Invalid Format (/services/Z... vs /services/T...) 72 | self.paste.body = "https://hooks.slack.com/services/Z00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" 73 | self.assertFalse(self.analyzer.match(self.paste)) 74 | 75 | 76 | if __name__ == "__main__": 77 | unittest.main() 78 | -------------------------------------------------------------------------------- /pastepwn/analyzers/tests/steamkeyanalyzer_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from pastepwn.analyzers.steamkeyanalyzer import SteamKeyAnalyzer 5 | 6 | 7 | class TestSteamKeyAnalyzer(unittest.TestCase): 8 | def setUp(self): 9 | self.analyzer = SteamKeyAnalyzer(None) 10 | self.paste = mock.Mock() 11 | 12 | def test_match_positive(self): 13 | """Test if positives are recognized""" 14 | # steam key dump 15 | self.paste.body = "EZFNY-7GECB-94WRD" 16 | self.assertTrue(self.analyzer.match(self.paste)) 17 | 18 | # steam key dump 19 | self.paste.body = "10NL7-7E8VK-ZUSOD" 20 | self.assertTrue(self.analyzer.match(self.paste)) 21 | 22 | # steam key dump 23 | self.paste.body = "CB6AW-XHHB1-IANNO" 24 | self.assertTrue(self.analyzer.match(self.paste)) 25 | 26 | # steam key dump 27 | self.paste.body = "E4XJ8-2MRI0-RX4I5" 28 | self.assertTrue(self.analyzer.match(self.paste)) 29 | 30 | # 5 segment steam key 31 | self.paste.body = "1AB2C-D3FGH-456I7-JK8LM-NOP9Q" 32 | self.assertTrue(self.analyzer.match(self.paste)) 33 | 34 | def test_intext(self): 35 | """Test if matches inside text are recognized""" 36 | # part of a sentence 37 | self.paste.body = "Hey, I have your key right here: EL0SY-DC710-X0C5W!" 38 | match = self.analyzer.match(self.paste) 39 | self.assertTrue(match) 40 | self.assertEqual("EL0SY-DC710-X0C5W", match[0]) 41 | 42 | def test_multiple(self): 43 | """Test if multiple matches are recognized""" 44 | # Newline seperated steam key 45 | self.paste.body = "E4XJ8-2MRI0-RX4I5\nCIZ36-WD38P-QZI6U" 46 | match = self.analyzer.match(self.paste) 47 | self.assertTrue(match) 48 | self.assertEqual(2, len(match)) 49 | self.assertEqual("E4XJ8-2MRI0-RX4I5", match[0]) 50 | self.assertEqual("CIZ36-WD38P-QZI6U", match[1]) 51 | 52 | def test_match_negative(self): 53 | """Test if negatives are not recognized""" 54 | self.paste.body = "" 55 | self.assertFalse(self.analyzer.match(self.paste)) 56 | 57 | self.paste.body = None 58 | self.assertFalse(self.analyzer.match(self.paste)) 59 | 60 | # Invalid length 61 | self.paste.body = "CBCB6AW-XHHB1-IANNO" 62 | self.assertFalse(self.analyzer.match(self.paste)) 63 | 64 | # Lower-case 65 | self.paste.body = "e4xj8-2mri0-rx4i5" 66 | self.assertFalse(self.analyzer.match(self.paste)) 67 | 68 | 69 | if __name__ == "__main__": 70 | unittest.main() 71 | -------------------------------------------------------------------------------- /pastepwn/analyzers/tests/stripeapikeyanalyzer_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from pastepwn.analyzers.stripeapikeyanalyzer import StripeApiKeyAnalyzer 5 | 6 | 7 | class TestStripeApiKeyAnalyzer(unittest.TestCase): 8 | def setUp(self): 9 | self.analyzer = StripeApiKeyAnalyzer(None) 10 | self.paste = mock.Mock() 11 | 12 | def test_match_positive(self): 13 | """Test if positives are recognized""" 14 | # steam key dump 15 | self.paste.body = "sk_test_4fsrdffsdf345345dfgfg34i" 16 | self.assertTrue(self.analyzer.match(self.paste)) 17 | 18 | # steam key dump 19 | self.paste.body = "rk_test_4fsrdffsdf345345dfgfg34i" 20 | self.assertTrue(self.analyzer.match(self.paste)) 21 | 22 | # steam key dump 23 | self.paste.body = "sk_live_4fsrdffsdf345345dfgfg34i" 24 | self.assertTrue(self.analyzer.match(self.paste)) 25 | 26 | # steam key dump 27 | self.paste.body = "rk_live_4fsrdffsdf345345dfgfg34i" 28 | self.assertTrue(self.analyzer.match(self.paste)) 29 | 30 | # steam key dump 31 | self.paste.body = "sk_test_YUTGF76uyh876Tyg87T786Tu" 32 | self.assertTrue(self.analyzer.match(self.paste)) 33 | 34 | def test_intext(self): 35 | """Test if matches inside text are recognized""" 36 | self.paste.body = "There is my api key: sk_test_YUTGF76uyh876Tyg87T786Tu - take good care of it" 37 | match = self.analyzer.match(self.paste) 38 | self.assertTrue(match) 39 | self.assertEqual("sk_test_YUTGF76uyh876Tyg87T786Tu", match[0]) 40 | 41 | def test_multiple(self): 42 | """Test if multiple matches are recognized""" 43 | self.paste.body = "There is my api key: sk_test_YUTGF76uyh876Tyg87T786Tu - take good care of it. The other one is sk_live_4fsrdffsdf345345dfgfg34i" 44 | match = self.analyzer.match(self.paste) 45 | self.assertTrue(match) 46 | self.assertEqual(2, len(match)) 47 | self.assertEqual("sk_test_YUTGF76uyh876Tyg87T786Tu", match[0]) 48 | self.assertEqual("sk_live_4fsrdffsdf345345dfgfg34i", match[1]) 49 | 50 | def test_match_negative(self): 51 | """Test if negatives are not recognized""" 52 | self.paste.body = "" 53 | self.assertFalse(self.analyzer.match(self.paste)) 54 | 55 | self.paste.body = None 56 | self.assertFalse(self.analyzer.match(self.paste)) 57 | 58 | # Invalid length 59 | self.paste.body = "sk_test_YUTGF76uyh876" 60 | self.assertFalse(self.analyzer.match(self.paste)) 61 | 62 | # Invalid first letter 63 | self.paste.body = "pk_test_YUTGF76uyh876Tyg87T786Tu" 64 | self.assertFalse(self.analyzer.match(self.paste)) 65 | 66 | # Invalid state 67 | self.paste.body = "sk_staging_YUTGF76uyh876Tyg87T786Tu" 68 | self.assertFalse(self.analyzer.match(self.paste)) 69 | 70 | 71 | if __name__ == "__main__": 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /pastepwn/analyzers/tests/uplaykeyanalyzer_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from pastepwn.analyzers.uplaykeyanalyzer import UplayKeyAnalyzer 5 | 6 | 7 | class TestUplayKeyAnalyzer(unittest.TestCase): 8 | def setUp(self): 9 | self.analyzer = UplayKeyAnalyzer(None) 10 | self.paste = mock.Mock() 11 | 12 | def test_match_positive(self): 13 | """Test if positives are recognized""" 14 | # working uplay key 15 | self.paste.body = "MB9C-LV3C-4RG8-FME8" 16 | self.assertTrue(self.analyzer.match(self.paste)) 17 | 18 | # part of a sentence 19 | self.paste.body = "Hey, I have your key right here: MB9C-LV3C-4RG8-FME8!" 20 | self.assertTrue(self.analyzer.match(self.paste)) 21 | 22 | # Newline seperated uplay key 23 | self.paste.body = "MB9C-LV3C-4RG8-FME8\nMB9C-LV3C-4RG8-FME8" 24 | match = self.analyzer.match(self.paste) 25 | self.assertTrue(match) 26 | self.assertEqual("MB9C-LV3C-4RG8-FME8", match[0]) 27 | self.assertEqual("MB9C-LV3C-4RG8-FME8", match[1]) 28 | 29 | def test_match_negative(self): 30 | """Test if negatives are not recognized""" 31 | self.paste.body = "" 32 | self.assertFalse(self.analyzer.match(self.paste)) 33 | 34 | self.paste.body = None 35 | self.assertFalse(self.analyzer.match(self.paste)) 36 | 37 | # Invalid length 38 | self.paste.body = "MB9AC-LV3C-4RG8-FME8" 39 | self.assertFalse(self.analyzer.match(self.paste)) 40 | 41 | # Lower-case 42 | self.paste.body = "mb9c-lv3c-4rg8-fme8" 43 | self.assertFalse(self.analyzer.match(self.paste)) 44 | 45 | 46 | if __name__ == "__main__": 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /pastepwn/analyzers/uplaykeyanalyzer.py: -------------------------------------------------------------------------------- 1 | from .regexanalyzer import RegexAnalyzer 2 | 3 | 4 | class UplayKeyAnalyzer(RegexAnalyzer): 5 | """Analyzer to match uplay keys via regex.""" 6 | 7 | name = "UplayKeyAnalyzer" 8 | 9 | def __init__(self, actions): 10 | regex = r"\b(? bad 3 | 4 | from .paste import Paste 5 | from .actionhandler import ActionHandler 6 | from .scrapinghandler import ScrapingHandler 7 | from .pastedispatcher import PasteDispatcher 8 | from .pastepwn import PastePwn 9 | 10 | __all__ = ["ActionHandler", "Paste", "PasteDispatcher", "PastePwn", "ScrapingHandler"] 11 | -------------------------------------------------------------------------------- /pastepwn/core/actionhandler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from queue import Empty, Queue 3 | from threading import Event, Lock 4 | from time import sleep 5 | 6 | from pastepwn.util import join_threads, start_thread 7 | 8 | 9 | class ActionHandler: 10 | """Handler to execute all the actions, if an analyzer matches a paste""" 11 | 12 | def __init__(self, action_queue=None, exception_event=None, stop_event=None): 13 | self.logger = logging.getLogger(__name__) 14 | self.action_queue = action_queue or Queue() 15 | self.__exception_event = exception_event or Event() 16 | self.__stop_event = stop_event or Event() 17 | self.__threads = [] 18 | 19 | self.running = False 20 | self.__lock = Lock() 21 | 22 | def start(self, ready=None): 23 | """ 24 | Starts the actionhandler to execute actions if pastes are matched 25 | :param ready: Event to check from the outside if the actionhandler has been started 26 | :return: None 27 | """ 28 | with self.__lock: 29 | if not self.running: 30 | self.running = True 31 | 32 | thread = start_thread(self._start, "ActionHandler", self.__exception_event) 33 | self.__threads.append(thread) 34 | 35 | if ready is not None: 36 | ready.set() 37 | 38 | def stop(self): 39 | """ 40 | Stops the actionhandler 41 | :return: None 42 | """ 43 | self.__stop_event.set() 44 | while self.running: 45 | sleep(0.1) 46 | self.__stop_event.clear() 47 | 48 | join_threads(self.__threads) 49 | self.__threads = [] 50 | 51 | def _start(self): 52 | while self.running: 53 | try: 54 | # Get paste from queue 55 | actions, paste, analyzer, matches = self.action_queue.get(True, 1) 56 | except Empty: 57 | if self.__stop_event.is_set(): 58 | self.logger.debug("orderly stopping ActionHandler") 59 | self.running = False 60 | break 61 | elif self.__exception_event.is_set(): 62 | self.logger.critical("stopping ActionHandler due to exception in another thread") 63 | self.running = False 64 | break 65 | continue 66 | 67 | if actions is None: 68 | continue 69 | 70 | for action in actions: 71 | self._perform_action_wrapper(action, paste, analyzer, matches) 72 | 73 | def _perform_action_wrapper(self, action, paste, analyzer, matches): 74 | """A wrapper around the perform method to catch exceptions""" 75 | self.logger.debug(f"Performing action '{action.name}' on paste '{paste.key}' matched by analyzer '{analyzer.identifier}'!") 76 | try: 77 | action.perform(paste, analyzer.identifier, matches) 78 | except Exception: 79 | self.logger.exception(f"While performing the action '{action.name}' an exception occurred") 80 | -------------------------------------------------------------------------------- /pastepwn/core/paste.py: -------------------------------------------------------------------------------- 1 | class Paste: 2 | """Representation of a paste object used for example by pastebin""" 3 | 4 | def __init__(self, key, title, user, size, date, expire, syntax, scrape_url, full_url): 5 | self.key = key 6 | self.title = title 7 | self.user = user 8 | self.size = size 9 | self.date = date 10 | self.expire = expire 11 | self.syntax = syntax 12 | self.scrape_url = scrape_url 13 | self.full_url = full_url 14 | self.body = "" 15 | 16 | def set_body(self, body): 17 | """ 18 | Sets the body of the paste 19 | :param body: String containing the content of the paste 20 | :return: 21 | """ 22 | self.body = str(body or "") 23 | 24 | def __str__(self): 25 | return str(self.to_dict()) 26 | 27 | __repr__ = __str__ 28 | 29 | def to_dict(self): 30 | """ 31 | Generates a dict out of the paste 32 | :return: Paste as dict 33 | """ 34 | return self.__dict__.copy() 35 | -------------------------------------------------------------------------------- /pastepwn/core/pastedispatcher.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from queue import Empty, Queue 3 | from threading import Event, Lock 4 | from time import sleep 5 | 6 | from pastepwn.util import join_threads, start_thread 7 | 8 | 9 | class PasteDispatcher: 10 | """The PasteDispatcher dispatches the downloaded pastes to the analyzers""" 11 | 12 | def __init__(self, paste_queue, action_queue=None, exception_event=None): 13 | self.logger = logging.getLogger(__name__) 14 | self.paste_queue = paste_queue 15 | self.action_queue = action_queue or Queue() 16 | self.analyzers = [] 17 | self.running = False 18 | 19 | self.__lock = Lock() 20 | self.__threads = [] 21 | self.__thread_pool = set() 22 | self.__exception_event = exception_event or Event() 23 | self.__stop_event = Event() 24 | 25 | def add_analyzer(self, analyzer): 26 | """Adds an analyzer to the list of analyzers""" 27 | with self.__lock: 28 | self.analyzers.append(analyzer) 29 | 30 | def start(self, workers=4, ready=None): 31 | """Starts dispatching the downloaded pastes to the list of analyzers""" 32 | with self.__lock: 33 | if not self.running: 34 | if not self.analyzers: 35 | self.logger.warning("No analyzers added! At least one analyzer must be added prior to use!") 36 | return None 37 | 38 | self.running = True 39 | thread = start_thread(self._start_analyzing, "PasteDispatcher", exception_event=self.__exception_event) 40 | self.__threads.append(thread) 41 | 42 | # Start thread pool with worker threads 43 | # for i in range(workers): 44 | # thread = Thread(target=self._pool_thread, name="analyzer_{0}".format(i)) 45 | # self.__thread_pool.add(thread) 46 | # thread.start() 47 | 48 | if ready is not None: 49 | ready.set() 50 | 51 | return self.action_queue 52 | 53 | def _start_analyzing(self): 54 | while self.running: 55 | try: 56 | # Get paste from queue 57 | paste = self.paste_queue.get(True, 1) 58 | except Empty: 59 | if self.__stop_event.is_set(): 60 | self.logger.debug("orderly stopping") 61 | self.running = False 62 | break 63 | elif self.__exception_event.is_set(): 64 | self.logger.critical("stopping due to exception in another thread") 65 | self.running = False 66 | break 67 | continue 68 | 69 | # TODO implement thread pool to limit number of parallel executed threads 70 | # Don't add these threads to the list. Otherwise they will just block the list 71 | start_thread(self._process_paste, "process_paste", paste=paste, exception_event=self.__exception_event) 72 | 73 | def _process_paste(self, paste): 74 | self.logger.debug(f"Analyzing Paste: {paste.key}") 75 | for analyzer in self.analyzers: 76 | matches = analyzer.match(paste) 77 | 78 | if matches: 79 | # If the analyzer just returns a boolean, we pass an empty list 80 | if isinstance(matches, bool): 81 | # matches == True, hence we pass an empty list 82 | matches = [] 83 | elif not isinstance(matches, list): 84 | # when matches is not a bool, we pass the object as list 85 | matches = [matches] 86 | actions = analyzer.actions 87 | self.action_queue.put((actions, paste, analyzer, matches)) 88 | 89 | def stop(self): 90 | """Stops dispatching pastes to the analyzers""" 91 | self.__stop_event.set() 92 | while self.running: 93 | sleep(0.1) 94 | self.__stop_event.clear() 95 | 96 | join_threads(self.__threads) 97 | self.__threads = [] 98 | -------------------------------------------------------------------------------- /pastepwn/core/scrapinghandler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from queue import Queue 3 | from threading import Event, Lock 4 | 5 | from pastepwn.util import start_thread 6 | 7 | 8 | class ScrapingHandler: 9 | """Class to handle all the given scrapers to fetch pastes from different sources""" 10 | 11 | def __init__(self, paste_queue=None, exception_event=None): 12 | self.logger = logging.getLogger(__name__) 13 | self.running = False 14 | self.__exception_event = exception_event or Event() 15 | self.paste_queue = paste_queue or Queue() 16 | self.__lock = Lock() 17 | self.__threads = [] 18 | self.scrapers = [] 19 | 20 | def add_scraper(self, scraper, restart_scraping=False): 21 | """Adds a scraper to the list of scrapers""" 22 | self.scrapers.append(scraper) 23 | 24 | if self.running and restart_scraping: 25 | logging.info("Restarting scrapers...") 26 | self.stop() 27 | self.start() 28 | 29 | def start(self): 30 | """Starts scraping pastes from the provided sources""" 31 | with self.__lock: 32 | if not self.running: 33 | # There needs to be at least one scraper 34 | if not self.scrapers: 35 | self.logger.warning("No scrapers added! At least one scraper must be added prior to use!") 36 | return None 37 | 38 | self.running = True 39 | # Start all scraper threads 40 | for scraper in self.scrapers: 41 | thread = start_thread(scraper.start, scraper.name, paste_queue=self.paste_queue, exception_event=self.__exception_event) 42 | self.__threads.append(thread) 43 | 44 | # Return the update queue so the main thread can insert updates 45 | return self.paste_queue 46 | 47 | return None 48 | 49 | def stop(self): 50 | """Stops scraping pastes""" 51 | with self.__lock: 52 | if not self.running: 53 | return 54 | 55 | self.logger.debug("Stopping scrapers...") 56 | self.running = False 57 | for scraper in self.scrapers: 58 | scraper.stop() 59 | self._join_threads() 60 | 61 | def _join_threads(self): 62 | """End all threads and join them back into the main thread""" 63 | for thread in self.__threads: 64 | self.logger.debug(f"Joining thread {thread.name}") 65 | thread.join() 66 | self.logger.debug(f"Thread {thread.name} has ended") 67 | 68 | self.__threads = [] 69 | -------------------------------------------------------------------------------- /pastepwn/core/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-Rickyy-b/pastepwn/6832d70cb1eb9e141e49a776b69cf2844906025b/pastepwn/core/tests/__init__.py -------------------------------------------------------------------------------- /pastepwn/core/tests/paste_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pastepwn import Paste 4 | 5 | 6 | class PasteTest(unittest.TestCase): 7 | def setUp(self): 8 | p = { 9 | "scrape_url": "https://scrape.pastebin.com/api_scrape_item.php?i=0CeaNm8Y", 10 | "full_url": "https://pastebin.com/0CeaNm8Y", 11 | "date": "1442911802", 12 | "key": "0CeaNm8Y", 13 | "size": "890", 14 | "expire": "1442998159", 15 | "title": "Once we all know when we goto function", 16 | "syntax": "java", 17 | "user": "admin", 18 | "body": "This is a test for pastepwn", 19 | } 20 | 21 | self.p = p 22 | self.paste = Paste(p.get("key"), p.get("title"), p.get("user"), p.get("size"), p.get("date"), p.get("expire"), p.get("syntax"), p.get("scrape_url"), p.get("full_url")) 23 | 24 | def tearDown(self): 25 | pass 26 | 27 | def test_init_paste(self): 28 | self.assertEqual(self.p.get("key"), self.paste.key) 29 | self.assertEqual(self.p.get("title"), self.paste.title) 30 | self.assertEqual(self.p.get("user"), self.paste.user) 31 | self.assertEqual(self.p.get("size"), self.paste.size) 32 | self.assertEqual(self.p.get("date"), self.paste.date) 33 | self.assertEqual(self.p.get("expire"), self.paste.expire) 34 | self.assertEqual(self.p.get("syntax"), self.paste.syntax) 35 | self.assertEqual(self.p.get("scrape_url"), self.paste.scrape_url) 36 | self.assertEqual(self.p.get("full_url"), self.paste.full_url) 37 | self.assertEqual("", self.paste.body) 38 | 39 | def test_set_body(self): 40 | my_body = "This is a test for pastepwn" 41 | self.paste.set_body(my_body) 42 | self.assertEqual(my_body, self.paste.body) 43 | 44 | def test_empty_body(self): 45 | empty_body = "" 46 | self.paste.set_body(None) 47 | self.assertEqual(empty_body, self.paste.body) 48 | 49 | def test_to_dict(self): 50 | my_body = "This is a test for pastepwn" 51 | self.paste.set_body(my_body) 52 | 53 | paste_dict = self.paste.to_dict() 54 | self.assertEqual(self.p, paste_dict) 55 | 56 | 57 | if __name__ == "__main__": 58 | unittest.main() 59 | -------------------------------------------------------------------------------- /pastepwn/database/__init__.py: -------------------------------------------------------------------------------- 1 | from .abstractdb import AbstractDB 2 | from .mongodb import MongoDB 3 | from .mysqldb import MysqlDB 4 | from .sqlitedb import SQLiteDB 5 | 6 | __all__ = ["AbstractDB", "MongoDB", "MysqlDB", "SQLiteDB"] 7 | -------------------------------------------------------------------------------- /pastepwn/database/abstractdb.py: -------------------------------------------------------------------------------- 1 | class AbstractDB: 2 | def __init__(self): 3 | pass 4 | 5 | def store(self, paste): 6 | """Stores a paste in the database""" 7 | raise NotImplementedError 8 | 9 | def get(self, key): 10 | """Fetches a paste from the database""" 11 | raise NotImplementedError 12 | -------------------------------------------------------------------------------- /pastepwn/database/mongodb.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pymongo 4 | from pymongo.errors import ConnectionFailure 5 | 6 | from .abstractdb import AbstractDB 7 | 8 | 9 | class MongoDB(AbstractDB): 10 | def __init__(self, ip="127.0.0.1", port=27017, dbname="pastepwn", collectionname="pastes"): 11 | super().__init__() 12 | self.logger = logging.getLogger(__name__) 13 | self.logger.debug(f"Initializing MongoDB - {ip}:{port}") 14 | timeout_ms = 5000 15 | self.db = pymongo.MongoClient(ip, port, serverSelectionTimeoutMS=timeout_ms) 16 | 17 | try: 18 | self.db.admin.command("ismaster") 19 | except ConnectionFailure: 20 | self.logger.exception("An exception occurred while initializing the database") 21 | raise 22 | 23 | self.logger.debug("Connected to database!") 24 | 25 | self.db = self.db[dbname] 26 | self.collection = self.db[collectionname] 27 | self.collection.create_index([("key", pymongo.ASCENDING)], unique=True) 28 | 29 | def _insert_data(self, data): 30 | self.collection.update_one({"key": data["key"]}, {"$set": data}, upsert=True) 31 | 32 | def _get_data(self, key, value): 33 | return self.collection.find({key: value}) 34 | 35 | # TODO def update_data(self, ) 36 | 37 | # TODO def delete_data(self,) 38 | 39 | def count(self, key, value): 40 | return self.collection.find({key: value}).count() 41 | 42 | def count_all(self): 43 | return self.collection.count() 44 | 45 | def store(self, paste): 46 | self.logger.debug(f"Storing paste {paste.key}") 47 | 48 | try: 49 | self._insert_data(paste.to_dict()) 50 | except pymongo.errors.DuplicateKeyError: 51 | self.logger.debug(f"Duplicate key '{paste.key}' - Not storing paste") 52 | 53 | def get(self, key): 54 | return self._get_data("key", key) 55 | -------------------------------------------------------------------------------- /pastepwn/database/mysqldb.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import mysql.connector 4 | 5 | from .abstractdb import AbstractDB 6 | 7 | 8 | class MysqlDB(AbstractDB): 9 | def __init__(self, ip="127.0.0.1", port=3306, unix_socket=None, dbname="pastepwn", username=None, password=None, timeout=10): 10 | super().__init__() 11 | self.logger = logging.getLogger(__name__) 12 | self.logger.debug(f"Initializing MySQLDB - {ip}:{port}") 13 | 14 | # https://dev.mysql.com/doc/connector-python/en/connector-python-connectargs.html 15 | if unix_socket: 16 | self.db = mysql.connector.connect(host=ip, user=username, passwd=password, unix_socket=unix_socket, connection_timeout=timeout) 17 | else: 18 | self.db = mysql.connector.connect(host=ip, port=port, user=username, passwd=password, database=dbname, connection_timeout=timeout) 19 | 20 | self.cursor = self.db.cursor() 21 | self._create_tables() 22 | 23 | self.logger.debug("Connected to database!") 24 | 25 | def _create_tables(self): 26 | # Although the length of 'key' should never exceed 8 chars, 27 | # making it longer prevents from future issues. 28 | self.cursor.execute("""CREATE TABLE IF NOT EXISTS `pastes` ( 29 | `key` VARCHAR(30) NOT NULL UNIQUE, 30 | `title` TEXT, 31 | `user` TEXT, 32 | `size` INTEGER, 33 | `date` INTEGER, 34 | `expire` INTEGER, 35 | `syntax` TEXT, 36 | `scrape_url` TEXT, 37 | `full_url` TEXT, 38 | `body` TEXT, 39 | PRIMARY KEY(`key`));""") 40 | self.db.commit() 41 | 42 | def _insert_data(self, paste): 43 | self.cursor.execute( 44 | "INSERT INTO `pastes` (`key`, `title`, `user`, `size`, `date`, `expire`, `syntax`, `scrape_url`, `full_url`, `body`) " 45 | "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s);", 46 | (paste.key, paste.title, paste.user, paste.size, paste.date, paste.expire, paste.syntax, paste.scrape_url, paste.full_url, paste.body), 47 | ) 48 | self.db.commit() 49 | 50 | def _get_data(self, key, value): 51 | raise NotImplementedError 52 | 53 | def count(self, key, value): 54 | # TODO add filter to counting 55 | return self.cursor.execute("SELECT count(*) FROM pastes") 56 | 57 | def count_all(self): 58 | return self.cursor.execute("SELECT count(*) FROM pastes") 59 | 60 | def store(self, paste): 61 | self.logger.debug(f"Storing paste {paste.key}") 62 | 63 | try: 64 | self._insert_data(paste) 65 | except Exception as e: 66 | self.logger.debug(f"Exception '{e}'") 67 | 68 | def get(self, key): 69 | return self._get_data("key", key) 70 | -------------------------------------------------------------------------------- /pastepwn/database/sqlitedb.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pathlib 3 | import sqlite3 4 | 5 | from .abstractdb import AbstractDB 6 | 7 | 8 | class SQLiteDB(AbstractDB): 9 | """Database class representing an sqlite database instance""" 10 | 11 | def __init__(self, dbpath="pastepwn"): 12 | super().__init__() 13 | self.dbpath = pathlib.Path(dbpath) 14 | self.logger = logging.getLogger(__name__) 15 | self.logger.debug(f"Initializing SQLite - {self.dbpath}") 16 | 17 | # Check if the folder path exists 18 | if not self.dbpath.exists(): 19 | # If not, create the path and the file 20 | dbdir = self.dbpath.parent 21 | if not dbdir.exists(): 22 | dbdir.mkdir() 23 | self.dbpath.touch() 24 | elif self.dbpath.is_dir(): 25 | msg = f"'{self.dbpath}' is a directory. Use different path/name for database." 26 | raise ValueError(msg) 27 | 28 | try: 29 | self.db = sqlite3.connect(str(self.dbpath), check_same_thread=False) 30 | except Exception: 31 | self.logger.exception("An exception happened when initializing the database") 32 | raise 33 | 34 | self.db.text_factory = lambda x: str(x, "utf-8", "ignore") 35 | self.cursor = self.db.cursor() 36 | self._create_tables() 37 | 38 | self.logger.debug("Connected to database!") 39 | 40 | def _create_tables(self): 41 | self.dbpath.touch() 42 | 43 | self.cursor.execute("""CREATE TABLE IF NOT EXISTS 'pastes' ( 44 | 'key' TEXT NOT NULL UNIQUE, 45 | 'title' TEXT, 46 | 'user' TEXT, 47 | 'size' INTEGER, 48 | 'date' INTEGER, 49 | 'expire' INTEGER, 50 | 'syntax' TEXT, 51 | 'scrape_url' TEXT, 52 | 'full_url' TEXT, 53 | 'body' TEXT, 54 | PRIMARY KEY('key'))""") 55 | self.db.commit() 56 | 57 | def _insert_data(self, paste): 58 | self.cursor.execute( 59 | "INSERT INTO pastes (key, title, user, size, date, expire, syntax, scrape_url, full_url, body) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 60 | (paste.key, paste.title, paste.user, paste.size, paste.date, paste.expire, paste.syntax, paste.scrape_url, paste.full_url, paste.body), 61 | ) 62 | self.db.commit() 63 | 64 | def _update_data(self, paste): 65 | self.cursor.execute("UPDATE pastes SET body = ? WHERE key = ?", (paste.body, paste.key)) 66 | self.db.commit() 67 | 68 | def _get_data(self, key, value): 69 | pass 70 | 71 | def close_connection(self): 72 | """Closes the connection to the sqlite database""" 73 | self.cursor.close() 74 | self.db.close() 75 | 76 | def count(self, key, value): 77 | # TODO add filter to counting 78 | return self.cursor.execute("SELECT count(*) FROM pastes") 79 | 80 | def count_all(self): 81 | return self.cursor.execute("SELECT count(*) FROM pastes") 82 | 83 | def store(self, paste): 84 | self.logger.debug(f"Storing paste {paste.key}") 85 | 86 | try: 87 | self._insert_data(paste) 88 | except Exception as e: 89 | self.logger.debug(f"Exception '{e}'") 90 | if "UNIQUE constraint failed: pastes.key" in str(e): 91 | self.logger.debug("Doing upsert") 92 | self._update_data(paste) 93 | 94 | def get(self, key): 95 | return self._get_data("key", key) 96 | -------------------------------------------------------------------------------- /pastepwn/database/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-Rickyy-b/pastepwn/6832d70cb1eb9e141e49a776b69cf2844906025b/pastepwn/database/tests/__init__.py -------------------------------------------------------------------------------- /pastepwn/database/tests/mongodb_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import string 4 | import unittest 5 | 6 | from pastepwn import Paste 7 | from pastepwn.database import MongoDB 8 | 9 | 10 | @unittest.skipIf(os.environ.get("CI"), "Skipping this test on CI.") 11 | class MongoDBTest(unittest.TestCase): 12 | def setUp(self): 13 | rand_text = ["".join(random.choices(string.ascii_letters + string.digits, k=8)) for _ in range(3)] 14 | 15 | p = { 16 | "scrape_url": "https://scrape.pastebin.com/api_scrape_item.php?i=" + rand_text[0], 17 | "full_url": "https://pastebin.com/" + rand_text[0], 18 | "date": "1442911802", 19 | "key": rand_text[0], 20 | "size": "890", 21 | "expire": "1442998159", 22 | "title": "Once we all know when we goto function", 23 | "syntax": "java", 24 | "user": "admin", 25 | "body": rand_text[1:], 26 | } 27 | 28 | self.p = p 29 | self.paste = Paste(p.get("key"), p.get("title"), p.get("user"), p.get("size"), p.get("date"), p.get("expire"), p.get("syntax"), p.get("scrape_url"), p.get("full_url")) 30 | 31 | self.database = MongoDB(collectionname="pastepwn_test") 32 | 33 | def tearDown(self): 34 | self.database.db.drop_collection("pastepwn_test") 35 | 36 | def test_insert_same_key(self): 37 | # Insert a paste two times with the same body 38 | for body_text in self.p.get("body"): 39 | self.paste.set_body(body_text) 40 | self.database.store(self.paste) 41 | 42 | stored_paste = self.database.get(self.p.get("key")) 43 | comparison = self.p.get("body")[1] 44 | self.assertEqual(stored_paste.next().get("body"), comparison) 45 | 46 | 47 | if __name__ == "__main__": 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /pastepwn/database/tests/sqlite_test.py: -------------------------------------------------------------------------------- 1 | import random 2 | import shutil 3 | import string 4 | import unittest 5 | from pathlib import Path 6 | 7 | from pastepwn import Paste 8 | from pastepwn.database import SQLiteDB 9 | 10 | 11 | class SQLiteDBTest(unittest.TestCase): 12 | def setUp(self): 13 | rand_text = ["".join(random.choices(string.ascii_letters + string.digits, k=8)) for _ in range(3)] 14 | 15 | p = { 16 | "scrape_url": "https://scrape.pastebin.com/api_scrape_item.php?i=" + rand_text[0], 17 | "full_url": "https://pastebin.com/" + rand_text[0], 18 | "date": "1442911802", 19 | "key": rand_text[0], 20 | "size": "890", 21 | "expire": "1442998159", 22 | "title": "Once we all know when we goto function", 23 | "syntax": "java", 24 | "user": "admin", 25 | "body": rand_text[1:], 26 | } 27 | 28 | self.p = p 29 | self.paste = Paste(p.get("key"), p.get("title"), p.get("user"), p.get("size"), p.get("date"), p.get("expire"), p.get("syntax"), p.get("scrape_url"), p.get("full_url")) 30 | 31 | self.database = SQLiteDB(dbpath="sqlite_test/pastepwn_test") 32 | 33 | def tearDown(self): 34 | self.database.close_connection() 35 | 36 | shutil.rmtree(Path(self.database.dbpath).parent) 37 | 38 | def test_insert_same_key(self): 39 | for body_text in self.p["body"]: 40 | self.paste.set_body(body_text) 41 | self.database.store(self.paste) 42 | 43 | self.assertEqual(self.database.cursor.execute("SELECT body FROM pastes WHERE key = ?", (self.p["key"],)).fetchone()[0], self.p["body"][1]) 44 | 45 | 46 | if __name__ == "__main__": 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /pastepwn/errors/__init__.py: -------------------------------------------------------------------------------- 1 | from .errors import InvalidActionError, PastepwnError 2 | 3 | __all__ = ["InvalidActionError", "PastepwnError"] 4 | -------------------------------------------------------------------------------- /pastepwn/errors/errors.py: -------------------------------------------------------------------------------- 1 | class PastepwnError(Exception): 2 | """Representation of a pastepwn error object.""" 3 | 4 | def __init__(self, message): 5 | super().__init__(message) 6 | self.message = message 7 | 8 | def __str__(self): 9 | return str(self.message) 10 | 11 | 12 | class InvalidActionError(PastepwnError): 13 | """Representation of an error for invalid actions passed to analyzers""" 14 | 15 | pass 16 | -------------------------------------------------------------------------------- /pastepwn/errors/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-Rickyy-b/pastepwn/6832d70cb1eb9e141e49a776b69cf2844906025b/pastepwn/errors/tests/__init__.py -------------------------------------------------------------------------------- /pastepwn/errors/tests/errors_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock 3 | 4 | from pastepwn.errors.errors import PastepwnError 5 | 6 | 7 | class TestErrors(unittest.TestCase): 8 | def setUp(self): 9 | pass 10 | 11 | def test_PastepwnError(self): 12 | """Test if the PastepwnError returns its string message""" 13 | msg = "This is a test message" 14 | error = PastepwnError(msg) 15 | self.assertEqual(msg, error.message) 16 | self.assertEqual(msg, str(error)) 17 | 18 | mock = Mock() 19 | mock.__str__ = Mock(return_value="This is just another test message") 20 | error = PastepwnError(mock) 21 | self.assertEqual(mock, error.message) 22 | self.assertEqual(str(mock), str(error)) 23 | 24 | 25 | if __name__ == "__main__": 26 | unittest.main() 27 | -------------------------------------------------------------------------------- /pastepwn/scraping/__init__.py: -------------------------------------------------------------------------------- 1 | from .basicscraper import BasicScraper 2 | 3 | __all__ = ["BasicScraper"] 4 | -------------------------------------------------------------------------------- /pastepwn/scraping/basicscraper.py: -------------------------------------------------------------------------------- 1 | from threading import Event 2 | from time import sleep 3 | 4 | 5 | class BasicScraper: 6 | """Abstract class for scraper instances""" 7 | 8 | name = "BasicScraper" 9 | 10 | def __init__(self, exception_event=None): 11 | self.running = False 12 | self._stop_event = Event() 13 | self._exception_event = exception_event or Event() 14 | 15 | def init_exception_event(self, exception_event): 16 | """Sets an exception event which can be set in order to stop scraping""" 17 | self._exception_event = exception_event 18 | 19 | def start(self, paste_queue): 20 | """Starts the scraping process""" 21 | raise NotImplementedError 22 | 23 | def stop(self): 24 | """Stops the scraping process""" 25 | if self.running: 26 | self._stop_event.set() 27 | while self.running: 28 | sleep(0.1) 29 | self._stop_event.clear() 30 | -------------------------------------------------------------------------------- /pastepwn/scraping/pastebin/__init__.py: -------------------------------------------------------------------------------- 1 | from .pastebinscraper import PastebinScraper 2 | 3 | __all__ = ["PastebinScraper"] 4 | -------------------------------------------------------------------------------- /pastepwn/scraping/pastebin/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | from .ipnotregisterederror import IPNotRegisteredError 2 | from .pastedeletedexception import PasteDeletedException 3 | from .pasteemptyexception import PasteEmptyException 4 | from .pastenotreadyexception import PasteNotReadyException 5 | 6 | __all__ = ["IPNotRegisteredError", "PasteDeletedException", "PasteEmptyException", "PasteNotReadyException"] 7 | -------------------------------------------------------------------------------- /pastepwn/scraping/pastebin/exceptions/ipnotregisterederror.py: -------------------------------------------------------------------------------- 1 | class IPNotRegisteredError(Exception): 2 | """Exception class indicating that your IP is not witelisted on pastebin""" 3 | 4 | def __init__(self, ip_address): 5 | super().__init__(f"The IP you use for scraping ({ip_address}) was not whitelisted. Visit https://pastebin.com/doc_scraping_api to get access!") 6 | -------------------------------------------------------------------------------- /pastepwn/scraping/pastebin/exceptions/pastedeletedexception.py: -------------------------------------------------------------------------------- 1 | """Module for the PasteDeletedException""" 2 | 3 | 4 | class PasteDeletedException(Exception): 5 | """Exception class indicating a paste as been deleted""" 6 | 7 | pass 8 | -------------------------------------------------------------------------------- /pastepwn/scraping/pastebin/exceptions/pasteemptyexception.py: -------------------------------------------------------------------------------- 1 | """Module for the PasteEmptyException""" 2 | 3 | 4 | class PasteEmptyException(Exception): 5 | """Exception class indicating a paste is empty""" 6 | 7 | pass 8 | -------------------------------------------------------------------------------- /pastepwn/scraping/pastebin/exceptions/pastenotreadyexception.py: -------------------------------------------------------------------------------- 1 | """Module for the PasteNotReadyException""" 2 | 3 | 4 | class PasteNotReadyException(Exception): 5 | """Exception class indicating a paste is not ready for downloading yet""" 6 | 7 | pass 8 | -------------------------------------------------------------------------------- /pastepwn/scraping/pastebin/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-Rickyy-b/pastepwn/6832d70cb1eb9e141e49a776b69cf2844906025b/pastepwn/scraping/pastebin/tests/__init__.py -------------------------------------------------------------------------------- /pastepwn/scraping/pastebin/tests/pastebinscraper_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pastepwn.scraping.pastebin import PastebinScraper 4 | from pastepwn.scraping.pastebin.exceptions import IPNotRegisteredError, PasteDeletedException, PasteEmptyException, PasteNotReadyException 5 | 6 | 7 | class TestPastebinscraper(unittest.TestCase): 8 | def setUp(self) -> None: 9 | self.pastebinscraper = PastebinScraper() 10 | 11 | def test_empty(self): 12 | with self.assertRaises(PasteEmptyException): 13 | self.pastebinscraper._check_error("") 14 | 15 | def test_not_ready(self): 16 | with self.assertRaises(PasteNotReadyException): 17 | self.pastebinscraper._check_error("File is not ready for scraping yet. Try again in 1 minute.") 18 | 19 | def test_deleted(self): 20 | with self.assertRaises(PasteDeletedException): 21 | self.pastebinscraper._check_error("Error, we cannot find this paste.") 22 | 23 | def _check_ip_not_registered(self, ip_list): 24 | shell = "YOUR IP: {} DOES NOT HAVE ACCESS. VISIT: https://pastebin.com/doc_scraping_api TO GET ACCESS!" 25 | for ip in ip_list: 26 | with self.assertRaises(IPNotRegisteredError): 27 | self.pastebinscraper._check_error(shell.format(ip)) 28 | print(f"The following IP was not detected: {ip}") 29 | 30 | def test_ipv4_not_registered(self): 31 | """Test if the _check_error method detects different IPv4 addresses. It's okay to also detect invalid addresses where an octed is > 255)""" 32 | ipv4_test = [ 33 | "1.1.1.1", 34 | "10.1.5.6", 35 | "1.10.5.6", 36 | "1.1.50.6", 37 | "1.1.5.60", 38 | "1.1.50.60", 39 | "1.10.50.60", 40 | "10.10.50.60", 41 | "10.10.50.255", 42 | "10.10.255.255", 43 | "10.255.255.255", 44 | "255.255.255.255", 45 | "333.333.333.333", 46 | ] 47 | 48 | self._check_ip_not_registered(ipv4_test) 49 | 50 | def test_ipv6_not_registered(self): 51 | ipv6_test = [ 52 | "fe80::21d8:f50:c295:c4be", 53 | "2001:cdba:0000:0000:0000:0000:3257:9652", 54 | "2001:cdba:0:0:0:0:3257:9652", 55 | "2001:cdba::3257:9652", 56 | "2001:cdba::1222", 57 | "21DA:D3:0:2F3B:2AA:FF:FE28:9C5A", 58 | "2001:cdba::1:2:3:3257:9652", 59 | "FE80::8329", 60 | "FE80::FFFF:8329", 61 | "FE80::B3FF:FFFF:8329", 62 | "FE80::0202:B3FF:FFFF:8329", 63 | "FE80:0000:0000:0000:0202:B3FF:FFFF:8329", 64 | ] 65 | # TODO: IPv6 addresses with double colon AND full zero groups (of 16 bits) are currently not recognized by the used regex. An example address would 66 | # be: `FE80::0000:0000:0202:B3FF:FFFF:8329` 67 | 68 | self._check_ip_not_registered(ipv6_test) 69 | 70 | 71 | if __name__ == "__main__": 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /pastepwn/util/__init__.py: -------------------------------------------------------------------------------- 1 | from .dictwrapper import DictWrapper 2 | from .listify import listify 3 | from .network import enforce_ip_version 4 | from .request import Request 5 | from .templatingengine import TemplatingEngine 6 | from .threadingutils import join_threads, start_thread 7 | 8 | __all__ = ["DictWrapper", "Request", "TemplatingEngine", "enforce_ip_version", "join_threads", "listify", "start_thread"] 9 | -------------------------------------------------------------------------------- /pastepwn/util/dictwrapper.py: -------------------------------------------------------------------------------- 1 | # https://stackoverflow.com/questions/19799609/leaving-values-blank-if-not-passed-in-str-format 2 | class DictWrapper(dict): 3 | """A wrapper around dicts which returns the key as string when missing. 4 | Used for the templating engine 5 | """ 6 | 7 | def __missing__(self, key): 8 | return f"${{{key}}}" 9 | -------------------------------------------------------------------------------- /pastepwn/util/listify.py: -------------------------------------------------------------------------------- 1 | def listify(obj): 2 | """Make sure the given object is a list 3 | 4 | :param obj: Any object - either None, a list of objects or a single object 5 | :return: The given object formatted as list 6 | """ 7 | if obj is None: 8 | # When the object is None, an empty list will be returned 9 | return [] 10 | elif isinstance(obj, list): 11 | # When the object is already a list, that list will be returned 12 | return obj 13 | 14 | # When a single object is passed to the method, a list with the 15 | # object as single item will be returned 16 | return [obj] 17 | -------------------------------------------------------------------------------- /pastepwn/util/network.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import socket 3 | 4 | import requests.packages.urllib3.util.connection as urllib3_cn 5 | 6 | 7 | def _allowed_gai_family_4(): 8 | """https://github.com/urllib3/urllib3/blob/master/src/urllib3/util/connection.py""" 9 | return socket.AF_INET 10 | 11 | 12 | def _allowed_gai_family_6(): 13 | """https://github.com/urllib3/urllib3/blob/master/src/urllib3/util/connection.py""" 14 | return socket.AF_INET6 15 | 16 | 17 | def _allowed_gai_family_unspec(): 18 | """https://github.com/urllib3/urllib3/blob/master/src/urllib3/util/connection.py""" 19 | return socket.AF_UNSPEC 20 | 21 | 22 | def enforce_ip_version(version=None): 23 | """ 24 | Tries to enforce either IPv4 or IPv6, depending on the version parameter 25 | :param version: The IP version to use 26 | :return: 27 | """ 28 | if version is None: 29 | urllib3_cn.allowed_gai_family = _allowed_gai_family_unspec 30 | return 31 | 32 | version = str(version) 33 | 34 | if version == "4": 35 | logging.info("Enforcing IPv4!") 36 | urllib3_cn.allowed_gai_family = _allowed_gai_family_4 37 | 38 | elif version == "6": 39 | logging.info("Enforcing IPv6!") 40 | if not urllib3_cn.HAS_IPV6: 41 | raise Exception("Your system can't handle IPv6!") 42 | 43 | urllib3_cn.allowed_gai_family = _allowed_gai_family_6 44 | 45 | elif version == "5": 46 | raise ValueError("Internet Stream Protocol? Really? There is no actual IPv5!") 47 | 48 | else: 49 | raise ValueError("No valid value specified for 'version' parameter! Please use either 4 or 6") 50 | -------------------------------------------------------------------------------- /pastepwn/util/request.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from threading import Lock 3 | 4 | from requests import Session, utils 5 | from requests.exceptions import Timeout 6 | 7 | 8 | class Request: 9 | _instance = None 10 | _initialized = False 11 | _lock = Lock() 12 | 13 | def __new__(cls, *args, **kwargs): 14 | # override method to implement singleton 15 | # source: http://alacret.blogspot.com/2015/04/python-thread-safe-singleton-pattern.html 16 | if Request._instance is None: 17 | with Request._lock: 18 | if Request._instance is None: 19 | Request._instance = super().__new__(cls) 20 | return Request._instance 21 | 22 | def __init__(self, proxies=None, headers=None): 23 | if not self._initialized: 24 | self.logger = logging.getLogger(__name__) 25 | self.session = Session() 26 | self.proxies = proxies 27 | self.headers = headers 28 | self.logger.info("Using the following custom proxies: %s", proxies) 29 | system_proxies = utils.get_environ_proxies("https://example.com") 30 | self.logger.info("Using the following system proxies: %s", system_proxies) 31 | Request._initialized = True 32 | 33 | def _request_wrapper(self, data, timeout, *args, **kwargs): 34 | headers = {"User-Agent": "pastepwn (https://github.com/d-Rickyy-b/pastepwn)"} 35 | 36 | if self.headers is not None: 37 | headers.update(self.headers) 38 | 39 | try: 40 | response = self.session.request(*args, headers=headers, proxies=self.proxies, data=data, timeout=timeout, **kwargs) 41 | except Timeout: 42 | url = kwargs.get("url") 43 | self.logger.warning("Timeout while requesting %s!", url) 44 | return "" 45 | 46 | return response.content.decode("utf-8") 47 | 48 | def get(self, url, data=None, timeout=5): 49 | return self._request_wrapper(method="GET", url=url, data=data, timeout=timeout) 50 | 51 | def post(self, url, data=None, timeout=5): 52 | return self._request_wrapper(method="POST", url=url, data=data, timeout=timeout) 53 | 54 | def put(self, url, data=None, timeout=5): 55 | return self._request_wrapper(method="PUT", url=url, data=data, timeout=timeout) 56 | 57 | def delete(self, url, data=None, timeout=5): 58 | return self._request_wrapper(method="DELETE", url=url, data=data, timeout=timeout) 59 | -------------------------------------------------------------------------------- /pastepwn/util/templatingengine.py: -------------------------------------------------------------------------------- 1 | from string import Template 2 | 3 | from pastepwn.util import DictWrapper 4 | 5 | 6 | class TemplatingEngine: 7 | """Wrapper class around the python templating feature""" 8 | 9 | @staticmethod 10 | def fill_template(paste, analyzer_name, template_string, matches=None, **kwargs): 11 | """ 12 | Returns a templated text with paste contents inserted into the template string 13 | Use ${key_name} in the template_string to insert paste contents into it 14 | :param paste: A paste which serves as the source for template filling 15 | :param analyzer_name: Name of the analyzer 16 | :param template_string: A template string describing how the variables should be filled in 17 | :param matches: A list of matches that was returned from the analyzer 18 | :return: Filled template 19 | """ 20 | paste_dict = paste.to_dict() 21 | paste_dict["analyzer_name"] = analyzer_name 22 | 23 | if matches is None: 24 | paste_dict["matches"] = "" 25 | else: 26 | # When there are elements in the matches object, we want them to be formatted as single string 27 | matches_str = "\n".join(matches) 28 | paste_dict["matches"] = matches_str 29 | 30 | # Possibility to insert own/custom values into the paste_dict thus gives more control over the template string 31 | for name, value in kwargs.items(): 32 | paste_dict[name] = value 33 | 34 | # Fallback if the template string is empty or non existent 35 | if template_string is None or template_string == "": 36 | template_string = "New paste matched by analyzer '${analyzer_name}' - Link: ${full_url}\n\nMatches:\n${matches}" 37 | 38 | template = Template(template_string) 39 | return template.safe_substitute(DictWrapper(paste_dict)) 40 | -------------------------------------------------------------------------------- /pastepwn/util/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-Rickyy-b/pastepwn/6832d70cb1eb9e141e49a776b69cf2844906025b/pastepwn/util/tests/__init__.py -------------------------------------------------------------------------------- /pastepwn/util/tests/dictwrapper_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pastepwn.util.dictwrapper import DictWrapper 4 | 5 | 6 | class TestDictwrapper(unittest.TestCase): 7 | def setUp(self): 8 | self.test_dict = {"key1": "value1", "key2": "value2"} 9 | 10 | def test_existing_key(self): 11 | test_wrapped = DictWrapper(self.test_dict) 12 | self.assertEqual(test_wrapped["key1"], "value1") 13 | 14 | def test_non_existing_key(self): 15 | test_wrapped = DictWrapper(self.test_dict) 16 | # Make sure that retreiving a valid key works 17 | self.assertEqual(test_wrapped["key1"], "value1") 18 | # Check if retreiving a nonexistent value works as expected 19 | self.assertEqual(test_wrapped["key3"], "${key3}") 20 | # Make sure that retreiving a valid key still works 21 | self.assertEqual(test_wrapped["key2"], "value2") 22 | 23 | 24 | if __name__ == "__main__": 25 | unittest.main() 26 | -------------------------------------------------------------------------------- /pastepwn/util/tests/listify_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock 3 | 4 | from pastepwn.util.listify import listify 5 | 6 | 7 | class ListifyTest(unittest.TestCase): 8 | def test_None(self): 9 | self.assertEqual([], listify(None), "Listify did not return empty list!") 10 | 11 | def test_list(self): 12 | obj = Mock() 13 | obj2 = Mock() 14 | obj3 = Mock() 15 | obj_list = [obj, obj2, obj3] 16 | 17 | self.assertEqual(obj_list, listify(obj_list), "Listify did not return the given list!") 18 | 19 | def test_single(self): 20 | obj = Mock() 21 | self.assertEqual([obj], listify(obj), "Listify did not return single object as list!") 22 | 23 | 24 | if __name__ == "__main__": 25 | unittest.main() 26 | -------------------------------------------------------------------------------- /pastepwn/util/tests/network_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | import unittest 4 | from unittest.mock import Mock 5 | 6 | import requests.packages.urllib3.util.connection as urllib3_cn 7 | 8 | from pastepwn.util import enforce_ip_version 9 | 10 | 11 | @unittest.skipIf("TRAVIS" in os.environ and os.environ["TRAVIS"] == "true", "Skipping this test on Travis CI.") 12 | class TestNetwork(unittest.TestCase): 13 | """Tests for the network utils - these tests might fail on CI""" 14 | 15 | def test_enforce_ip_version_4(self): 16 | enforce_ip_version(4) 17 | self.assertEqual(urllib3_cn.allowed_gai_family(), socket.AF_INET) 18 | enforce_ip_version("4") 19 | self.assertEqual(urllib3_cn.allowed_gai_family(), socket.AF_INET) 20 | 21 | def test_enforce_ip_version_5(self): 22 | with self.assertRaises(Exception): 23 | enforce_ip_version(5) 24 | 25 | with self.assertRaises(Exception): 26 | enforce_ip_version("5") 27 | 28 | def test_enforce_ip_version_6(self): 29 | enforce_ip_version(6) 30 | self.assertEqual(urllib3_cn.allowed_gai_family(), socket.AF_INET6) 31 | enforce_ip_version("6") 32 | self.assertEqual(urllib3_cn.allowed_gai_family(), socket.AF_INET6) 33 | 34 | def test_enforce_ip_version_None(self): 35 | enforce_ip_version(None) 36 | self.assertEqual(urllib3_cn.allowed_gai_family(), socket.AF_UNSPEC) 37 | 38 | enforce_ip_version() 39 | self.assertEqual(urllib3_cn.allowed_gai_family(), socket.AF_UNSPEC) 40 | 41 | def test_enforce_ip_version_obj(self): 42 | mock = Mock() 43 | with self.assertRaises(ValueError): 44 | enforce_ip_version(mock) 45 | 46 | 47 | if __name__ == "__main__": 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /pastepwn/util/tests/templatingengine_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pastepwn import Paste 4 | from pastepwn.util.templatingengine import TemplatingEngine 5 | 6 | 7 | class TestTemplatingEngine(unittest.TestCase): 8 | def setUp(self): 9 | """Sets up the test case""" 10 | p = { 11 | "scrape_url": "https://scrape.pastebin.com/api_scrape_item.php?i=0CeaNm8Y", 12 | "full_url": "https://pastebin.com/0CeaNm8Y", 13 | "date": "1442911802", 14 | "key": "0CeaNm8Y", 15 | "size": "890", 16 | "expire": "1442998159", 17 | "title": "Once we all know when we goto function", 18 | "syntax": "java", 19 | "user": "admin", 20 | "body": "This is a test for pastepwn", 21 | } 22 | 23 | self.p = p 24 | self.paste = Paste(p.get("key"), p.get("title"), p.get("user"), p.get("size"), p.get("date"), p.get("expire"), p.get("syntax"), p.get("scrape_url"), p.get("full_url")) 25 | 26 | def test_fill_template(self): 27 | """Checks if templating engine inserts paste data correctly into the template""" 28 | analyzer_name = "TestAnalyzer" 29 | template = "Matched paste '${key}' by analyzer '${analyzer_name}'. URL is: '${full_url}'" 30 | expected = "Matched paste '{}' by analyzer '{}'. URL is: '{}'".format(self.p.get("key"), analyzer_name, self.p.get("full_url")) 31 | result = TemplatingEngine.fill_template(paste=self.paste, analyzer_name=analyzer_name, template_string=template) 32 | self.assertEqual(expected, result, msg="Filled template string is not the same as the expected result!") 33 | 34 | def test_fill_template_matches(self): 35 | """Checks if templating engine inserts the matches correctly into the template""" 36 | template = "Matches are: ${matches}" 37 | expected = "Matches are: +123456789\n+987654321" 38 | matches = ["+123456789", "+987654321"] 39 | result = TemplatingEngine.fill_template(paste=self.paste, analyzer_name=None, template_string=template, matches=matches) 40 | self.assertEqual(expected, result, msg="Filled template string is not the same as the expected result!") 41 | 42 | def test_fill_template_kwarg(self): 43 | """Checks if templating engine inserts arbitrary data via kwargs into the template""" 44 | template = "Completely new parameter ${random_param} unrelated to paste data can be ${ins} into this string" 45 | expected = "Completely new parameter 'pastepwnIsCool' unrelated to paste data can be inserted into this string" 46 | 47 | result = TemplatingEngine.fill_template(paste=self.paste, analyzer_name=None, template_string=template, random_param="'pastepwnIsCool'", ins="inserted") 48 | self.assertEqual(expected, result, msg="Filled template string is not the same as the expected result!") 49 | 50 | def test_fill_template_missing_param(self): 51 | """Checks if templating engine correctly handles nonexistent params""" 52 | template = "The nonexistent parameter ${i_do_not_exist} stays the same!" 53 | expected = "The nonexistent parameter ${i_do_not_exist} stays the same!" 54 | 55 | result = TemplatingEngine.fill_template(paste=self.paste, analyzer_name=None, template_string=template) 56 | self.assertEqual(expected, result, msg="Filled template string is not the same as the expected result!") 57 | 58 | if __name__ == "__main__": 59 | unittest.main() 60 | -------------------------------------------------------------------------------- /pastepwn/util/threadingutils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from threading import Thread, current_thread 3 | 4 | 5 | def start_thread(target, name, exception_event, *args, **kwargs): 6 | """ 7 | Starts a thread passed as argument and catches exceptions that happens during execution 8 | :param target: Method to be executed in the thread 9 | :param name: Name of the thread 10 | :param exception_event: An event that will be set if an exception occurred 11 | :param args: Arguments to be passed to the threaded method 12 | :param kwargs: Keyword-Arguments to be passed to the threaded method 13 | :return: 14 | """ 15 | thread = Thread(target=thread_wrapper, name=name, args=(target, exception_event, *args), kwargs=kwargs) 16 | thread.start() 17 | return thread 18 | 19 | 20 | def thread_wrapper(target, exception_event, *args, **kwargs): 21 | """ 22 | Wrapper around the execution of a passed method, that catches and logs exceptions 23 | :param target: Method to be executed 24 | :param exception_event: An event that will be set if an exception occurred 25 | :param args: Arguments to be passed to the target method 26 | :param kwargs: Keyword-Arguments to be passed to the target method 27 | :return: 28 | """ 29 | thread_name = current_thread().name 30 | logger = logging.getLogger(__name__) 31 | logger.debug(f"{thread_name} - thread started") 32 | try: 33 | target(*args, **kwargs) 34 | except Exception: 35 | exception_event.set() 36 | logger.exception("unhandled exception in %s", thread_name) 37 | raise 38 | logger.debug(f"{thread_name} - thread ended") 39 | 40 | 41 | def join_threads(threads): 42 | """ 43 | End all threads and join them back into the main thread 44 | :param threads: List of threads to be joined 45 | :return: 46 | """ 47 | logger = logging.getLogger(__name__) 48 | for thread in threads: 49 | logger.debug(f"Joining thread {thread.name}") 50 | thread.join() 51 | logger.debug(f"Thread {thread.name} has ended") 52 | -------------------------------------------------------------------------------- /pastepwn/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.1.0" 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pymongo>=3.11.3 2 | mysql-connector-python>=8.0.24 3 | requests>=2.31.0 4 | python-twitter>=3.5 5 | websockets>=9.1,<10 6 | -------------------------------------------------------------------------------- /requirements_minimum.txt: -------------------------------------------------------------------------------- 1 | requests>=2.31.0 2 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Starter script for the pastepwn docker image 3 | 4 | # If the user provided file does not exist, print an error 5 | if [ ! -f /pastepwn/start.py ]; then 6 | echo "The file 'start.py' wasn't found!" 7 | exit 1 8 | else 9 | python3 /pastepwn/start.py 10 | fi --------------------------------------------------------------------------------