diff --git a/.flake8 b/.flake8 deleted file mode 100644 index a6f057338b..0000000000 --- a/.flake8 +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -select = F,E722 -ignore = F403,F405,F541 -per-file-ignores = - */__init__.py:F401,F403 diff --git a/.gitattributes b/.gitattributes index 49edcb7119..00bf2637dc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,4 +5,4 @@ *.txt text eol=lf *.json text eol=lf *.md text eol=lf -*.sh text eol=lf \ No newline at end of file +*.sh text eol=lf diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bd3b2c06f0..1ad88ccb68 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,3 +6,12 @@ updates: interval: "weekly" target-branch: "dev" open-pull-requests-limit: 10 + - package-ecosystem: github-actions + directory: / + groups: + github-actions: + patterns: + - "*" # Group all Actions updates into a single larger pull request + schedule: + interval: weekly + target-branch: "dev" diff --git a/.github/workflows/distro_tests.yml b/.github/workflows/distro_tests.yml index 95f9d7b5f0..4e3f268d72 100644 --- a/.github/workflows/distro_tests.yml +++ b/.github/workflows/distro_tests.yml @@ -24,17 +24,17 @@ jobs: if [ "$ID" = "ubuntu" ] || [ "$ID" = "debian" ] || [ "$ID" = "kali" ] || [ "$ID" = "parrotsec" ]; then export DEBIAN_FRONTEND=noninteractive apt-get update - apt-get -y install curl git bash build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev + apt-get -y install curl git bash build-essential docker.io libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev elif [ "$ID" = "alpine" ]; then - apk add --no-cache bash gcc g++ musl-dev libffi-dev curl git make openssl-dev bzip2-dev zlib-dev xz-dev sqlite-dev + apk add --no-cache bash gcc g++ musl-dev libffi-dev docker curl git make openssl-dev bzip2-dev zlib-dev xz-dev sqlite-dev elif [ "$ID" = "arch" ]; then - pacman -Syu --noconfirm curl git bash base-devel + pacman -Syu --noconfirm curl docker git bash base-devel elif [ "$ID" = "fedora" ]; then - dnf install -y curl git bash gcc make openssl-devel bzip2-devel libffi-devel zlib-devel xz-devel tk-devel gdbm-devel readline-devel sqlite-devel python3-libdnf5 + dnf install -y curl docker git bash gcc make openssl-devel bzip2-devel libffi-devel zlib-devel xz-devel tk-devel gdbm-devel readline-devel sqlite-devel python3-libdnf5 elif [ "$ID" = "gentoo" ]; then echo "media-libs/libglvnd X" >> /etc/portage/package.use/libglvnd emerge-webrsync - emerge --update --newuse dev-vcs/git media-libs/mesa curl bash + emerge --update --newuse dev-vcs/git media-libs/mesa curl docker bash fi fi diff --git a/.github/workflows/docs_updater.yml b/.github/workflows/docs_updater.yml index 365d92a98c..a63d0987a6 100644 --- a/.github/workflows/docs_updater.yml +++ b/.github/workflows/docs_updater.yml @@ -30,5 +30,5 @@ jobs: token: ${{ secrets.BBOT_DOCS_UPDATER_PAT }} branch: update-docs base: dev - title: "Daily Docs Update" + title: "Automated Docs Update" body: "This is an automated pull request to update the documentation." diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 507b7ac547..b4efe9fdbf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,19 +15,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: psf/black@stable - with: - options: "--check" - - name: Install Python 3 - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - name: Install dependencies - run: | - pip install flake8 - - name: flake8 - run: | - flake8 + - run: | + pipx install ruff + ruff check + ruff format test: needs: lint runs-on: ubuntu-latest @@ -48,7 +39,7 @@ jobs: poetry install - name: Run tests run: | - poetry run pytest --exitfirst --reruns 2 -o timeout_func_only=true --timeout 1200 --disable-warnings --log-cli-level=INFO --cov-config=bbot/test/coverage.cfg --cov-report xml:cov.xml --cov=bbot . + poetry run pytest -vv --exitfirst --reruns 2 -o timeout_func_only=true --timeout 1200 --disable-warnings --log-cli-level=INFO --cov-config=bbot/test/coverage.cfg --cov-report xml:cov.xml --cov=bbot . - name: Upload Debug Logs uses: actions/upload-artifact@v3 with: diff --git a/.github/workflows/version_updater.yml b/.github/workflows/version_updater.yml index bb149820cf..81f4490514 100644 --- a/.github/workflows/version_updater.yml +++ b/.github/workflows/version_updater.yml @@ -9,13 +9,13 @@ jobs: update-nuclei-version: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: dev fetch-depth: 0 token: ${{ secrets.BBOT_DOCS_UPDATER_PAT }} - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies @@ -44,7 +44,7 @@ jobs: run: "sed -i '0,/\"version\": \".*\",/ s/\"version\": \".*\",/\"version\": \"${{ env.latest_version }}\",/g' bbot/modules/deadly/nuclei.py" - name: Create pull request to update the version if: steps.update-version.outcome == 'success' - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v7 with: token: ${{ secrets.BBOT_DOCS_UPDATER_PAT }} commit-message: "Update nuclei" @@ -61,13 +61,13 @@ jobs: update-trufflehog-version: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: dev fetch-depth: 0 token: ${{ secrets.BBOT_DOCS_UPDATER_PAT }} - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies @@ -96,7 +96,7 @@ jobs: run: "sed -i '0,/\"version\": \".*\",/ s/\"version\": \".*\",/\"version\": \"${{ env.latest_version }}\",/g' bbot/modules/trufflehog.py" - name: Create pull request to update the version if: steps.update-version.outcome == 'success' - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v7 with: token: ${{ secrets.BBOT_DOCS_UPDATER_PAT }} commit-message: "Update trufflehog" @@ -109,4 +109,4 @@ jobs: branch: "update-trufflehog" committer: blsaccess author: blsaccess - assignees: "TheTechromancer" \ No newline at end of file + assignees: "TheTechromancer" diff --git a/.gitmodules b/.gitmodules index 0033a29676..c85f090f5f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "bbot/modules/playground"] path = bbot/modules/playground url = https://github.com/blacklanternsecurity/bbot-module-playground - branch = main \ No newline at end of file + branch = main diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..d6643f2ad3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,48 @@ +# Learn more about this config here: https://pre-commit.com/ + +# To enable these pre-commit hooks run: +# `pipx install pre-commit` or `brew install pre-commit` +# Then in the project root directory run `pre-commit install` + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-added-large-files + - id: check-ast + - id: check-builtin-literals + - id: check-byte-order-marker + - id: check-case-conflict + # - id: check-docstring-first + # - id: check-executables-have-shebangs + - id: check-json + - id: check-merge-conflict + # - id: check-shebang-scripts-are-executable + - id: check-symlinks + - id: check-toml + - id: check-vcs-permalinks + - id: check-xml + # - id: check-yaml + - id: debug-statements + - id: destroyed-symlinks + # - id: detect-private-key + - id: end-of-file-fixer + - id: file-contents-sorter + - id: fix-byte-order-marker + - id: forbid-new-submodules + - id: forbid-submodules + - id: mixed-line-ending + - id: requirements-txt-fixer + - id: sort-simple-yaml + - id: trailing-whitespace + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.0 + hooks: + - id: ruff + - id: ruff-format + + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.23 + hooks: + - id: validate-pyproject diff --git a/README.md b/README.md index 50e26da26b..ee5790ee5d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![bbot_banner](https://github.com/user-attachments/assets/f02804ce-9478-4f1e-ac4d-9cf5620a3214)](https://github.com/blacklanternsecurity/bbot) -[![Python Version](https://img.shields.io/badge/python-3.9+-FF8400)](https://www.python.org) [![License](https://img.shields.io/badge/license-GPLv3-FF8400.svg)](https://github.com/blacklanternsecurity/bbot/blob/dev/LICENSE) [![DEF CON Recon Village 2024](https://img.shields.io/badge/DEF%20CON%20Demo%20Labs-2023-FF8400.svg)](https://www.reconvillage.org/talks) [![PyPi Downloads](https://static.pepy.tech/personalized-badge/bbot?right_color=orange&left_color=grey)](https://pepy.tech/project/bbot) [![Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Tests](https://github.com/blacklanternsecurity/bbot/actions/workflows/tests.yml/badge.svg?branch=stable)](https://github.com/blacklanternsecurity/bbot/actions?query=workflow%3A"tests") [![Codecov](https://codecov.io/gh/blacklanternsecurity/bbot/branch/dev/graph/badge.svg?token=IR5AZBDM5K)](https://codecov.io/gh/blacklanternsecurity/bbot) [![Discord](https://img.shields.io/discord/859164869970362439)](https://discord.com/invite/PZqkgxu5SA) +[![Python Version](https://img.shields.io/badge/python-3.9+-FF8400)](https://www.python.org) [![License](https://img.shields.io/badge/license-GPLv3-FF8400.svg)](https://github.com/blacklanternsecurity/bbot/blob/dev/LICENSE) [![DEF CON Recon Village 2024](https://img.shields.io/badge/DEF%20CON%20Demo%20Labs-2023-FF8400.svg)](https://www.reconvillage.org/talks) [![PyPi Downloads](https://static.pepy.tech/personalized-badge/bbot?right_color=orange&left_color=grey)](https://pepy.tech/project/bbot) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Tests](https://github.com/blacklanternsecurity/bbot/actions/workflows/tests.yml/badge.svg?branch=stable)](https://github.com/blacklanternsecurity/bbot/actions?query=workflow%3A"tests") [![Codecov](https://codecov.io/gh/blacklanternsecurity/bbot/branch/dev/graph/badge.svg?token=IR5AZBDM5K)](https://codecov.io/gh/blacklanternsecurity/bbot) [![Discord](https://img.shields.io/discord/859164869970362439)](https://discord.com/invite/PZqkgxu5SA) ### **BEEĀ·bot** is a multipurpose scanner inspired by [Spiderfoot](https://github.com/smicallef/spiderfoot), built to automate your **Recon**, **Bug Bounties**, and **ASM**! @@ -91,6 +91,10 @@ description: Recursive web spider modules: - httpx +blacklist: + # Prevent spider from invalidating sessions by logging out + - "RE:/.*(sign|log)[_-]?out" + config: web: # how many links to follow in a row @@ -191,10 +195,10 @@ flags: ```bash # everything everywhere all at once -bbot -t evilcorp.com -p kitchen-sink +bbot -t evilcorp.com -p kitchen-sink --allow-deadly # roughly equivalent to: -bbot -t evilcorp.com -p subdomain-enum cloud-enum code-enum email-enum spider web-basic paramminer dirbust-light web-screenshots +bbot -t evilcorp.com -p subdomain-enum cloud-enum code-enum email-enum spider web-basic paramminer dirbust-light web-screenshots --allow-deadly ``` @@ -222,8 +226,6 @@ config: baddns: enable_references: True - - ``` @@ -236,6 +238,24 @@ Click the graph below to explore the [inner workings](https://www.blacklanternse [![image](https://github.com/blacklanternsecurity/bbot/assets/20261699/e55ba6bd-6d97-48a6-96f0-e122acc23513)](https://www.blacklanternsecurity.com/bbot/Stable/how_it_works/) +## Output Modules + +- [Neo4j](docs/scanning/output.md#neo4j) +- [Teams](docs/scanning/output.md#teams) +- [Discord](docs/scanning/output.md#discord) +- [Slack](docs/scanning/output.md#slack) +- [Postgres](docs/scanning/output.md#postgres) +- [MySQL](docs/scanning/output.md#mysql) +- [SQLite](docs/scanning/output.md#sqlite) +- [Splunk](docs/scanning/output.md#splunk) +- [Elasticsearch](docs/scanning/output.md#elasticsearch) +- [CSV](docs/scanning/output.md#csv) +- [JSON](docs/scanning/output.md#json) +- [HTTP](docs/scanning/output.md#http) +- [Websocket](docs/scanning/output.md#websocket) + +...and [more](docs/scanning/output.md)! + ## BBOT as a Python Library #### Synchronous @@ -297,6 +317,11 @@ Targets can be any of the following: - `IP_RANGE` (`1.2.3.0/24`) - `OPEN_TCP_PORT` (`192.168.0.1:80`) - `URL` (`https://www.evilcorp.com`) +- `EMAIL_ADDRESS` (`bob@evilcorp.com`) +- `ORG_STUB` (`ORG:evilcorp`) +- `USER_STUB` (`USER:bobsmith`) +- `FILESYSTEM` (`FILESYSTEM:/tmp/asdf`) +- `MOBILE_APP` (`MOBILE_APP:https://play.google.com/store/apps/details?id=com.evilcorp.app`) For more information, see [Targets](https://www.blacklanternsecurity.com/bbot/Stable/scanning/#targets-t). To learn how BBOT handles scope, see [Scope](https://www.blacklanternsecurity.com/bbot/Stable/scanning/#scope). diff --git a/bbot-docker.sh b/bbot-docker.sh index e4e0bb9e43..3db958f94a 100755 --- a/bbot-docker.sh +++ b/bbot-docker.sh @@ -1,2 +1,3 @@ -# run the docker image -docker run --rm -it -v "$HOME/.bbot:/root/.bbot" -v "$HOME/.config/bbot:/root/.config/bbot" blacklanternsecurity/bbot:stable "$@" +# OUTPUTS SCAN DATA TO ~/.bbot/scans + +docker run --rm -it -v "$HOME/.bbot/scans:/root/.bbot/scans" -v "$HOME/.config/bbot:/root/.config/bbot" blacklanternsecurity/bbot:stable "$@" diff --git a/bbot/cli.py b/bbot/cli.py index 877f2bcaa5..db595dfc1d 100755 --- a/bbot/cli.py +++ b/bbot/cli.py @@ -29,7 +29,6 @@ async def _main(): - import asyncio import traceback from contextlib import suppress @@ -45,7 +44,6 @@ async def _main(): global scan_name try: - # start by creating a default scan preset preset = Preset(_log=True, name="bbot_cli_main") # parse command line arguments and merge into preset @@ -80,8 +78,7 @@ async def _main(): return # if we're listing modules or their options - if options.list_modules or options.list_module_options: - + if options.list_modules or options.list_output_modules or options.list_module_options: # if no modules or flags are specified, enable everything if not (options.modules or options.output_modules or options.flags): for module, preloaded in preset.module_loader.preloaded().items(): @@ -99,7 +96,17 @@ async def _main(): print("") print("### MODULES ###") print("") - for row in preset.module_loader.modules_table(preset.modules).splitlines(): + modules = sorted(set(preset.scan_modules + preset.internal_modules)) + for row in preset.module_loader.modules_table(modules).splitlines(): + print(row) + return + + # --list-output-modules + if options.list_output_modules: + print("") + print("### OUTPUT MODULES ###") + print("") + for row in preset.module_loader.modules_table(preset.output_modules).splitlines(): print(row) return @@ -133,8 +140,8 @@ async def _main(): ] if deadly_modules and not options.allow_deadly: log.hugewarning(f"You enabled the following deadly modules: {','.join(deadly_modules)}") - log.hugewarning(f"Deadly modules are highly intrusive") - log.hugewarning(f"Please specify --allow-deadly to continue") + log.hugewarning("Deadly modules are highly intrusive") + log.hugewarning("Please specify --allow-deadly to continue") return False # --current-preset @@ -172,9 +179,8 @@ async def _main(): log.trace(f"Command: {' '.join(sys.argv)}") if sys.stdin.isatty(): - # warn if any targets belong directly to a cloud provider - for event in scan.target.events: + for event in scan.target.seeds.events: if event.type == "DNS_NAME": cloudcheck_result = scan.helpers.cloudcheck(event.host) if cloudcheck_result: @@ -254,9 +260,7 @@ async def akeyboard_listen(): finally: # save word cloud with suppress(BaseException): - save_success, filename = scan.helpers.word_cloud.save() - if save_success: - log_to_stderr(f"Saved word cloud ({len(scan.helpers.word_cloud):,} words) to {filename}") + scan.helpers.word_cloud.save() # remove output directory if empty with suppress(BaseException): scan.home.rmdir() diff --git a/bbot/core/config/files.py b/bbot/core/config/files.py index c66e92116d..2be7bbaa1a 100644 --- a/bbot/core/config/files.py +++ b/bbot/core/config/files.py @@ -10,7 +10,6 @@ class BBOTConfigFiles: - config_dir = (Path.home() / ".config" / "bbot").resolve() defaults_filename = (bbot_code_dir / "defaults.yml").resolve() config_filename = (config_dir / "bbot.yml").resolve() diff --git a/bbot/core/config/logger.py b/bbot/core/config/logger.py index 2e42ef8dec..54866a63b6 100644 --- a/bbot/core/config/logger.py +++ b/bbot/core/config/logger.py @@ -1,3 +1,4 @@ +import os import sys import atexit import logging @@ -9,6 +10,7 @@ from ..helpers.misc import mkdir, error_and_exit from ...logger import colorize, loglevel_mapping +from ..multiprocess import SHARED_INTERPRETER_STATE debug_format = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s %(filename)s:%(lineno)s %(message)s") @@ -65,8 +67,9 @@ def __init__(self, core): self.listener = None - self.process_name = multiprocessing.current_process().name - if self.process_name == "MainProcess": + # if we haven't set up logging yet, do it now + if "_BBOT_LOGGING_SETUP" not in os.environ: + os.environ["_BBOT_LOGGING_SETUP"] = "1" self.queue = multiprocessing.Queue() self.setup_queue_handler() # Start the QueueListener @@ -113,7 +116,7 @@ def setup_queue_handler(self, logging_queue=None, log_level=logging.DEBUG): self.core_logger.setLevel(log_level) # disable asyncio logging for child processes - if self.process_name != "MainProcess": + if not SHARED_INTERPRETER_STATE.is_main_process: logging.getLogger("asyncio").setLevel(logging.ERROR) def addLoggingLevel(self, levelName, levelNum, methodName=None): diff --git a/bbot/core/core.py b/bbot/core/core.py index 47831af25d..5814052771 100644 --- a/bbot/core/core.py +++ b/bbot/core/core.py @@ -6,6 +6,7 @@ from omegaconf import OmegaConf from bbot.errors import BBOTError +from .multiprocess import SHARED_INTERPRETER_STATE DEFAULT_CONFIG = None @@ -41,9 +42,23 @@ def __init__(self): self.logger self.log = logging.getLogger("bbot.core") + self._prep_multiprocessing() + + def _prep_multiprocessing(self): import multiprocessing + from .helpers.process import BBOTProcess + + if SHARED_INTERPRETER_STATE.is_main_process: + # if this is the main bbot process, set the logger and queue for the first time + from functools import partialmethod - self.process_name = multiprocessing.current_process().name + BBOTProcess.__init__ = partialmethod( + BBOTProcess.__init__, log_level=self.logger.log_level, log_queue=self.logger.queue + ) + + # this makes our process class the default for process pools, etc. + mp_context = multiprocessing.get_context("spawn") + mp_context.Process = BBOTProcess @property def home(self): @@ -91,7 +106,7 @@ def default_config(self): if DEFAULT_CONFIG is None: self.default_config = self.files_config.get_default_config() # ensure bbot home dir - if not "home" in self.default_config: + if "home" not in self.default_config: self.default_config["home"] = "~/.bbot" return DEFAULT_CONFIG @@ -187,12 +202,14 @@ def create_process(self, *args, **kwargs): if os.environ.get("BBOT_TESTING", "") == "True": process = self.create_thread(*args, **kwargs) else: - if self.process_name == "MainProcess": + if SHARED_INTERPRETER_STATE.is_scan_process: from .helpers.process import BBOTProcess process = BBOTProcess(*args, **kwargs) else: - raise BBOTError(f"Tried to start server from process {self.process_name}") + import multiprocessing + + raise BBOTError(f"Tried to start server from process {multiprocessing.current_process().name}") process.daemon = True return process diff --git a/bbot/core/engine.py b/bbot/core/engine.py index 26288ab8de..d8c58bfd80 100644 --- a/bbot/core/engine.py +++ b/bbot/core/engine.py @@ -10,6 +10,7 @@ import contextlib import contextvars import zmq.asyncio +import multiprocessing from pathlib import Path from concurrent.futures import CancelledError from contextlib import asynccontextmanager, suppress @@ -17,6 +18,7 @@ from bbot.core import CORE from bbot.errors import BBOTEngineError from bbot.core.helpers.async_helpers import get_event_loop +from bbot.core.multiprocess import SHARED_INTERPRETER_STATE from bbot.core.helpers.misc import rand_string, in_exception_chain @@ -264,10 +266,8 @@ def available_commands(self): return [s for s in self.CMDS if isinstance(s, str)] def start_server(self): - import multiprocessing - process_name = multiprocessing.current_process().name - if process_name == "MainProcess": + if SHARED_INTERPRETER_STATE.is_scan_process: kwargs = dict(self.server_kwargs) # if we're in tests, we use a single event loop to avoid weird race conditions # this allows us to more easily mock http, etc. @@ -641,7 +641,7 @@ async def finished_tasks(self, tasks, timeout=None): except BaseException as e: if isinstance(e, (TimeoutError, asyncio.exceptions.TimeoutError)): self.log.warning(f"{self.name}: Timeout after {timeout:,} seconds in finished_tasks({tasks})") - for task in tasks: + for task in list(tasks): task.cancel() self._await_cancelled_task(task) else: @@ -683,5 +683,5 @@ async def cancel_all_tasks(self): for client_id in list(self.tasks): await self.cancel_task(client_id) for client_id, tasks in self.child_tasks.items(): - for task in tasks: + for task in list(tasks): await self._await_cancelled_task(task) diff --git a/bbot/core/event/base.py b/bbot/core/event/base.py index 30089dcc00..eccaa846b9 100644 --- a/bbot/core/event/base.py +++ b/bbot/core/event/base.py @@ -14,8 +14,8 @@ from typing import Optional from contextlib import suppress from radixtarget import RadixTarget -from urllib.parse import urljoin, parse_qs from pydantic import BaseModel, field_validator +from urllib.parse import urlparse, urljoin, parse_qs from .helpers import * @@ -175,8 +175,8 @@ def __init__( self._scope_distance = None self._module_priority = None self._resolved_hosts = set() - self.dns_children = dict() - self.raw_dns_records = dict() + self.dns_children = {} + self.raw_dns_records = {} self._discovery_context = "" self._discovery_context_regex = re.compile(r"\{(?:event|module)[^}]*\}") self.web_spider_distance = 0 @@ -203,7 +203,7 @@ def __init__( # self.scan holds the instantiated scan object (for helpers, etc.) self.scan = scan if (not self.scan) and (not self._dummy): - raise ValidationError(f"Must specify scan") + raise ValidationError("Must specify scan") # self.scans holds a list of scan IDs from scans that encountered this event self.scans = [] if scans is not None: @@ -222,7 +222,7 @@ def __init__( self.parent = parent if (not self.parent) and (not self._dummy): - raise ValidationError(f"Must specify event parent") + raise ValidationError("Must specify event parent") if tags is not None: for tag in tags: @@ -301,9 +301,9 @@ def internal(self, value): The purpose of internal events is to enable speculative/explorative discovery without cluttering the console with irrelevant or uninteresting events. """ - if not value in (True, False): + if value not in (True, False): raise ValueError(f'"internal" must be boolean, not {type(value)}') - if value == True: + if value is True: self.add_tag("internal") else: self.remove_tag("internal") @@ -341,6 +341,21 @@ def host_original(self): return self.host return self._host_original + @property + def host_filterable(self): + """ + A string version of the event that's used for regex-based blacklisting. + + For example, the user can specify "REGEX:.*.evilcorp.com" in their blacklist, and this regex + will be applied against this property. + """ + parsed_url = getattr(self, "parsed_url", None) + if parsed_url is not None: + return parsed_url.geturl() + if self.host is not None: + return str(self.host) + return "" + @property def port(self): self.host @@ -500,22 +515,25 @@ def scope_distance(self, scope_distance): new_scope_distance = min(self.scope_distance, scope_distance) if self._scope_distance != new_scope_distance: # remove old scope distance tags - for t in list(self.tags): - if t.startswith("distance-"): - self.remove_tag(t) - if self.host: - if scope_distance == 0: - self.add_tag("in-scope") - self.remove_tag("affiliate") - else: - self.remove_tag("in-scope") - self.add_tag(f"distance-{new_scope_distance}") self._scope_distance = new_scope_distance + self.refresh_scope_tags() # apply recursively to parent events parent_scope_distance = getattr(self.parent, "scope_distance", None) if parent_scope_distance is not None and self.parent is not self: self.parent.scope_distance = new_scope_distance + 1 + def refresh_scope_tags(self): + for t in list(self.tags): + if t.startswith("distance-"): + self.remove_tag(t) + if self.host: + if self.scope_distance == 0: + self.add_tag("in-scope") + self.remove_tag("affiliate") + else: + self.remove_tag("in-scope") + self.add_tag(f"distance-{self.scope_distance}") + @property def scope_description(self): """ @@ -572,7 +590,7 @@ def parent(self, parent): if t in ("spider-danger", "spider-max"): self.add_tag(t) elif not self._dummy: - log.warning(f"Tried to set invalid parent on {self}: (got: {parent})") + log.warning(f"Tried to set invalid parent on {self}: (got: {repr(parent)} ({type(parent)}))") @property def parent_id(self): @@ -754,7 +772,7 @@ def json(self, mode="json", siem_friendly=False): Returns: dict: JSON-serializable dictionary representation of the event object. """ - j = dict() + j = {} # type, ID, scope description for i in ("type", "id", "uuid", "scope_description", "netloc"): v = getattr(self, i, "") @@ -998,18 +1016,20 @@ def __init__(self, *args, **kwargs): if not self.host: for parent in self.get_parents(include_self=True): # inherit closest URL - if not "url" in self.data: + if "url" not in self.data: parent_url = getattr(parent, "parsed_url", None) if parent_url is not None: self.data["url"] = parent_url.geturl() # inherit closest path - if not "path" in self.data and isinstance(parent.data, dict): + if "path" not in self.data and isinstance(parent.data, dict) and not parent.type == "HTTP_RESPONSE": parent_path = parent.data.get("path", None) if parent_path is not None: self.data["path"] = parent_path # inherit closest host if parent.host: self.data["host"] = str(parent.host) + # we do this to refresh the hash + self.data = self.data break # die if we still haven't found a host if not self.host: @@ -1025,6 +1045,9 @@ def sanitize_data(self, data): blob = None try: self._data_path = Path(data["path"]) + # prepend the scan's home dir if the path is relative + if not self._data_path.is_absolute(): + self._data_path = self.scan.home / self._data_path if self._data_path.is_file(): self.add_tag("file") if file_blobs: @@ -1112,8 +1135,7 @@ def __init__(self, *args, **kwargs): class IP_RANGE(DnsEvent): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - net = ipaddress.ip_network(self.data, strict=False) - self.add_tag(f"ipv{net.version}") + self.add_tag(f"ipv{self.host.version}") def sanitize_data(self, data): return str(ipaddress.ip_network(str(data), strict=False)) @@ -1164,7 +1186,6 @@ def __init__(self, *args, **kwargs): self.num_redirects = getattr(self.parent, "num_redirects", 0) def _data_id(self): - data = super()._data_id() # remove the querystring for URL/URL_UNVERIFIED events, because we will conditionally add it back in (based on settings) @@ -1212,7 +1233,7 @@ def sanitize_data(self, data): def add_tag(self, tag): host_same_as_parent = self.parent and self.host == self.parent.host - if tag == "spider-danger" and host_same_as_parent and not "spider-danger" in self.tags: + if tag == "spider-danger" and host_same_as_parent and "spider-danger" not in self.tags: # increment the web spider distance if self.type == "URL_UNVERIFIED": self.web_spider_distance += 1 @@ -1234,7 +1255,7 @@ def with_port(self): def _words(self): first_elem = self.parsed_url.path.lstrip("/").split("/")[0] - if not "." in first_elem: + if "." not in first_elem: return extract_words(first_elem) return set() @@ -1251,7 +1272,6 @@ def http_status(self): class URL(URL_UNVERIFIED): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1263,7 +1283,7 @@ def __init__(self, *args, **kwargs): @property def resolved_hosts(self): # TODO: remove this when we rip out httpx - return set(".".join(i.split("-")[1:]) for i in self.tags if i.startswith("ip-")) + return {".".join(i.split("-")[1:]) for i in self.tags if i.startswith("ip-")} @property def pretty_string(self): @@ -1293,7 +1313,6 @@ class URL_HINT(URL_UNVERIFIED): class WEB_PARAMETER(DictHostEvent): - def _data_id(self): # dedupe by url:name:param_type url = self.data.get("url", "") @@ -1339,18 +1358,22 @@ def sanitize_data(self, data): self.parsed_url = self.validators.validate_url_parsed(url) data["url"] = self.parsed_url.geturl() - header_dict = {} - for i in data.get("raw_header", "").splitlines(): - if len(i) > 0 and ":" in i: - k, v = i.split(":", 1) - k = k.strip().lower() - v = v.lstrip() - if k in header_dict: - header_dict[k].append(v) - else: - header_dict[k] = [v] + if not "raw_header" in data: + raise ValueError("raw_header is required for HTTP_RESPONSE events") + + if "header-dict" not in data: + header_dict = {} + for i in data.get("raw_header", "").splitlines(): + if len(i) > 0 and ":" in i: + k, v = i.split(":", 1) + k = k.strip().lower() + v = v.lstrip() + if k in header_dict: + header_dict[k].append(v) + else: + header_dict[k] = [v] + data["header-dict"] = header_dict - data["header-dict"] = header_dict # move URL to the front of the dictionary for visibility data = dict(data) new_data = {"url": data.pop("url")} @@ -1364,6 +1387,13 @@ def _words(self): def _pretty_string(self): return f'{self.data["hash"]["header_mmh3"]}:{self.data["hash"]["body_mmh3"]}' + @property + def raw_response(self): + """ + Formats the status code, headers, and body into a single string formatted as an HTTP/1.1 response. + """ + return f'{self.data["raw_header"]}{self.data["body"]}' + @property def http_status(self): try: @@ -1548,17 +1578,22 @@ def __init__(self, *args, **kwargs): # detect type of file content using magic from bbot.core.helpers.libmagic import get_magic_info, get_compression - extension, mime_type, description, confidence = get_magic_info(self.data["path"]) - self.data["magic_extension"] = extension - self.data["magic_mime_type"] = mime_type - self.data["magic_description"] = description - self.data["magic_confidence"] = confidence - # detection compression - compression = get_compression(mime_type) - if compression: - self.add_tag("compressed") - self.add_tag(f"{compression}-archive") - self.data["compression"] = compression + try: + extension, mime_type, description, confidence = get_magic_info(self.data["path"]) + self.data["magic_extension"] = extension + self.data["magic_mime_type"] = mime_type + self.data["magic_description"] = description + self.data["magic_confidence"] = confidence + # detection compression + compression = get_compression(mime_type) + if compression: + self.add_tag("compressed") + self.add_tag(f"{compression}-archive") + self.data["compression"] = compression + # refresh hash + self.data = self.data + except Exception as e: + log.debug(f"Error detecting file type: {type(e).__name__}: {e}") class RAW_DNS_RECORD(DictHostEvent, DnsEvent): @@ -1569,6 +1604,27 @@ class RAW_DNS_RECORD(DictHostEvent, DnsEvent): class MOBILE_APP(DictEvent): _always_emit = True + def _sanitize_data(self, data): + if isinstance(data, str): + data = {"url": data} + if "url" not in data: + raise ValidationError("url is required for MOBILE_APP events") + url = data["url"] + # parse URL + try: + self.parsed_url = urlparse(url) + except Exception as e: + raise ValidationError(f"Error parsing URL {url}: {e}") + if not "id" in data: + # extract "id" getparam + params = parse_qs(self.parsed_url.query) + try: + _id = params["id"][0] + except Exception: + raise ValidationError("id is required for MOBILE_APP events") + data["id"] = _id + return data + def _pretty_string(self): return self.data["url"] @@ -1630,6 +1686,8 @@ def make_event( When working within a module's `handle_event()`, use the instance method `self.make_event()` instead of calling this function directly. """ + if not data: + raise ValidationError("No data provided") # allow tags to be either a string or an array if not tags: @@ -1639,23 +1697,23 @@ def make_event( tags = set(tags) if is_event(data): - data = copy(data) - if scan is not None and not data.scan: - data.scan = scan - if scans is not None and not data.scans: - data.scans = scans + event = copy(data) + if scan is not None and not event.scan: + event.scan = scan + if scans is not None and not event.scans: + event.scans = scans if module is not None: - data.module = module + event.module = module if parent is not None: - data.parent = parent + event.parent = parent if context is not None: - data.discovery_context = context - if internal == True: - data.internal = True + event.discovery_context = context + if internal is True: + event.internal = True if tags: - data.tags = tags.union(data.tags) + event.tags = tags.union(event.tags) event_type = data.type - return data + return event else: if event_type is None: event_type, data = get_event_type(data) @@ -1685,6 +1743,13 @@ def make_event( if event_type == "USERNAME" and validators.soft_validate(data, "email"): event_type = "EMAIL_ADDRESS" tags.add("affiliate") + # Convert single-host IP_RANGE to IP_ADDRESS + if event_type == "IP_RANGE": + with suppress(Exception): + net = ipaddress.ip_network(data, strict=False) + if net.prefixlen == net.max_prefixlen: + event_type = "IP_ADDRESS" + data = net.network_address event_class = globals().get(event_type, DefaultEvent) diff --git a/bbot/core/helpers/bloom.py b/bbot/core/helpers/bloom.py index 357c715c03..62d2caa38f 100644 --- a/bbot/core/helpers/bloom.py +++ b/bbot/core/helpers/bloom.py @@ -5,14 +5,14 @@ class BloomFilter: """ - Simple bloom filter implementation capable of rougly 400K lookups/s. + Simple bloom filter implementation capable of roughly 400K lookups/s. BBOT uses bloom filters in scenarios like DNS brute-forcing, where it's useful to keep track of which mutations have been tried so far. A 100-megabyte bloom filter (800M bits) can store 10M entries with a .01% false-positive rate. A python hash is 36 bytes. So if you wanted to store these in a set, this would take up - 36 * 10M * 2 (key+value) == 720 megabytes. So we save rougly 7 times the space. + 36 * 10M * 2 (key+value) == 720 megabytes. So we save roughly 7 times the space. """ def __init__(self, size=8000000): @@ -64,8 +64,15 @@ def _fnv1a_hash(self, data): hash = (hash * 0x01000193) % 2**32 # 16777619 return hash - def __del__(self): + def close(self): + """Explicitly close the memory-mapped file.""" self.mmap_file.close() + def __del__(self): + try: + self.close() + except Exception: + pass + def __contains__(self, item): return self.check(item) diff --git a/bbot/core/helpers/command.py b/bbot/core/helpers/command.py index 16f9c9131c..d4f017b330 100644 --- a/bbot/core/helpers/command.py +++ b/bbot/core/helpers/command.py @@ -210,9 +210,10 @@ async def _write_proc_line(proc, chunk): return True except Exception as e: proc_args = [str(s) for s in getattr(proc, "args", [])] - command = " ".join(proc_args) - log.warning(f"Error writing line to stdin for command: {command}: {e}") - log.trace(traceback.format_exc()) + command = " ".join(proc_args).strip() + if command: + log.warning(f"Error writing line to stdin for command: {command}: {e}") + log.trace(traceback.format_exc()) return False @@ -268,11 +269,11 @@ def _prepare_command_kwargs(self, command, kwargs): (['sudo', '-E', '-A', 'LD_LIBRARY_PATH=...', 'PATH=...', 'ls', '-l'], {'limit': 104857600, 'stdout': -1, 'stderr': -1, 'env': environ(...)}) """ # limit = 100MB (this is needed for cases like httpx that are sending large JSON blobs over stdout) - if not "limit" in kwargs: + if "limit" not in kwargs: kwargs["limit"] = 1024 * 1024 * 100 - if not "stdout" in kwargs: + if "stdout" not in kwargs: kwargs["stdout"] = asyncio.subprocess.PIPE - if not "stderr" in kwargs: + if "stderr" not in kwargs: kwargs["stderr"] = asyncio.subprocess.PIPE sudo = kwargs.pop("sudo", False) @@ -285,7 +286,7 @@ def _prepare_command_kwargs(self, command, kwargs): # use full path of binary, if not already specified binary = command[0] - if not "/" in binary: + if "/" not in binary: binary_full_path = which(binary) if binary_full_path is None: raise SubprocessError(f'Command "{binary}" was not found') diff --git a/bbot/core/helpers/depsinstaller/installer.py b/bbot/core/helpers/depsinstaller/installer.py index 652a109890..48d2f970fa 100644 --- a/bbot/core/helpers/depsinstaller/installer.py +++ b/bbot/core/helpers/depsinstaller/installer.py @@ -14,17 +14,49 @@ from ansible_runner.interface import run from subprocess import CalledProcessError -from ..misc import can_sudo_without_password, os_platform, rm_at_exit +from ..misc import can_sudo_without_password, os_platform, rm_at_exit, get_python_constraints log = logging.getLogger("bbot.core.helpers.depsinstaller") class DepsInstaller: + CORE_DEPS = { + # core BBOT dependencies in the format of binary: package_name + # each one will only be installed if the binary is not found + "unzip": "unzip", + "zipinfo": "unzip", + "curl": "curl", + "git": "git", + "make": "make", + "gcc": "gcc", + "bash": "bash", + "which": "which", + "unrar": "unrar-free", + "tar": "tar", + # debian why are you like this + "7z": [ + { + "name": "Install 7zip (Debian)", + "package": {"name": ["p7zip-full"], "state": "present"}, + "become": True, + "when": "ansible_facts['os_family'] == 'Debian'", + }, + { + "name": "Install 7zip (Non-Debian)", + "package": {"name": ["p7zip"], "state": "present"}, + "become": True, + "when": "ansible_facts['os_family'] != 'Debian'", + }, + ], + } + def __init__(self, parent_helper): self.parent_helper = parent_helper self.preset = self.parent_helper.preset self.core = self.preset.core + self.os_platform = os_platform() + # respect BBOT's http timeout self.web_config = self.parent_helper.config.get("web", {}) http_timeout = self.web_config.get("http_timeout", 30) @@ -44,7 +76,13 @@ def __init__(self, parent_helper): self.parent_helper.mkdir(self.command_status) self.setup_status = self.read_setup_status() - self.deps_behavior = self.parent_helper.config.get("deps_behavior", "abort_on_failure").lower() + # make sure we're using a minimal git config + self.minimal_git_config = self.data_dir / "minimal_git.config" + self.minimal_git_config.touch() + os.environ["GIT_CONFIG_GLOBAL"] = str(self.minimal_git_config) + + self.deps_config = self.parent_helper.config.get("deps", {}) + self.deps_behavior = self.deps_config.get("behavior", "abort_on_failure").lower() self.ansible_debug = self.core.logger.log_level <= logging.DEBUG self.venv = "" if sys.prefix != sys.base_prefix: @@ -91,11 +129,11 @@ async def install(self, *modules): or self.deps_behavior == "force_install" ): if not notified: - log.hugeinfo(f"Installing module dependencies. Please be patient, this may take a while.") + log.hugeinfo("Installing module dependencies. Please be patient, this may take a while.") notified = True log.verbose(f'Installing dependencies for module "{m}"') # get sudo access if we need it - if preloaded.get("sudo", False) == True: + if preloaded.get("sudo", False) is True: self.ensure_root(f'Module "{m}" needs root privileges to install its dependencies.') success = await self.install_module(m) self.setup_status[module_hash] = success @@ -153,7 +191,7 @@ async def install_module(self, module): deps_common = preloaded["deps"]["common"] if deps_common: for dep_common in deps_common: - if self.setup_status.get(dep_common, False) == True: + if self.setup_status.get(dep_common, False) is True: log.debug( f'Skipping installation of dependency "{dep_common}" for module "{module}" since it is already installed' ) @@ -171,10 +209,13 @@ async def pip_install(self, packages, constraints=None): command = [sys.executable, "-m", "pip", "install", "--upgrade"] + packages - if constraints: - constraints_tempfile = self.parent_helper.tempfile(constraints, pipe=False) - command.append("--constraint") - command.append(constraints_tempfile) + # if no custom constraints are provided, use the constraints of the currently installed version of bbot + if constraints is not None: + constraints = get_python_constraints() + + constraints_tempfile = self.parent_helper.tempfile(constraints, pipe=False) + command.append("--constraint") + command.append(constraints_tempfile) process = None try: @@ -193,28 +234,32 @@ def apt_install(self, packages): """ Install packages with the OS's default package manager (apt, pacman, dnf, etc.) """ - packages_str = ",".join(packages) + args, kwargs = self._make_apt_ansible_args(packages) + success, err = self.ansible_run(module="package", args=args, **kwargs) + if success: + log.info(f'Successfully installed OS packages "{",".join(sorted(packages))}"') + else: + log.warning( + f"Failed to install OS packages ({err}). Recommend installing the following packages manually:" + ) + for p in packages: + log.warning(f" - {p}") + return success + + def _make_apt_ansible_args(self, packages): + packages_str = ",".join(sorted(packages)) log.info(f"Installing the following OS packages: {packages_str}") args = {"name": packages_str, "state": "present"} # , "update_cache": True, "cache_valid_time": 86400} kwargs = {} # don't sudo brew - if os_platform() != "darwin": + if self.os_platform != "darwin": kwargs = { "ansible_args": { "ansible_become": True, "ansible_become_method": "sudo", } } - success, err = self.ansible_run(module="package", args=args, **kwargs) - if success: - log.info(f'Successfully installed OS packages "{packages_str}"') - else: - log.warning( - f"Failed to install OS packages ({err}). Recommend installing the following packages manually:" - ) - for p in packages: - log.warning(f" - {p}") - return success + return args, kwargs def shell(self, module, commands): tasks = [] @@ -235,7 +280,7 @@ def shell(self, module, commands): if success: log.info(f"Successfully ran {len(commands):,} shell commands") else: - log.warning(f"Failed to run shell dependencies") + log.warning("Failed to run shell dependencies") return success def tasks(self, module, tasks): @@ -260,7 +305,7 @@ def ansible_run(self, tasks=None, module=None, args=None, ansible_args=None): for task in tasks: if "package" in task: # special case for macos - if os_platform() == "darwin": + if self.os_platform == "darwin": # don't sudo brew task["become"] = False # brew doesn't support update_cache @@ -283,8 +328,8 @@ def ansible_run(self, tasks=None, module=None, args=None, ansible_args=None): }, module=module, module_args=module_args, - quiet=not self.ansible_debug, - verbosity=(3 if self.ansible_debug else 0), + quiet=True, + verbosity=0, cancel_callback=lambda: None, ) @@ -294,14 +339,14 @@ def ansible_run(self, tasks=None, module=None, args=None, ansible_args=None): err = "" for e in res.events: if self.ansible_debug and not success: - log.debug(json.dumps(e, indent=4)) + log.debug(json.dumps(e, indent=2)) if e["event"] == "runner_on_failed": err = e["event_data"]["res"]["msg"] break return success, err def read_setup_status(self): - setup_status = dict() + setup_status = {} if self.setup_status_cache.is_file(): with open(self.setup_status_cache) as f: with suppress(Exception): @@ -338,26 +383,30 @@ def ensure_root(self, message=""): def install_core_deps(self): to_install = set() + to_install_friendly = set() + playbook = [] self._install_sudo_askpass() # ensure tldextract data is cached self.parent_helper.tldextract("evilcorp.co.uk") - # command: package_name - core_deps = { - "unzip": "unzip", - "zipinfo": "unzip", - "curl": "curl", - "git": "git", - "make": "make", - "gcc": "gcc", - "bash": "bash", - "which": "which", - } - for command, package_name in core_deps.items(): + for command, package_name_or_playbook in self.CORE_DEPS.items(): if not self.parent_helper.which(command): - to_install.add(package_name) + to_install_friendly.add(command) + if isinstance(package_name_or_playbook, str): + to_install.add(package_name_or_playbook) + else: + playbook.extend(package_name_or_playbook) if to_install: + playbook.append( + { + "name": "Install Core BBOT Dependencies", + "package": {"name": list(to_install), "state": "present"}, + "become": True, + } + ) + if playbook: + log.info(f"Installing core BBOT dependencies: {','.join(sorted(to_install_friendly))}") self.ensure_root() - self.apt_install(list(to_install)) + self.ansible_run(tasks=playbook) def _setup_sudo_cache(self): if not self._sudo_cache_setup: diff --git a/bbot/core/helpers/diff.py b/bbot/core/helpers/diff.py index 59ee96567c..ea7ca3a864 100644 --- a/bbot/core/helpers/diff.py +++ b/bbot/core/helpers/diff.py @@ -94,14 +94,14 @@ async def _baseline(self): baseline_1_json = xmltodict.parse(baseline_1.text) baseline_2_json = xmltodict.parse(baseline_2.text) except ExpatError: - log.debug(f"Cant HTML parse for {self.baseline_url}. Switching to text parsing as a backup") + log.debug(f"Can't HTML parse for {self.baseline_url}. Switching to text parsing as a backup") baseline_1_json = baseline_1.text.split("\n") baseline_2_json = baseline_2.text.split("\n") ddiff = DeepDiff(baseline_1_json, baseline_2_json, ignore_order=True, view="tree") self.ddiff_filters = [] - for k, v in ddiff.items(): + for k in ddiff.keys(): for x in list(ddiff[k]): log.debug(f"Added {k} filter for path: {x.path()}") self.ddiff_filters.append(x.path()) @@ -140,7 +140,7 @@ def compare_headers(self, headers_1, headers_2): ddiff = DeepDiff(headers_1, headers_2, ignore_order=True, view="tree") - for k, v in ddiff.items(): + for k in ddiff.keys(): for x in list(ddiff[k]): try: header_value = str(x).split("'")[1] @@ -183,7 +183,7 @@ async def compare( await self._baseline() - if timeout == None: + if timeout is None: timeout = self.timeout reflection = False @@ -203,7 +203,7 @@ async def compare( ) if subject_response is None: - # this can be caused by a WAF not liking the header, so we really arent interested in it + # this can be caused by a WAF not liking the header, so we really aren't interested in it return (True, "403", reflection, subject_response) if check_reflection: @@ -225,7 +225,7 @@ async def compare( subject_json = xmltodict.parse(subject_response.text) except ExpatError: - log.debug(f"Cant HTML parse for {subject.split('?')[0]}. Switching to text parsing as a backup") + log.debug(f"Can't HTML parse for {subject.split('?')[0]}. Switching to text parsing as a backup") subject_json = subject_response.text.split("\n") diff_reasons = [] @@ -238,11 +238,11 @@ async def compare( different_headers = self.compare_headers(self.baseline.headers, subject_response.headers) if different_headers: - log.debug(f"headers were different, no match") + log.debug("headers were different, no match") diff_reasons.append("header") - if self.compare_body(self.baseline_json, subject_json) == False: - log.debug(f"difference in HTML body, no match") + if self.compare_body(self.baseline_json, subject_json) is False: + log.debug("difference in HTML body, no match") diff_reasons.append("body") @@ -275,6 +275,6 @@ async def canary_check(self, url, mode, rounds=3): ) # if a nonsense header "caused" a difference, we need to abort. We also need to abort if our canary was reflected - if match == False or reflection == True: + if match is False or reflection is True: return False return True diff --git a/bbot/core/helpers/dns/brute.py b/bbot/core/helpers/dns/brute.py index 0c3799ca5b..9bf796d243 100644 --- a/bbot/core/helpers/dns/brute.py +++ b/bbot/core/helpers/dns/brute.py @@ -41,10 +41,13 @@ async def dnsbrute(self, module, domain, subdomains, type=None): type = "A" type = str(type).strip().upper() - wildcard_rdtypes = await self.parent_helper.dns.is_wildcard_domain(domain, (type, "CNAME")) - if wildcard_rdtypes: + wildcard_domains = await self.parent_helper.dns.is_wildcard_domain(domain, (type, "CNAME")) + wildcard_rdtypes = set() + for domain, rdtypes in wildcard_domains.items(): + wildcard_rdtypes.update(rdtypes) + if wildcard_domains: self.log.hugewarning( - f"Aborting massdns on {domain} because it's a wildcard domain ({','.join(wildcard_rdtypes)})" + f"Aborting massdns on {domain} because it's a wildcard domain ({','.join(sorted(wildcard_rdtypes))})" ) return [] @@ -161,7 +164,7 @@ def gen_random_subdomains(self, n=50): for i in range(0, max(0, n - 5)): d = delimiters[i % len(delimiters)] l = lengths[i % len(lengths)] - segments = list(random.choice(self.devops_mutations) for _ in range(l)) + segments = [random.choice(self.devops_mutations) for _ in range(l)] segments.append(self.parent_helper.rand_string(length=8, digits=False)) subdomain = d.join(segments) yield subdomain diff --git a/bbot/core/helpers/dns/dns.py b/bbot/core/helpers/dns/dns.py index 43380b7465..89758b35ff 100644 --- a/bbot/core/helpers/dns/dns.py +++ b/bbot/core/helpers/dns/dns.py @@ -16,7 +16,6 @@ class DNSHelper(EngineClient): - SERVER_CLASS = DNSEngine ERROR_CLASS = DNSError @@ -179,7 +178,7 @@ def _wildcard_prevalidation(self, host): host = clean_dns_record(host) # skip check if it's an IP or a plain hostname - if is_ip(host) or not "." in host: + if is_ip(host) or "." not in host: return False # skip if query isn't a dns name diff --git a/bbot/core/helpers/dns/engine.py b/bbot/core/helpers/dns/engine.py index 219339c308..47df899b05 100644 --- a/bbot/core/helpers/dns/engine.py +++ b/bbot/core/helpers/dns/engine.py @@ -24,7 +24,6 @@ class DNSEngine(EngineServer): - CMDS = { 0: "resolve", 1: "resolve_raw", @@ -55,7 +54,7 @@ def __init__(self, socket_path, config={}, debug=False): dns_omit_queries = self.dns_config.get("omit_queries", None) if not dns_omit_queries: dns_omit_queries = [] - self.dns_omit_queries = dict() + self.dns_omit_queries = {} for d in dns_omit_queries: d = d.split(":") if len(d) == 2: @@ -73,7 +72,7 @@ def __init__(self, socket_path, config={}, debug=False): self.wildcard_ignore = [] self.wildcard_ignore = tuple([str(d).strip().lower() for d in self.wildcard_ignore]) self.wildcard_tests = self.dns_config.get("wildcard_tests", 5) - self._wildcard_cache = dict() + self._wildcard_cache = {} # since wildcard detection takes some time, This is to prevent multiple # modules from kicking off wildcard detection for the same domain at the same time self._wildcard_lock = NamedLock() @@ -83,7 +82,7 @@ def __init__(self, socket_path, config={}, debug=False): self._last_connectivity_warning = time.time() # keeps track of warnings issued for wildcard detection to prevent duplicate warnings self._dns_warnings = set() - self._errors = dict() + self._errors = {} self._debug = self.dns_config.get("debug", False) self._dns_cache = LRUCache(maxsize=10000) @@ -476,7 +475,6 @@ async def is_wildcard(self, query, rdtypes, raw_dns_records=None): # for every parent domain, starting with the shortest parents = list(domain_parents(query)) for parent in parents[::-1]: - # check if the parent domain is set up with wildcards wildcard_results = await self.is_wildcard_domain(parent, rdtypes_to_check) @@ -640,7 +638,7 @@ async def _connectivity_check(self, interval=5): self._last_dns_success = time.time() return True if time.time() - self._last_connectivity_warning > interval: - self.log.warning(f"DNS queries are failing, please check your internet connection") + self.log.warning("DNS queries are failing, please check your internet connection") self._last_connectivity_warning = time.time() self._errors.clear() return False diff --git a/bbot/core/helpers/dns/helpers.py b/bbot/core/helpers/dns/helpers.py index c18a2c1620..340af5a425 100644 --- a/bbot/core/helpers/dns/helpers.py +++ b/bbot/core/helpers/dns/helpers.py @@ -1,6 +1,6 @@ import logging -from bbot.core.helpers.regexes import dns_name_regex +from bbot.core.helpers.regexes import dns_name_extraction_regex from bbot.core.helpers.misc import clean_dns_record, smart_decode log = logging.getLogger("bbot.core.helpers.dns") @@ -198,7 +198,7 @@ def add_result(rdtype, _record): elif rdtype == "TXT": for s in record.strings: s = smart_decode(s) - for match in dns_name_regex.finditer(s): + for match in dns_name_extraction_regex.finditer(s): start, end = match.span() host = s[start:end] add_result(rdtype, host) diff --git a/bbot/core/helpers/dns/mock.py b/bbot/core/helpers/dns/mock.py index 17ee2759ae..3f6fd83ea5 100644 --- a/bbot/core/helpers/dns/mock.py +++ b/bbot/core/helpers/dns/mock.py @@ -5,7 +5,6 @@ class MockResolver: - def __init__(self, mock_data=None, custom_lookup_fn=None): self.mock_data = mock_data if mock_data else {} self._custom_lookup_fn = custom_lookup_fn diff --git a/bbot/core/helpers/files.py b/bbot/core/helpers/files.py index fb92d1c8b8..5e7d2d88d4 100644 --- a/bbot/core/helpers/files.py +++ b/bbot/core/helpers/files.py @@ -83,7 +83,7 @@ def _feed_pipe(self, pipe, content, text=True): for c in content: p.write(decode_fn(c) + newline) except BrokenPipeError: - log.debug(f"Broken pipe in _feed_pipe()") + log.debug("Broken pipe in _feed_pipe()") except ValueError: log.debug(f"Error _feed_pipe(): {traceback.format_exc()}") except KeyboardInterrupt: diff --git a/bbot/core/helpers/helper.py b/bbot/core/helpers/helper.py index 9565c1623e..78ccf67155 100644 --- a/bbot/core/helpers/helper.py +++ b/bbot/core/helpers/helper.py @@ -12,10 +12,11 @@ from .regex import RegexHelper from .wordcloud import WordCloud from .interactsh import Interactsh -from ...scanner.target import Target from .depsinstaller import DepsInstaller from .async_helpers import get_event_loop +from bbot.scanner.target import BaseTarget + log = logging.getLogger("bbot.core.helpers") @@ -152,11 +153,13 @@ def temp_filename(self, extension=None): return self.temp_dir / filename def clean_old_scans(self): - _filter = lambda x: x.is_dir() and self.regexes.scan_name_regex.match(x.name) + def _filter(x): + return x.is_dir() and self.regexes.scan_name_regex.match(x.name) + self.clean_old(self.scans_dir, keep=self.keep_old_scans, filter=_filter) - def make_target(self, *events, **kwargs): - return Target(*events, **kwargs) + def make_target(self, *targets, **kwargs): + return BaseTarget(*targets, scan=self.scan, **kwargs) @property def config(self): diff --git a/bbot/core/helpers/interactsh.py b/bbot/core/helpers/interactsh.py index f707fac93a..c809999a3b 100644 --- a/bbot/core/helpers/interactsh.py +++ b/bbot/core/helpers/interactsh.py @@ -155,7 +155,7 @@ async def register(self, callback=None): break if not self.server: - raise InteractshError(f"Failed to register with an interactsh server") + raise InteractshError("Failed to register with an interactsh server") log.info( f"Successfully registered to interactsh server {self.server} with correlation_id {self.correlation_id} [{self.domain}]" @@ -181,7 +181,7 @@ async def deregister(self): >>> await interactsh_client.deregister() """ if not self.server or not self.correlation_id or not self.secret: - raise InteractshError(f"Missing required information to deregister") + raise InteractshError("Missing required information to deregister") headers = {} if self.token: @@ -226,7 +226,7 @@ async def poll(self): ] """ if not self.server or not self.correlation_id or not self.secret: - raise InteractshError(f"Missing required information to poll") + raise InteractshError("Missing required information to poll") headers = {} if self.token: diff --git a/bbot/core/helpers/libmagic.py b/bbot/core/helpers/libmagic.py index 77a9eebce9..37612f558e 100644 --- a/bbot/core/helpers/libmagic.py +++ b/bbot/core/helpers/libmagic.py @@ -2,7 +2,6 @@ def get_magic_info(file): - magic_detections = puremagic.magic_file(file) if magic_detections: magic_detections.sort(key=lambda x: x.confidence, reverse=True) @@ -15,54 +14,52 @@ def get_compression(mime_type): mime_type = mime_type.lower() # from https://github.com/cdgriffith/puremagic/blob/master/puremagic/magic_data.json compression_map = { - "application/gzip": "gzip", # Gzip compressed file - "application/zip": "zip", # Zip archive - "application/x-bzip2": "bzip2", # Bzip2 compressed file - "application/x-xz": "xz", # XZ compressed file - "application/x-7z-compressed": "7z", # 7-Zip archive - "application/vnd.rar": "rar", # RAR archive - "application/x-lzma": "lzma", # LZMA compressed file - "application/x-compress": "compress", # Unix compress file - "application/zstd": "zstd", # Zstandard compressed file - "application/x-lz4": "lz4", # LZ4 compressed file - "application/x-tar": "tar", # Tar archive - "application/x-zip-compressed-fb2": "zip", # Zip archive (FB2) - "application/epub+zip": "zip", # EPUB book (Zip archive) - "application/pak": "pak", # PAK archive - "application/x-lha": "lha", # LHA archive "application/arj": "arj", # ARJ archive - "application/vnd.ms-cab-compressed": "cab", # Microsoft Cabinet archive - "application/x-sit": "sit", # StuffIt archive "application/binhex": "binhex", # BinHex encoded file - "application/x-lrzip": "lrzip", # Long Range ZIP - "application/x-alz": "alz", # ALZip archive - "application/x-tgz": "tgz", # Gzip compressed Tar archive - "application/x-gzip": "gzip", # Gzip compressed file - "application/x-lzip": "lzip", # Lzip compressed file - "application/x-zstd-compressed-tar": "zstd", # Zstandard compressed Tar archive - "application/x-lz4-compressed-tar": "lz4", # LZ4 compressed Tar archive - "application/vnd.comicbook+zip": "zip", # Comic book archive (Zip) - "application/vnd.palm": "palm", # Palm OS data + "application/epub+zip": "zip", # EPUB book (Zip archive) "application/fictionbook2+zip": "zip", # FictionBook 2.0 (Zip) "application/fictionbook3+zip": "zip", # FictionBook 3.0 (Zip) + "application/gzip": "gzip", # Gzip compressed file + "application/java-archive": "zip", # Java Archive (JAR) + "application/pak": "pak", # PAK archive + "application/vnd.android.package-archive": "zip", # Android package (APK) + "application/vnd.comicbook-rar": "rar", # Comic book archive (RAR) + "application/vnd.comicbook+zip": "zip", # Comic book archive (Zip) + "application/vnd.ms-cab-compressed": "cab", # Microsoft Cabinet archive + "application/vnd.palm": "palm", # Palm OS data + "application/vnd.rar": "rar", # RAR archive + "application/x-7z-compressed": "7z", # 7-Zip archive + "application/x-ace": "ace", # ACE archive + "application/x-alz": "alz", # ALZip archive + "application/x-arc": "arc", # ARC archive + "application/x-archive": "ar", # Unix archive + "application/x-bzip2": "bzip2", # Bzip2 compressed file + "application/x-compress": "compress", # Unix compress file "application/x-cpio": "cpio", # CPIO archive + "application/x-gzip": "gzip", # Gzip compressed file + "application/x-itunes-ipa": "zip", # iOS application archive (IPA) "application/x-java-pack200": "pack200", # Java Pack200 archive + "application/x-lha": "lha", # LHA archive + "application/x-lrzip": "lrzip", # Long Range ZIP + "application/x-lz4-compressed-tar": "lz4", # LZ4 compressed Tar archive + "application/x-lz4": "lz4", # LZ4 compressed file + "application/x-lzip": "lzip", # Lzip compressed file + "application/x-lzma": "lzma", # LZMA compressed file "application/x-par2": "par2", # PAR2 recovery file + "application/x-qpress": "qpress", # Qpress archive "application/x-rar-compressed": "rar", # RAR archive - "application/java-archive": "zip", # Java Archive (JAR) - "application/x-webarchive": "zip", # Web archive (Zip) - "application/vnd.android.package-archive": "zip", # Android package (APK) - "application/x-itunes-ipa": "zip", # iOS application archive (IPA) + "application/x-sit": "sit", # StuffIt archive "application/x-stuffit": "sit", # StuffIt archive - "application/x-archive": "ar", # Unix archive - "application/x-qpress": "qpress", # Qpress archive + "application/x-tar": "tar", # Tar archive + "application/x-tgz": "tgz", # Gzip compressed Tar archive + "application/x-webarchive": "zip", # Web archive (Zip) "application/x-xar": "xar", # XAR archive - "application/x-ace": "ace", # ACE archive + "application/x-xz": "xz", # XZ compressed file + "application/x-zip-compressed-fb2": "zip", # Zip archive (FB2) "application/x-zoo": "zoo", # Zoo archive - "application/x-arc": "arc", # ARC archive "application/x-zstd-compressed-tar": "zstd", # Zstandard compressed Tar archive - "application/x-lz4-compressed-tar": "lz4", # LZ4 compressed Tar archive - "application/vnd.comicbook-rar": "rar", # Comic book archive (RAR) + "application/zip": "zip", # Zip archive + "application/zstd": "zstd", # Zstandard compressed file } return compression_map.get(mime_type, "") diff --git a/bbot/core/helpers/misc.py b/bbot/core/helpers/misc.py index c493bd4d37..92c9e523fd 100644 --- a/bbot/core/helpers/misc.py +++ b/bbot/core/helpers/misc.py @@ -391,7 +391,7 @@ def url_parents(u): parent_list = [] while 1: parent = parent_url(u) - if parent == None: + if parent is None: return parent_list elif parent not in parent_list: parent_list.append(parent) @@ -512,7 +512,7 @@ def domain_stem(domain): - Utilizes the `tldextract` function for domain parsing. """ parsed = tldextract(str(domain)) - return f".".join(parsed.subdomain.split(".") + parsed.domain.split(".")).strip(".") + return ".".join(parsed.subdomain.split(".") + parsed.domain.split(".")).strip(".") def ip_network_parents(i, include_self=False): @@ -559,13 +559,12 @@ def is_port(p): return p and p.isdigit() and 0 <= int(p) <= 65535 -def is_dns_name(d, include_local=True): +def is_dns_name(d): """ Determines if the given string is a valid DNS name. Args: d (str): The string to be checked. - include_local (bool): Consider local hostnames to be valid (hostnames without periods) Returns: bool: True if the string is a valid DNS name, False otherwise. @@ -575,28 +574,24 @@ def is_dns_name(d, include_local=True): True >>> is_dns_name('localhost') True - >>> is_dns_name('localhost', include_local=False) - False >>> is_dns_name('192.168.1.1') False """ if is_ip(d): return False d = smart_decode(d) - if include_local: - if bbot_regexes.hostname_regex.match(d): - return True - if bbot_regexes.dns_name_regex.match(d): + if bbot_regexes.dns_name_validation_regex.match(d): return True return False -def is_ip(d, version=None): +def is_ip(d, version=None, include_network=False): """ Checks if the given string or object represents a valid IP address. Args: d (str or ipaddress.IPvXAddress): The IP address to check. + include_network (bool, optional): Whether to include network types (IPv4Network or IPv6Network). Defaults to False. version (int, optional): The IP version to validate (4 or 6). Default is None. Returns: @@ -612,12 +607,17 @@ def is_ip(d, version=None): >>> is_ip('evilcorp.com') False """ + ip = None try: ip = ipaddress.ip_address(d) - if version is None or ip.version == version: - return True except Exception: - pass + if include_network: + try: + ip = ipaddress.ip_network(d, strict=False) + except Exception: + pass + if ip is not None and (version is None or ip.version == version): + return True return False @@ -915,12 +915,12 @@ def extract_params_xml(xml_data, compare_mode="getparam"): # Define valid characters for each mode based on RFCs valid_chars_dict = { - "header": set( + "header": { chr(c) for c in range(33, 127) if chr(c) in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" - ), - "getparam": set(chr(c) for c in range(33, 127) if chr(c) not in ":/?#[]@!$&'()*+,;="), - "postparam": set(chr(c) for c in range(33, 127) if chr(c) not in ":/?#[]@!$&'()*+,;="), - "cookie": set(chr(c) for c in range(33, 127) if chr(c) not in '()<>@,;:"/[]?={} \t'), + }, + "getparam": {chr(c) for c in range(33, 127) if chr(c) not in ":/?#[]@!$&'()*+,;="}, + "postparam": {chr(c) for c in range(33, 127) if chr(c) not in ":/?#[]@!$&'()*+,;="}, + "cookie": {chr(c) for c in range(33, 127) if chr(c) not in '()<>@,;:"/[]?={} \t'}, } @@ -1142,7 +1142,7 @@ def chain_lists( """ if isinstance(l, str): l = [l] - final_list = dict() + final_list = {} for entry in l: for s in split_regex.split(entry): f = s.strip() @@ -1339,7 +1339,7 @@ def search_dict_by_key(key, d): if isinstance(d, dict): if key in d: yield d[key] - for k, v in d.items(): + for v in d.values(): yield from search_dict_by_key(key, v) elif isinstance(d, list): for v in d: @@ -1406,7 +1406,7 @@ def search_dict_values(d, *regexes): results.add(h) yield result elif isinstance(d, dict): - for _, v in d.items(): + for v in d.values(): yield from search_dict_values(v, *regexes) elif isinstance(d, list): for v in d: @@ -2391,7 +2391,7 @@ def in_exception_chain(e, exc_types): ... if not in_exception_chain(e, (KeyboardInterrupt, asyncio.CancelledError)): ... raise """ - return any([isinstance(_, exc_types) for _ in get_exception_chain(e)]) + return any(isinstance(_, exc_types) for _ in get_exception_chain(e)) def get_traceback_details(e): @@ -2807,3 +2807,21 @@ def safe_format(s, **kwargs): Format string while ignoring unused keys (prevents KeyError) """ return s.format_map(SafeDict(kwargs)) + + +def get_python_constraints(): + req_regex = re.compile(r"([^(]+)\s*\((.*)\)", re.IGNORECASE) + + def clean_requirement(req_string): + # Extract package name and version constraints from format like "package (>=1.0,<2.0)" + match = req_regex.match(req_string) + if match: + name, constraints = match.groups() + return f"{name.strip()}{constraints}" + + return req_string + + from importlib.metadata import distribution + + dist = distribution("bbot") + return [clean_requirement(r) for r in dist.requires] diff --git a/bbot/core/helpers/names_generator.py b/bbot/core/helpers/names_generator.py index a0a569e530..e2d9b74311 100644 --- a/bbot/core/helpers/names_generator.py +++ b/bbot/core/helpers/names_generator.py @@ -293,6 +293,7 @@ "alyssa", "amanda", "amber", + "amir", "amy", "andrea", "andrew", diff --git a/bbot/core/helpers/process.py b/bbot/core/helpers/process.py index ad83791072..30f143985f 100644 --- a/bbot/core/helpers/process.py +++ b/bbot/core/helpers/process.py @@ -1,17 +1,12 @@ import logging import traceback import threading -import multiprocessing from multiprocessing.context import SpawnProcess from .misc import in_exception_chain -current_process = multiprocessing.current_process() - - class BBOTThread(threading.Thread): - default_name = "default bbot thread" def __init__(self, *args, **kwargs): @@ -28,7 +23,6 @@ def run(self): class BBOTProcess(SpawnProcess): - default_name = "bbot process pool" def __init__(self, *args, **kwargs): @@ -57,17 +51,3 @@ def run(self): if not in_exception_chain(e, (KeyboardInterrupt,)): log.warning(f"Error in {self.name}: {e}") log.trace(traceback.format_exc()) - - -if current_process.name == "MainProcess": - # if this is the main bbot process, set the logger and queue for the first time - from bbot.core import CORE - from functools import partialmethod - - BBOTProcess.__init__ = partialmethod( - BBOTProcess.__init__, log_level=CORE.logger.log_level, log_queue=CORE.logger.queue - ) - -# this makes our process class the default for process pools, etc. -mp_context = multiprocessing.get_context("spawn") -mp_context.Process = BBOTProcess diff --git a/bbot/core/helpers/regex.py b/bbot/core/helpers/regex.py index f0bee1fc0a..97d8cbe6ec 100644 --- a/bbot/core/helpers/regex.py +++ b/bbot/core/helpers/regex.py @@ -41,7 +41,7 @@ async def findall_multi(self, compiled_regexes, *args, threads=10, **kwargs): """ if not isinstance(compiled_regexes, dict): raise ValueError('compiled_regexes must be a dictionary like this: {"regex_name": }') - for k, v in compiled_regexes.items(): + for v in compiled_regexes.values(): self.ensure_compiled_regex(v) tasks = {} diff --git a/bbot/core/helpers/regexes.py b/bbot/core/helpers/regexes.py index 1fd513e5aa..adf8abb650 100644 --- a/bbot/core/helpers/regexes.py +++ b/bbot/core/helpers/regexes.py @@ -23,7 +23,7 @@ _ipv4_regex = r"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}" ipv4_regex = re.compile(_ipv4_regex, re.I) -# IPv6 is complicated, so we have accomodate alternative patterns, +# IPv6 is complicated, so we have accommodate alternative patterns, # :(:[A-F0-9]{1,4}){1,7} == ::1, ::ffff:1 # ([A-F0-9]{1,4}:){1,7}: == 2001::, 2001:db8::, 2001:db8:0:1:2:3:: # ([A-F0-9]{1,4}:){1,6}:([A-F0-9]{1,4}) == 2001::1, 2001:db8::1, 2001:db8:0:1:2:3::1 @@ -36,15 +36,12 @@ _ipv4_regex + r"\/[0-9]{1,2}", _ipv6_regex + r"\/[0-9]{1,3}", ) -ip_range_regexes = list(re.compile(r, re.I) for r in _ip_range_regexes) +ip_range_regexes = [re.compile(r, re.I) for r in _ip_range_regexes] # dns names with periods -_dns_name_regex = r"(?:\w(?:[\w-]{0,100}\w)?\.)+(?:[xX][nN]--)?[^\W_]{1,63}\.?" -dns_name_regex = re.compile(_dns_name_regex, re.I) - -# dns names without periods -_hostname_regex = r"(?!\w*\.\w+)\w(?:[\w-]{0,100}\w)?" -hostname_regex = re.compile(r"^" + _hostname_regex + r"$", re.I) +_dns_name_regex = r"(?:\w(?:[\w-]{0,100}\w)?\.?)+(?:[xX][nN]--)?[^\W_]{1,63}\.?" +dns_name_extraction_regex = re.compile(_dns_name_regex, re.I) +dns_name_validation_regex = re.compile(r"^" + _dns_name_regex + r"$", re.I) _email_regex = r"(?:[^\W_][\w\-\.\+']{,100})@" + _dns_name_regex email_regex = re.compile(_email_regex, re.I) @@ -60,17 +57,15 @@ _open_port_regexes = ( _dns_name_regex + r":[0-9]{1,5}", - _hostname_regex + r":[0-9]{1,5}", r"\[" + _ipv6_regex + r"\]:[0-9]{1,5}", ) -open_port_regexes = list(re.compile(r, re.I) for r in _open_port_regexes) +open_port_regexes = [re.compile(r, re.I) for r in _open_port_regexes] _url_regexes = ( r"https?://" + _dns_name_regex + r"(?::[0-9]{1,5})?(?:(?:/|\?).*)?", - r"https?://" + _hostname_regex + r"(?::[0-9]{1,5})?(?:(?:/|\?).*)?", r"https?://\[" + _ipv6_regex + r"\](?::[0-9]{1,5})?(?:(?:/|\?).*)?", ) -url_regexes = list(re.compile(r, re.I) for r in _url_regexes) +url_regexes = [re.compile(r, re.I) for r in _url_regexes] _double_slash_regex = r"/{2,}" double_slash_regex = re.compile(_double_slash_regex) @@ -82,10 +77,7 @@ for k, regexes in ( ( "DNS_NAME", - ( - r"^" + _dns_name_regex + r"$", - r"^" + _hostname_regex + r"$", - ), + (r"^" + _dns_name_regex + r"$",), ), ( "EMAIL_ADDRESS", @@ -117,7 +109,7 @@ scan_name_regex = re.compile(r"[a-z]{3,20}_[a-z]{3,20}") -# For use with excavate paramaters extractor +# For use with excavate parameters extractor input_tag_regex = re.compile( r"]+?name=[\"\']?([\.$\w]+)[\"\']?(?:[^>]*?value=[\"\']([=+\/\w]*)[\"\'])?[^>]*>" ) @@ -139,7 +131,7 @@ textarea_tag_regex = re.compile( r']*\bname=["\']?(\w+)["\']?[^>]*>(.*?)', re.IGNORECASE | re.DOTALL ) -tag_attribute_regex = re.compile(r"<[^>]*(?:href|src)\s*=\s*[\"\']([^\"\']+)[\"\'][^>]*>") +tag_attribute_regex = re.compile(r"<[^>]*(?:href|action|src)\s*=\s*[\"\']?(?!mailto:)([^\s\'\"\>]+)[\"\']?[^>]*>") valid_netloc = r"[^\s!@#$%^&()=/?\\'\";~`<>]+" diff --git a/bbot/core/helpers/validators.py b/bbot/core/helpers/validators.py index 417683adf2..88acb01462 100644 --- a/bbot/core/helpers/validators.py +++ b/bbot/core/helpers/validators.py @@ -132,7 +132,7 @@ def validate_host(host: Union[str, ipaddress.IPv4Address, ipaddress.IPv6Address] @validator def validate_severity(severity: str): severity = str(severity).strip().upper() - if not severity in ("UNKNOWN", "INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"): + if severity not in ("UNKNOWN", "INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"): raise ValueError(f"Invalid severity: {severity}") return severity @@ -299,7 +299,6 @@ def is_email(email): class Validators: - def __init__(self, parent_helper): self.parent_helper = parent_helper diff --git a/bbot/core/helpers/web/client.py b/bbot/core/helpers/web/client.py index c09a0e4856..28788e04d9 100644 --- a/bbot/core/helpers/web/client.py +++ b/bbot/core/helpers/web/client.py @@ -56,7 +56,7 @@ def __init__(self, *args, **kwargs): # timeout http_timeout = self._web_config.get("http_timeout", 20) - if not "timeout" in kwargs: + if "timeout" not in kwargs: kwargs["timeout"] = http_timeout # headers diff --git a/bbot/core/helpers/web/engine.py b/bbot/core/helpers/web/engine.py index 6d13d775c8..4401a219fd 100644 --- a/bbot/core/helpers/web/engine.py +++ b/bbot/core/helpers/web/engine.py @@ -14,7 +14,6 @@ class HTTPEngine(EngineServer): - CMDS = { 0: "request", 1: "request_batch", @@ -138,7 +137,7 @@ async def stream_request(self, url, **kwargs): if max_size is not None: max_size = human_to_bytes(max_size) kwargs["follow_redirects"] = follow_redirects - if not "method" in kwargs: + if "method" not in kwargs: kwargs["method"] = "GET" try: total_size = 0 diff --git a/bbot/core/helpers/web/web.py b/bbot/core/helpers/web/web.py index b05b2d798f..6c712e1e3e 100644 --- a/bbot/core/helpers/web/web.py +++ b/bbot/core/helpers/web/web.py @@ -19,7 +19,6 @@ class WebHelper(EngineClient): - SERVER_CLASS = HTTPEngine ERROR_CLASS = WebError @@ -58,7 +57,7 @@ def __init__(self, parent_helper): self.ssl_verify = self.config.get("ssl_verify", False) engine_debug = self.config.get("engine", {}).get("debug", False) super().__init__( - server_kwargs={"config": self.config, "target": self.parent_helper.preset.target.radix_only}, + server_kwargs={"config": self.config, "target": self.parent_helper.preset.target.minimal}, debug=engine_debug, ) @@ -262,7 +261,7 @@ async def wordlist(self, path, lines=None, **kwargs): """ if not path: raise WordlistError(f"Invalid wordlist: {path}") - if not "cache_hrs" in kwargs: + if "cache_hrs" not in kwargs: kwargs["cache_hrs"] = 720 if self.parent_helper.is_url(path): filename = await self.download(str(path), **kwargs) @@ -351,7 +350,7 @@ async def curl(self, *args, **kwargs): headers[hk] = hv # add the timeout - if not "timeout" in kwargs: + if "timeout" not in kwargs: timeout = http_timeout curl_command.append("-m") @@ -452,7 +451,7 @@ def beautifulsoup( Perform an html parse of the 'markup' argument and return a soup instance >>> email_type = soup.find(type="email") - Searches the soup instance for all occurances of the passed in argument + Searches the soup instance for all occurrences of the passed in argument """ try: soup = BeautifulSoup( diff --git a/bbot/core/helpers/wordcloud.py b/bbot/core/helpers/wordcloud.py index fbd4e75930..a5d9b9aaaf 100644 --- a/bbot/core/helpers/wordcloud.py +++ b/bbot/core/helpers/wordcloud.py @@ -111,7 +111,7 @@ def mutations( results = set() for word in words: h = hash(word) - if not h in results: + if h not in results: results.add(h) yield (word,) if numbers > 0: @@ -119,7 +119,7 @@ def mutations( for word in words: for number_mutation in self.get_number_mutations(word, n=numbers, padding=number_padding): h = hash(number_mutation) - if not h in results: + if h not in results: results.add(h) yield (number_mutation,) for word in words: @@ -322,7 +322,7 @@ def json(self, limit=None): @property def default_filename(self): - return self.parent_helper.preset.scan.home / f"wordcloud.tsv" + return self.parent_helper.preset.scan.home / "wordcloud.tsv" def save(self, filename=None, limit=None): """ @@ -357,7 +357,7 @@ def save(self, filename=None, limit=None): log.debug(f"Saved word cloud ({len(self):,} words) to {filename}") return True, filename else: - log.debug(f"No words to save") + log.debug("No words to save") except Exception as e: import traceback @@ -421,7 +421,7 @@ def mutations(self, words, max_mutations=None): def mutate(self, word, max_mutations=None, mutations=None): if mutations is None: mutations = self.top_mutations(max_mutations) - for mutation, count in mutations.items(): + for mutation in mutations.keys(): ret = [] for s in mutation: if s is not None: diff --git a/bbot/core/modules.py b/bbot/core/modules.py index 7fd38a33f4..c83d34a96f 100644 --- a/bbot/core/modules.py +++ b/bbot/core/modules.py @@ -153,7 +153,7 @@ def preload(self, module_dirs=None): else: log.debug(f"Preloading {module_name} from disk") if module_dir.name == "modules": - namespace = f"bbot.modules" + namespace = "bbot.modules" else: namespace = f"bbot.modules.{module_dir.name}" try: @@ -235,7 +235,7 @@ def _preloaded(self): return self.__preloaded def get_recursive_dirs(self, *dirs): - dirs = set(Path(d).resolve() for d in dirs) + dirs = {Path(d).resolve() for d in dirs} for d in list(dirs): if not d.is_dir(): continue @@ -337,74 +337,73 @@ def preload_module(self, module_file): # look for classes if type(root_element) == ast.ClassDef: for class_attr in root_element.body: - # class attributes that are dictionaries if type(class_attr) == ast.Assign and type(class_attr.value) == ast.Dict: # module options - if any([target.id == "options" for target in class_attr.targets]): + if any(target.id == "options" for target in class_attr.targets): config.update(ast.literal_eval(class_attr.value)) # module options - elif any([target.id == "options_desc" for target in class_attr.targets]): + elif any(target.id == "options_desc" for target in class_attr.targets): options_desc.update(ast.literal_eval(class_attr.value)) # module metadata - elif any([target.id == "meta" for target in class_attr.targets]): + elif any(target.id == "meta" for target in class_attr.targets): meta = ast.literal_eval(class_attr.value) # class attributes that are lists if type(class_attr) == ast.Assign and type(class_attr.value) == ast.List: # flags - if any([target.id == "flags" for target in class_attr.targets]): + if any(target.id == "flags" for target in class_attr.targets): for flag in class_attr.value.elts: if type(flag.value) == str: flags.add(flag.value) # watched events - elif any([target.id == "watched_events" for target in class_attr.targets]): + elif any(target.id == "watched_events" for target in class_attr.targets): for event_type in class_attr.value.elts: if type(event_type.value) == str: watched_events.add(event_type.value) # produced events - elif any([target.id == "produced_events" for target in class_attr.targets]): + elif any(target.id == "produced_events" for target in class_attr.targets): for event_type in class_attr.value.elts: if type(event_type.value) == str: produced_events.add(event_type.value) # bbot module dependencies - elif any([target.id == "deps_modules" for target in class_attr.targets]): + elif any(target.id == "deps_modules" for target in class_attr.targets): for dep_module in class_attr.value.elts: if type(dep_module.value) == str: deps_modules.add(dep_module.value) # python dependencies - elif any([target.id == "deps_pip" for target in class_attr.targets]): + elif any(target.id == "deps_pip" for target in class_attr.targets): for dep_pip in class_attr.value.elts: if type(dep_pip.value) == str: deps_pip.append(dep_pip.value) - elif any([target.id == "deps_pip_constraints" for target in class_attr.targets]): + elif any(target.id == "deps_pip_constraints" for target in class_attr.targets): for dep_pip in class_attr.value.elts: if type(dep_pip.value) == str: deps_pip_constraints.append(dep_pip.value) # apt dependencies - elif any([target.id == "deps_apt" for target in class_attr.targets]): + elif any(target.id == "deps_apt" for target in class_attr.targets): for dep_apt in class_attr.value.elts: if type(dep_apt.value) == str: deps_apt.append(dep_apt.value) # bash dependencies - elif any([target.id == "deps_shell" for target in class_attr.targets]): + elif any(target.id == "deps_shell" for target in class_attr.targets): for dep_shell in class_attr.value.elts: deps_shell.append(ast.literal_eval(dep_shell)) # ansible playbook - elif any([target.id == "deps_ansible" for target in class_attr.targets]): + elif any(target.id == "deps_ansible" for target in class_attr.targets): ansible_tasks = ast.literal_eval(class_attr.value) # shared/common module dependencies - elif any([target.id == "deps_common" for target in class_attr.targets]): + elif any(target.id == "deps_common" for target in class_attr.targets): for dep_common in class_attr.value.elts: if type(dep_common.value) == str: deps_common.append(dep_common.value) for task in ansible_tasks: - if not "become" in task: + if "become" not in task: task["become"] = False # don't sudo brew - elif os_platform() == "darwin" and ("package" in task and task.get("become", False) == True): + elif os_platform() == "darwin" and ("package" in task and task.get("become", False) is True): task["become"] = False preloaded_data = { @@ -437,8 +436,8 @@ def preload_module(self, module_file): f'Error while preloading module "{module_file}": No shared dependency named "{dep_common}" (choices: {common_choices})' ) for ansible_task in ansible_task_list: - if any(x == True for x in search_dict_by_key("become", ansible_task)) or any( - x == True for x in search_dict_by_key("ansible_become", ansible_tasks) + if any(x is True for x in search_dict_by_key("become", ansible_task)) or any( + x is True for x in search_dict_by_key("ansible_become", ansible_tasks) ): preloaded_data["sudo"] = True return preloaded_data @@ -541,7 +540,7 @@ def recommend_dependencies(self, modules): with suppress(KeyError): choices.remove(modname) if event_type not in resolve_choices: - resolve_choices[event_type] = dict() + resolve_choices[event_type] = {} deps = resolve_choices[event_type] self.add_or_create(deps, "required_by", modname) for c in choices: @@ -640,7 +639,7 @@ def modules_options(self, modules=None, mod_type=None): def modules_options_table(self, modules=None, mod_type=None): table = [] header = ["Config Option", "Type", "Description", "Default"] - for module_name, module_options in self.modules_options(modules, mod_type).items(): + for module_options in self.modules_options(modules, mod_type).values(): table += module_options return make_table(table, header) diff --git a/bbot/core/multiprocess.py b/bbot/core/multiprocess.py new file mode 100644 index 0000000000..5b2b2263fb --- /dev/null +++ b/bbot/core/multiprocess.py @@ -0,0 +1,58 @@ +import os +import atexit +from contextlib import suppress + + +class SharedInterpreterState: + """ + A class to track the primary BBOT process. + + Used to prevent spawning multiple unwanted processes with multiprocessing. + """ + + def __init__(self): + self.main_process_var_name = "_BBOT_MAIN_PID" + self.scan_process_var_name = "_BBOT_SCAN_PID" + atexit.register(self.cleanup) + + @property + def is_main_process(self): + is_main_process = self.main_pid == os.getpid() + return is_main_process + + @property + def is_scan_process(self): + is_scan_process = os.getpid() == self.scan_pid + return is_scan_process + + @property + def main_pid(self): + main_pid = int(os.environ.get(self.main_process_var_name, 0)) + if main_pid == 0: + main_pid = os.getpid() + # if main PID is not set, set it to the current PID + os.environ[self.main_process_var_name] = str(main_pid) + return main_pid + + @property + def scan_pid(self): + scan_pid = int(os.environ.get(self.scan_process_var_name, 0)) + if scan_pid == 0: + scan_pid = os.getpid() + # if scan PID is not set, set it to the current PID + os.environ[self.scan_process_var_name] = str(scan_pid) + return scan_pid + + def update_scan_pid(self): + os.environ[self.scan_process_var_name] = str(os.getpid()) + + def cleanup(self): + with suppress(Exception): + if self.is_main_process: + with suppress(KeyError): + del os.environ[self.main_process_var_name] + with suppress(KeyError): + del os.environ[self.scan_process_var_name] + + +SHARED_INTERPRETER_STATE = SharedInterpreterState() diff --git a/bbot/db/sql/models.py b/bbot/db/sql/models.py index 7677a181e2..d6e7656108 100644 --- a/bbot/db/sql/models.py +++ b/bbot/db/sql/models.py @@ -3,9 +3,9 @@ import json import logging -from datetime import datetime from pydantic import ConfigDict from typing import List, Optional +from datetime import datetime, timezone from typing_extensions import Annotated from pydantic.functional_validators import AfterValidator from sqlmodel import inspect, Column, Field, SQLModel, JSON, String, DateTime as SQLADateTime @@ -69,7 +69,6 @@ def __eq__(self, other): class Event(BBOTBaseModel, table=True): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) data = self._get_data(self.data, self.type) @@ -114,6 +113,7 @@ def _get_data(data, type): discovery_context: str = "" discovery_path: List[str] = Field(default=[], sa_type=JSON) parent_chain: List[str] = Field(default=[], sa_type=JSON) + inserted_at: NaiveUTC = Field(default_factory=lambda: datetime.now(timezone.utc)) ### SCAN ### @@ -140,8 +140,8 @@ class Target(BBOTBaseModel, table=True): seeds: List = Field(default=[], sa_type=JSON) whitelist: List = Field(default=None, sa_type=JSON) blacklist: List = Field(default=[], sa_type=JSON) - hash: str = Field(sa_column=Column("hash", String, unique=True, primary_key=True, index=True)) - scope_hash: str = Field(sa_column=Column("scope_hash", String, index=True)) - seed_hash: str = Field(sa_column=Column("seed_hashhash", String, index=True)) - whitelist_hash: str = Field(sa_column=Column("whitelist_hash", String, index=True)) - blacklist_hash: str = Field(sa_column=Column("blacklist_hash", String, index=True)) + hash: str = Field(sa_column=Column("hash", String(length=255), unique=True, primary_key=True, index=True)) + scope_hash: str = Field(sa_column=Column("scope_hash", String(length=255), index=True)) + seed_hash: str = Field(sa_column=Column("seed_hashhash", String(length=255), index=True)) + whitelist_hash: str = Field(sa_column=Column("whitelist_hash", String(length=255), index=True)) + blacklist_hash: str = Field(sa_column=Column("blacklist_hash", String(length=255), index=True)) diff --git a/bbot/defaults.yml b/bbot/defaults.yml index ca215a1edf..61638595a0 100644 --- a/bbot/defaults.yml +++ b/bbot/defaults.yml @@ -14,6 +14,9 @@ folder_blobs: false ### SCOPE ### scope: + # strict scope means only exact DNS names are considered in-scope + # subdomains are not included unless they are explicitly provided in the target list + strict: false # Filter by scope distance which events are displayed in the output # 0 == show only in-scope events (affiliates are always shown) # 1 == show all events up to distance-1 (1 hop from target) @@ -71,7 +74,7 @@ dns: web: # HTTP proxy - http_proxy: + http_proxy: # Web user-agent user_agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.2151.97 # Set the maximum number of HTTP links that can be followed in a row (0 == no spidering allowed) @@ -109,6 +112,13 @@ engine: deps: ffuf: version: "2.1.0" + # How to handle installation of module dependencies + # Choices are: + # - abort_on_failure (default) - if a module dependency fails to install, abort the scan + # - retry_failed - try again to install failed dependencies + # - ignore_failed - run the scan regardless of what happens with dependency installation + # - disable - completely disable BBOT's dependency system (you are responsible for installing tools, pip packages, etc.) + behavior: abort_on_failure ### ADVANCED OPTIONS ### @@ -126,14 +136,6 @@ dnsresolve: True # Cloud provider tagging cloudcheck: True -# How to handle installation of module dependencies -# Choices are: -# - abort_on_failure (default) - if a module dependency fails to install, abort the scan -# - retry_failed - try again to install failed dependencies -# - ignore_failed - run the scan regardless of what happens with dependency installation -# - disable - completely disable BBOT's dependency system (you are responsible for installing tools, pip packages, etc.) -deps_behavior: abort_on_failure - # Strip querystring from URLs by default url_querystring_remove: True # When query string is retained, by default collapse parameter values down to a single value per parameter diff --git a/bbot/modules/anubisdb.py b/bbot/modules/anubisdb.py index b456365e55..597f5520d3 100644 --- a/bbot/modules/anubisdb.py +++ b/bbot/modules/anubisdb.py @@ -38,7 +38,7 @@ async def abort_if(self, event): return True, "DNS name is unresolved" return await super().abort_if(event) - def parse_results(self, r, query): + async def parse_results(self, r, query): results = set() json = r.json() if json: diff --git a/bbot/modules/azure_tenant.py b/bbot/modules/azure_tenant.py index e4be4380d2..f17911c86a 100644 --- a/bbot/modules/azure_tenant.py +++ b/bbot/modules/azure_tenant.py @@ -102,7 +102,7 @@ async def query(self, domain): status_code = getattr(r, "status_code", 0) if status_code not in (200, 421): self.verbose(f'Error retrieving azure_tenant domains for "{domain}" (status code: {status_code})') - return set(), dict() + return set(), {} found_domains = list(set(await self.helpers.re.findall(self.d_xml_regex, r.text))) domains = set() @@ -116,7 +116,7 @@ async def query(self, domain): self.scan.word_cloud.absorb_word(d) r = await openid_task - openid_config = dict() + openid_config = {} with suppress(Exception): openid_config = r.json() diff --git a/bbot/modules/baddns.py b/bbot/modules/baddns.py index 443606f7ea..d0e4c6c1be 100644 --- a/bbot/modules/baddns.py +++ b/bbot/modules/baddns.py @@ -55,7 +55,6 @@ async def setup(self): return True async def handle_event(self, event): - tasks = [] for ModuleClass in self.select_modules(): kwargs = { @@ -75,7 +74,6 @@ async def handle_event(self, event): tasks.append((module_instance, task)) async for completed_task in self.helpers.as_completed([task for _, task in tasks]): - module_instance = next((m for m, t in tasks if t == completed_task), None) try: task_result = await completed_task @@ -116,7 +114,7 @@ async def handle_event(self, event): context=f'{{module}}\'s "{r_dict["module"]}" module found {{event.type}}: {r_dict["description"]}', ) else: - self.warning(f"Got unrecognized confidence level: {r['confidence']}") + self.warning(f"Got unrecognized confidence level: {r_dict['confidence']}") found_domains = r_dict.get("found_domains", None) if found_domains: diff --git a/bbot/modules/baddns_direct.py b/bbot/modules/baddns_direct.py index 33b6b9575f..f8881dedb3 100644 --- a/bbot/modules/baddns_direct.py +++ b/bbot/modules/baddns_direct.py @@ -51,7 +51,6 @@ async def handle_event(self, event): CNAME_direct_instance = CNAME_direct_module(event.host, **kwargs) if await CNAME_direct_instance.dispatch(): - results = CNAME_direct_instance.analyze() if results and len(results) > 0: for r in results: diff --git a/bbot/modules/base.py b/bbot/modules/base.py index 956d59c98c..48d190f29b 100644 --- a/bbot/modules/base.py +++ b/bbot/modules/base.py @@ -51,7 +51,7 @@ class BaseModule: target_only (bool): Accept only the initial target event(s). Default is False. - in_scope_only (bool): Accept only explicitly in-scope events. Default is False. + in_scope_only (bool): Accept only explicitly in-scope events, regardless of the scan's search distance. Default is False. options (Dict): Customizable options for the module, e.g., {"api_key": ""}. Empty dict by default. @@ -311,7 +311,7 @@ async def require_api_key(self): if self.auth_secret: try: await self.ping() - self.hugesuccess(f"API is ready") + self.hugesuccess("API is ready") return True, "" except Exception as e: self.trace(traceback.format_exc()) @@ -332,10 +332,10 @@ def api_key(self, api_keys): def cycle_api_key(self): if len(self._api_keys) > 1: - self.verbose(f"Cycling API key") + self.verbose("Cycling API key") self._api_keys.insert(0, self._api_keys.pop()) else: - self.debug(f"No extra API keys to cycle") + self.debug("No extra API keys to cycle") @property def api_retries(self): @@ -669,7 +669,7 @@ async def _worker(self): if self.incoming_event_queue is not False: event = await self.incoming_event_queue.get() else: - self.debug(f"Event queue is in bad state") + self.debug("Event queue is in bad state") break except asyncio.queues.QueueEmpty: continue @@ -700,7 +700,7 @@ async def _worker(self): else: self.error(f"Critical failure in module {self.name}: {e}") self.error(traceback.format_exc()) - self.log.trace(f"Worker stopped") + self.log.trace("Worker stopped") @property def max_scope_distance(self): @@ -743,7 +743,7 @@ def _event_precheck(self, event): if event.type in ("FINISHED",): return True, "its type is FINISHED" if self.errored: - return False, f"module is in error state" + return False, "module is in error state" # exclude non-watched types if not any(t in self.get_watched_events() for t in ("*", event.type)): return False, "its type is not in watched_events" @@ -770,7 +770,7 @@ async def _event_postcheck(self, event): # check duplicates is_incoming_duplicate, reason = self.is_incoming_duplicate(event, add=True) if is_incoming_duplicate and not self.accept_dupes: - return False, f"module has already seen it" + (f" ({reason})" if reason else "") + return False, "module has already seen it" + (f" ({reason})" if reason else "") return acceptable, reason @@ -863,7 +863,7 @@ async def queue_event(self, event): """ async with self._task_counter.count("queue_event()", _log=False): if self.incoming_event_queue is False: - self.debug(f"Not in an acceptable state to queue incoming event") + self.debug("Not in an acceptable state to queue incoming event") return acceptable, reason = self._event_precheck(event) if not acceptable: @@ -879,7 +879,7 @@ async def queue_event(self, event): if event.type != "FINISHED": self.scan._new_activity = True except AttributeError: - self.debug(f"Not in an acceptable state to queue incoming event") + self.debug("Not in an acceptable state to queue incoming event") async def queue_outgoing_event(self, event, **kwargs): """ @@ -904,7 +904,7 @@ async def queue_outgoing_event(self, event, **kwargs): try: await self.outgoing_event_queue.put((event, kwargs)) except AttributeError: - self.debug(f"Not in an acceptable state to queue outgoing event") + self.debug("Not in an acceptable state to queue outgoing event") def set_error_state(self, message=None, clear_outgoing_queue=False, critical=False): """ @@ -939,7 +939,7 @@ def set_error_state(self, message=None, clear_outgoing_queue=False, critical=Fal self.errored = True # clear incoming queue if self.incoming_event_queue is not False: - self.debug(f"Emptying event_queue") + self.debug("Emptying event_queue") with suppress(asyncio.queues.QueueEmpty): while 1: self.incoming_event_queue.get_nowait() @@ -1126,7 +1126,7 @@ def prepare_api_request(self, url, kwargs): """ if self.api_key: url = url.format(api_key=self.api_key) - if not "headers" in kwargs: + if "headers" not in kwargs: kwargs["headers"] = {} kwargs["headers"]["Authorization"] = f"Bearer {self.api_key}" return url, kwargs @@ -1142,7 +1142,7 @@ async def api_request(self, *args, **kwargs): # loop until we have a successful request for _ in range(self.api_retries): - if not "headers" in kwargs: + if "headers" not in kwargs: kwargs["headers"] = {} new_url, kwargs = self.prepare_api_request(url, kwargs) kwargs["url"] = new_url @@ -1589,7 +1589,7 @@ async def _worker(self): event = incoming kwargs = {} else: - self.debug(f"Event queue is in bad state") + self.debug("Event queue is in bad state") break except asyncio.queues.QueueEmpty: await asyncio.sleep(0.1) @@ -1644,7 +1644,7 @@ async def _worker(self): else: self.critical(f"Critical failure in intercept module {self.name}: {e}") self.critical(traceback.format_exc()) - self.log.trace(f"Worker stopped") + self.log.trace("Worker stopped") async def get_incoming_event(self): """ @@ -1675,7 +1675,7 @@ async def queue_event(self, event, kwargs=None): try: self.incoming_event_queue.put_nowait((event, kwargs)) except AttributeError: - self.debug(f"Not in an acceptable state to queue incoming event") + self.debug("Not in an acceptable state to queue incoming event") async def _event_postcheck(self, event): return await self._event_postcheck_inner(event) diff --git a/bbot/modules/bevigil.py b/bbot/modules/bevigil.py index f3889e7fd4..8e70fe4143 100644 --- a/bbot/modules/bevigil.py +++ b/bbot/modules/bevigil.py @@ -60,14 +60,14 @@ async def request_urls(self, query): url = f"{self.base_url}/{self.helpers.quote(query)}/urls/" return await self.api_request(url) - def parse_subdomains(self, r, query=None): + async def parse_subdomains(self, r, query=None): results = set() subdomains = r.json().get("subdomains") if subdomains: results.update(subdomains) return results - def parse_urls(self, r, query=None): + async def parse_urls(self, r, query=None): results = set() urls = r.json().get("urls") if urls: diff --git a/bbot/modules/binaryedge.py b/bbot/modules/binaryedge.py index e9f6224b6d..e712beec56 100644 --- a/bbot/modules/binaryedge.py +++ b/bbot/modules/binaryedge.py @@ -37,6 +37,6 @@ async def request_url(self, query): url = f"{self.base_url}/query/domains/subdomain/{self.helpers.quote(query)}" return await self.api_request(url) - def parse_results(self, r, query): + async def parse_results(self, r, query): j = r.json() return j.get("events", []) diff --git a/bbot/modules/bufferoverrun.py b/bbot/modules/bufferoverrun.py index 1eba8ad4c3..9523dc6269 100644 --- a/bbot/modules/bufferoverrun.py +++ b/bbot/modules/bufferoverrun.py @@ -33,7 +33,7 @@ async def request_url(self, query): url = f"{self.commercial_base_url if self.commercial else self.base_url}?q=.{query}" return await self.api_request(url) - def parse_results(self, r, query): + async def parse_results(self, r, query): j = r.json() subdomains_set = set() if isinstance(j, dict): @@ -44,5 +44,4 @@ def parse_results(self, r, query): subdomain = parts[4].strip() if subdomain and subdomain.endswith(f".{query}"): subdomains_set.add(subdomain) - for subdomain in subdomains_set: - yield subdomain + return subdomains_set diff --git a/bbot/modules/builtwith.py b/bbot/modules/builtwith.py index 19e880034c..9887f18225 100644 --- a/bbot/modules/builtwith.py +++ b/bbot/modules/builtwith.py @@ -62,7 +62,7 @@ async def request_redirects(self, query): url = f"{self.base_url}/redirect1/api.json?KEY={{api_key}}&LOOKUP={query}" return await self.api_request(url) - def parse_domains(self, r, query): + async def parse_domains(self, r, query): """ This method returns a set of subdomains. Each subdomain is an "FQDN" that was reported in the "Detailed Technology Profile" page on builtwith.com @@ -92,7 +92,7 @@ def parse_domains(self, r, query): self.verbose(f"No results for {query}: {error}") return results_set - def parse_redirects(self, r, query): + async def parse_redirects(self, r, query): """ This method creates a set. Each entry in the set is either an Inbound or Outbound Redirect reported in the "Redirect Profile" page on builtwith.com diff --git a/bbot/modules/bypass403.py b/bbot/modules/bypass403.py index 4f3b51789b..61fb510775 100644 --- a/bbot/modules/bypass403.py +++ b/bbot/modules/bypass403.py @@ -92,7 +92,7 @@ async def do_checks(self, compare_helper, event, collapse_threshold): return None sig = self.format_signature(sig, event) - if sig[2] != None: + if sig[2] is not None: headers = dict(sig[2]) else: headers = None @@ -106,13 +106,13 @@ async def do_checks(self, compare_helper, event, collapse_threshold): continue # In some cases WAFs will respond with a 200 code which causes a false positive - if subject_response != None: + if subject_response is not None: for ws in waf_strings: if ws in subject_response.text: self.debug("Rejecting result based on presence of WAF string") return - if match == False: + if match is False: if str(subject_response.status_code)[0] != "4": if sig[2]: added_header_tuple = next(iter(sig[2].items())) @@ -165,13 +165,13 @@ async def filter_event(self, event): return False def format_signature(self, sig, event): - if sig[3] == True: + if sig[3] is True: cleaned_path = event.parsed_url.path.strip("/") else: cleaned_path = event.parsed_url.path.lstrip("/") kwargs = {"scheme": event.parsed_url.scheme, "netloc": event.parsed_url.netloc, "path": cleaned_path} formatted_url = sig[1].format(**kwargs) - if sig[2] != None: + if sig[2] is not None: formatted_headers = {k: v.format(**kwargs) for k, v in sig[2].items()} else: formatted_headers = None diff --git a/bbot/modules/c99.py b/bbot/modules/c99.py index 7e703966b3..17fea87a13 100644 --- a/bbot/modules/c99.py +++ b/bbot/modules/c99.py @@ -20,13 +20,14 @@ class c99(subdomain_enum_apikey): async def ping(self): url = f"{self.base_url}/randomnumber?key={{api_key}}&between=1,100&json" response = await self.api_request(url) - assert response.json()["success"] == True, getattr(response, "text", "no response from server") + assert response.json()["success"] is True, getattr(response, "text", "no response from server") async def request_url(self, query): url = f"{self.base_url}/subdomainfinder?key={{api_key}}&domain={self.helpers.quote(query)}&json" return await self.api_request(url) - def parse_results(self, r, query): + async def parse_results(self, r, query): + results = set() j = r.json() if isinstance(j, dict): subdomains = j.get("subdomains", []) @@ -34,4 +35,5 @@ def parse_results(self, r, query): for s in subdomains: subdomain = s.get("subdomain", "") if subdomain: - yield subdomain + results.add(subdomain) + return results diff --git a/bbot/modules/censys.py b/bbot/modules/censys.py index cb8a7c9560..3779363fa5 100644 --- a/bbot/modules/censys.py +++ b/bbot/modules/censys.py @@ -15,25 +15,21 @@ class censys(subdomain_enum_apikey): "author": "@TheTechromancer", "auth_required": True, } - options = {"api_id": "", "api_secret": "", "max_pages": 5} + options = {"api_key": "", "max_pages": 5} options_desc = { - "api_id": "Censys.io API ID", - "api_secret": "Censys.io API Secret", + "api_key": "Censys.io API Key in the format of 'key:secret'", "max_pages": "Maximum number of pages to fetch (100 results per page)", } base_url = "https://search.censys.io/api" async def setup(self): - self.api_id = self.config.get("api_id", "") - self.api_secret = self.config.get("api_secret", "") - self.auth = (self.api_id, self.api_secret) self.max_pages = self.config.get("max_pages", 5) return await super().setup() async def ping(self): url = f"{self.base_url}/v1/account" - resp = await self.helpers.request(url, auth=self.auth) + resp = await self.api_request(url) d = resp.json() assert isinstance(d, dict), f"Invalid response from {url}: {resp}" quota = d.get("quota", {}) @@ -41,6 +37,11 @@ async def ping(self): allowance = int(quota.get("allowance", 0)) assert used < allowance, "No quota remaining" + def prepare_api_request(self, url, kwargs): + api_id, api_secret = self.api_key.split(":", 1) + kwargs["auth"] = (api_id, api_secret) + return url, kwargs + async def query(self, query): results = set() cursor = "" @@ -52,11 +53,10 @@ async def query(self, query): } if cursor: json_data.update({"cursor": cursor}) - resp = await self.helpers.request( + resp = await self.api_request( url, method="POST", json=json_data, - auth=self.auth, ) if resp is None: @@ -96,7 +96,3 @@ async def query(self, query): break return results - - @property - def auth_secret(self): - return self.api_id and self.api_secret diff --git a/bbot/modules/certspotter.py b/bbot/modules/certspotter.py index d4d7703659..c6cbc6eb6d 100644 --- a/bbot/modules/certspotter.py +++ b/bbot/modules/certspotter.py @@ -17,9 +17,11 @@ def request_url(self, query): url = f"{self.base_url}/issuances?domain={self.helpers.quote(query)}&include_subdomains=true&expand=dns_names" return self.api_request(url, timeout=self.http_timeout + 30) - def parse_results(self, r, query): + async def parse_results(self, r, query): + results = set() json = r.json() if json: for r in json: for dns_name in r.get("dns_names", []): - yield dns_name.lstrip(".*").rstrip(".") + results.add(dns_name.lstrip(".*").rstrip(".")) + return results diff --git a/bbot/modules/chaos.py b/bbot/modules/chaos.py index cba4e7ea4c..15a321046a 100644 --- a/bbot/modules/chaos.py +++ b/bbot/modules/chaos.py @@ -26,7 +26,8 @@ async def request_url(self, query): url = f"{self.base_url}/{domain}/subdomains" return await self.api_request(url) - def parse_results(self, r, query): + async def parse_results(self, r, query): + results = set() j = r.json() subdomains_set = set() if isinstance(j, dict): @@ -39,4 +40,5 @@ def parse_results(self, r, query): for s in subdomains_set: full_subdomain = f"{s}.{domain}" if full_subdomain and full_subdomain.endswith(f".{query}"): - yield full_subdomain + results.add(full_subdomain) + return results diff --git a/bbot/modules/columbus.py b/bbot/modules/columbus.py index 6e3e9ce0b4..0a33ee12a6 100644 --- a/bbot/modules/columbus.py +++ b/bbot/modules/columbus.py @@ -17,9 +17,9 @@ async def request_url(self, query): url = f"{self.base_url}/{self.helpers.quote(query)}?days=365" return await self.api_request(url) - def parse_results(self, r, query): + async def parse_results(self, r, query): results = set() json = r.json() if json and isinstance(json, list): - return set([f"{s.lower()}.{query}" for s in json]) + return {f"{s.lower()}.{query}" for s in json} return results diff --git a/bbot/modules/crt.py b/bbot/modules/crt.py index 441dbbb9ba..05735c4e93 100644 --- a/bbot/modules/crt.py +++ b/bbot/modules/crt.py @@ -23,7 +23,8 @@ async def request_url(self, query): url = self.helpers.add_get_params(self.base_url, params).geturl() return await self.api_request(url, timeout=self.http_timeout + 30) - def parse_results(self, r, query): + async def parse_results(self, r, query): + results = set() j = r.json() for cert_info in j: if not type(cert_info) == dict: @@ -35,4 +36,5 @@ def parse_results(self, r, query): domain = cert_info.get("name_value") if domain: for d in domain.splitlines(): - yield d.lower() + results.add(d.lower()) + return results diff --git a/bbot/modules/deadly/ffuf.py b/bbot/modules/deadly/ffuf.py index 6144d0b13e..0351fe93aa 100644 --- a/bbot/modules/deadly/ffuf.py +++ b/bbot/modules/deadly/ffuf.py @@ -28,7 +28,7 @@ class ffuf(BaseModule): deps_common = ["ffuf"] - banned_characters = set([" "]) + banned_characters = {" "} blacklist = ["images", "css", "image"] in_scope_only = True @@ -52,7 +52,7 @@ async def setup(self): async def handle_event(self, event): if self.helpers.url_depth(event.data) > self.config.get("max_depth"): - self.debug(f"Exceeded max depth, aborting event") + self.debug("Exceeded max depth, aborting event") return # only FFUF against a directory @@ -122,7 +122,7 @@ async def baseline_ffuf(self, url, exts=[""], prefix="", suffix="", mode="normal continue # if the codes are different, we should abort, this should also be a warning, as it is highly unusual behavior - if len(set(d["status"] for d in canary_results)) != 1: + if len({d["status"] for d in canary_results}) != 1: self.warning("Got different codes for each baseline. This could indicate load balancing") filters[ext] = ["ABORT", "BASELINE_CHANGED_CODES"] continue @@ -148,7 +148,7 @@ async def baseline_ffuf(self, url, exts=[""], prefix="", suffix="", mode="normal continue # we start by seeing if all of the baselines have the same character count - if len(set(d["length"] for d in canary_results)) == 1: + if len({d["length"] for d in canary_results}) == 1: self.debug("All baseline results had the same char count, we can make a filter on that") filters[ext] = [ "-fc", @@ -161,7 +161,7 @@ async def baseline_ffuf(self, url, exts=[""], prefix="", suffix="", mode="normal continue # if that doesn't work we can try words - if len(set(d["words"] for d in canary_results)) == 1: + if len({d["words"] for d in canary_results}) == 1: self.debug("All baseline results had the same word count, we can make a filter on that") filters[ext] = [ "-fc", @@ -174,7 +174,7 @@ async def baseline_ffuf(self, url, exts=[""], prefix="", suffix="", mode="normal continue # as a last resort we will try lines - if len(set(d["lines"] for d in canary_results)) == 1: + if len({d["lines"] for d in canary_results}) == 1: self.debug("All baseline results had the same word count, we can make a filter on that") filters[ext] = [ "-fc", @@ -252,7 +252,7 @@ async def execute_ffuf( self.warning(f"Exiting from FFUF run early, received an ABORT filter: [{filters[ext][1]}]") continue - elif filters[ext] == None: + elif filters[ext] is None: pass else: @@ -282,7 +282,7 @@ async def execute_ffuf( else: if mode == "normal": # before emitting, we are going to send another baseline. This will immediately catch things like a WAF flipping blocking on us mid-scan - if baseline == False: + if baseline is False: pre_emit_temp_canary = [ f async for f in self.execute_ffuf( diff --git a/bbot/modules/deadly/nuclei.py b/bbot/modules/deadly/nuclei.py index 1eb10cb23a..4f2d73c986 100644 --- a/bbot/modules/deadly/nuclei.py +++ b/bbot/modules/deadly/nuclei.py @@ -15,7 +15,7 @@ class nuclei(BaseModule): } options = { - "version": "3.3.5", + "version": "3.3.7", "tags": "", "templates": "", "severity": "", @@ -226,8 +226,8 @@ async def execute_nuclei(self, nuclei_input): command.append(f"-{cli_option}") command.append(option) - if self.scan.config.get("interactsh_disable") == True: - self.info("Disbling interactsh in accordance with global settings") + if self.scan.config.get("interactsh_disable") is True: + self.info("Disabling interactsh in accordance with global settings") command.append("-no-interactsh") if self.mode == "technology": diff --git a/bbot/modules/deadly/vhost.py b/bbot/modules/deadly/vhost.py index 66c1c516c4..29aa5b6438 100644 --- a/bbot/modules/deadly/vhost.py +++ b/bbot/modules/deadly/vhost.py @@ -23,7 +23,7 @@ class vhost(ffuf): } deps_common = ["ffuf"] - banned_characters = set([" ", "."]) + banned_characters = {" ", "."} in_scope_only = True @@ -73,7 +73,7 @@ async def handle_event(self, event): async def ffuf_vhost(self, host, basehost, event, wordlist=None, skip_dns_host=False): filters = await self.baseline_ffuf(f"{host}/", exts=[""], suffix=basehost, mode="hostheader") - self.debug(f"Baseline completed and returned these filters:") + self.debug("Baseline completed and returned these filters:") self.debug(filters) if not wordlist: wordlist = self.tempfile @@ -90,7 +90,7 @@ async def ffuf_vhost(self, host, basehost, event, wordlist=None, skip_dns_host=F parent=event, context=f"{{module}} brute-forced virtual hosts for {event.data} and found {{event.type}}: {vhost_str}", ) - if skip_dns_host == False: + if skip_dns_host is False: await self.emit_event( f"{vhost_dict['vhost']}{basehost}", "DNS_NAME", diff --git a/bbot/modules/digitorus.py b/bbot/modules/digitorus.py index 48c060346a..049343ac27 100644 --- a/bbot/modules/digitorus.py +++ b/bbot/modules/digitorus.py @@ -19,7 +19,7 @@ async def request_url(self, query): url = f"{self.base_url}/{self.helpers.quote(query)}" return await self.helpers.request(url) - def parse_results(self, r, query): + async def parse_results(self, r, query): results = set() content = getattr(r, "text", "") extract_regex = re.compile(r"[\w.-]+\." + query, re.I) diff --git a/bbot/modules/dnsbimi.py b/bbot/modules/dnsbimi.py index d974b1183e..00c69f8c20 100644 --- a/bbot/modules/dnsbimi.py +++ b/bbot/modules/dnsbimi.py @@ -19,7 +19,7 @@ # # NOTE: .svg file extensions are filtered from inclusion by default, modify "url_extension_blacklist" appropriately if you want the .svg image to be considered for download. # -# NOTE: use the "filedownload" module if you to download .svg and .pem files. .pem will be downloaded by defaut, .svg will require a customised configuration for that module. +# NOTE: use the "filedownload" module if you to download .svg and .pem files. .pem will be downloaded by default, .svg will require a customised configuration for that module. # # The domain portion of any URL_UNVERIFIED's will be extracted by the various internal modules if .svg is not filtered. # @@ -80,7 +80,7 @@ async def filter_event(self, event): return False, "event is wildcard" # there's no value in inspecting service records - if service_record(event.host) == True: + if service_record(event.host) is True: return False, "service record detected" return True diff --git a/bbot/modules/dnsbrute_mutations.py b/bbot/modules/dnsbrute_mutations.py index ef0b7a0337..5109798e57 100644 --- a/bbot/modules/dnsbrute_mutations.py +++ b/bbot/modules/dnsbrute_mutations.py @@ -1,3 +1,5 @@ +import time + from bbot.modules.base import BaseModule @@ -40,8 +42,11 @@ async def handle_event(self, event): except KeyError: self.found[domain] = {subdomain} - def get_parent_event(self, subdomain): - parent_host = self.helpers.closest_match(subdomain, self.parent_events) + async def get_parent_event(self, subdomain): + start = time.time() + parent_host = await self.helpers.run_in_executor(self.helpers.closest_match, subdomain, self.parent_events) + elapsed = time.time() - start + self.trace(f"{subdomain}: got closest match among {len(self.parent_events):,} parent events in {elapsed:.2f}s") return self.parent_events[parent_host] async def finish(self): @@ -124,7 +129,7 @@ def add_mutation(m): self._mutation_run_counter[domain] = mutation_run = 1 self._mutation_run_counter[domain] += 1 for hostname in results: - parent_event = self.get_parent_event(hostname) + parent_event = await self.get_parent_event(hostname) mutation_run_ordinal = self.helpers.integer_to_ordinal(mutation_run) await self.emit_event( hostname, diff --git a/bbot/modules/dnscaa.py b/bbot/modules/dnscaa.py index 1d18a811ad..1465cd8faf 100644 --- a/bbot/modules/dnscaa.py +++ b/bbot/modules/dnscaa.py @@ -2,7 +2,7 @@ # # Checks for and parses CAA DNS TXT records for IODEF reporting destination email addresses and/or URL's. # -# NOTE: when the target domain is initially resolved basic "dns_name_regex" matched targets will be extracted so we do not perform that again here. +# NOTE: when the target domain is initially resolved basic "dns_name_extraction_regex" matched targets will be extracted so we do not perform that again here. # # Example CAA records, # 0 iodef "mailto:dnsadmin@example.com" @@ -23,7 +23,7 @@ import re -from bbot.core.helpers.regexes import dns_name_regex, email_regex, url_regexes +from bbot.core.helpers.regexes import dns_name_extraction_regex, email_regex, url_regexes # Handle '0 iodef "mailto:support@hcaptcha.com"' # Handle '1 iodef "https://some.host.tld/caa;"' @@ -109,7 +109,7 @@ async def handle_event(self, event): elif caa_match.group("property").lower().startswith("issue"): if self._dns_names: - for match in dns_name_regex.finditer(caa_match.group("text")): + for match in dns_name_extraction_regex.finditer(caa_match.group("text")): start, end = match.span() name = caa_match.group("text")[start:end] diff --git a/bbot/modules/dnsdumpster.py b/bbot/modules/dnsdumpster.py index ab36b493e8..5c0ae29041 100644 --- a/bbot/modules/dnsdumpster.py +++ b/bbot/modules/dnsdumpster.py @@ -31,7 +31,7 @@ async def query(self, domain): html = self.helpers.beautifulsoup(res1.content, "html.parser") if html is False: - self.verbose(f"BeautifulSoup returned False") + self.verbose("BeautifulSoup returned False") return ret csrftoken = None @@ -82,7 +82,7 @@ async def query(self, domain): return ret html = self.helpers.beautifulsoup(res2.content, "html.parser") if html is False: - self.verbose(f"BeautifulSoup returned False") + self.verbose("BeautifulSoup returned False") return ret escaped_domain = re.escape(domain) match_pattern = re.compile(r"^[\w\.-]+\." + escaped_domain + r"$") diff --git a/bbot/modules/dnstlsrpt.py b/bbot/modules/dnstlsrpt.py new file mode 100644 index 0000000000..4232cc921f --- /dev/null +++ b/bbot/modules/dnstlsrpt.py @@ -0,0 +1,144 @@ +# dnstlsrpt.py +# +# Checks for and parses common TLS-RPT TXT records, e.g. _smtp._tls.target.domain +# +# TLS-RPT policies may contain email addresses or URL's for reporting destinations, typically the email addresses are software processed inboxes, but they may also be to individual humans or team inboxes. +# +# The domain portion of any email address or URL is also passively checked and added as appropriate, for additional inspection by other modules. +# +# Example records, +# _smtp._tls.example.com TXT "v=TLSRPTv1;rua=https://tlsrpt.azurewebsites.net/report" +# _smtp._tls.example.net TXT "v=TLSRPTv1; rua=mailto:sts-reports@example.net;" +# +# TODO: extract %{UNIQUE_ID}% from hosted services as ORG_STUB ? +# e.g. %{UNIQUE_ID}%@tlsrpt.hosted.service.provider is usually a tenant specific ID. +# e.g. tlsrpt@%{UNIQUE_ID}%.hosted.service.provider is usually a tenant specific ID. + +from bbot.modules.base import BaseModule +from bbot.core.helpers.dns.helpers import service_record + +import re + +from bbot.core.helpers.regexes import email_regex, url_regexes + +_tlsrpt_regex = r"^v=(?PTLSRPTv[0-9]+); *(?P.*)$" +tlsrpt_regex = re.compile(_tlsrpt_regex, re.I) + +_tlsrpt_kvp_regex = r"(?P\w+)=(?P[^;]+);*" +tlsrpt_kvp_regex = re.compile(_tlsrpt_kvp_regex) + +_csul = r"(?P[^, ]+)" +csul = re.compile(_csul) + + +class dnstlsrpt(BaseModule): + watched_events = ["DNS_NAME"] + produced_events = ["EMAIL_ADDRESS", "URL_UNVERIFIED", "RAW_DNS_RECORD"] + flags = ["subdomain-enum", "cloud-enum", "email-enum", "passive", "safe"] + meta = { + "description": "Check for TLS-RPT records", + "author": "@colin-stubbs", + "created_date": "2024-07-26", + } + options = { + "emit_emails": True, + "emit_raw_dns_records": False, + "emit_urls": True, + "emit_vulnerabilities": True, + } + options_desc = { + "emit_emails": "Emit EMAIL_ADDRESS events", + "emit_raw_dns_records": "Emit RAW_DNS_RECORD events", + "emit_urls": "Emit URL_UNVERIFIED events", + "emit_vulnerabilities": "Emit VULNERABILITY events", + } + + async def setup(self): + self.emit_emails = self.config.get("emit_emails", True) + self.emit_raw_dns_records = self.config.get("emit_raw_dns_records", False) + self.emit_urls = self.config.get("emit_urls", True) + self.emit_vulnerabilities = self.config.get("emit_vulnerabilities", True) + return await super().setup() + + def _incoming_dedup_hash(self, event): + # dedupe by parent + parent_domain = self.helpers.parent_domain(event.data) + return hash(parent_domain), "already processed parent domain" + + async def filter_event(self, event): + if "_wildcard" in str(event.host).split("."): + return False, "event is wildcard" + + # there's no value in inspecting service records + if service_record(event.host) is True: + return False, "service record detected" + + return True + + async def handle_event(self, event): + rdtype = "TXT" + tags = ["tlsrpt-record"] + hostname = f"_smtp._tls.{event.host}" + + r = await self.helpers.resolve_raw(hostname, type=rdtype) + + if r: + raw_results, errors = r + for answer in raw_results: + if self.emit_raw_dns_records: + await self.emit_event( + {"host": hostname, "type": rdtype, "answer": answer.to_text()}, + "RAW_DNS_RECORD", + parent=event, + tags=tags.append(f"{rdtype.lower()}-record"), + context=f"{rdtype} lookup on {hostname} produced {{event.type}}", + ) + + # we need to fix TXT data that may have been split across two different rdata's + # e.g. we will get a single string, but within that string we may have two parts such as: + # answer = '"part 1 that was really long" "part 2 that did not fit in part 1"' + # NOTE: the leading and trailing double quotes are essential as part of a raw DNS TXT record, or another record type that contains a free form text string as a component. + s = answer.to_text().strip('"').replace('" "', "") + + # validate TLSRPT record, tag appropriately + tlsrpt_match = tlsrpt_regex.search(s) + + if ( + tlsrpt_match + and tlsrpt_match.group("v") + and tlsrpt_match.group("kvps") + and tlsrpt_match.group("kvps") != "" + ): + for kvp_match in tlsrpt_kvp_regex.finditer(tlsrpt_match.group("kvps")): + key = kvp_match.group("k").lower() + + if key == "rua": + for csul_match in csul.finditer(kvp_match.group("v")): + if csul_match.group("uri"): + for match in email_regex.finditer(csul_match.group("uri")): + start, end = match.span() + email = csul_match.group("uri")[start:end] + + if self.emit_emails: + await self.emit_event( + email, + "EMAIL_ADDRESS", + tags=tags.append(f"tlsrpt-record-{key}"), + parent=event, + ) + + for url_regex in url_regexes: + for match in url_regex.finditer(csul_match.group("uri")): + start, end = match.span() + url = csul_match.group("uri")[start:end] + + if self.emit_urls: + await self.emit_event( + url, + "URL_UNVERIFIED", + tags=tags.append(f"tlsrpt-record-{key}"), + parent=event, + ) + + +# EOF diff --git a/bbot/modules/docker_pull.py b/bbot/modules/docker_pull.py index 65594736b4..22888bce3c 100644 --- a/bbot/modules/docker_pull.py +++ b/bbot/modules/docker_pull.py @@ -102,7 +102,7 @@ async def get_tags(self, registry, repository): url = f"{registry}/v2/{repository}/tags/list" r = await self.docker_api_request(url) if r is None or r.status_code != 200: - self.log.warning(f"Could not retrieve all tags for {repository} asuming tag:latest only.") + self.log.warning(f"Could not retrieve all tags for {repository} assuming tag:latest only.") self.log.debug(f"Response: {r}") return ["latest"] try: diff --git a/bbot/modules/dockerhub.py b/bbot/modules/dockerhub.py index b64b88705e..23f45ab756 100644 --- a/bbot/modules/dockerhub.py +++ b/bbot/modules/dockerhub.py @@ -31,7 +31,7 @@ async def handle_event(self, event): async def handle_org_stub(self, event): profile_name = event.data # docker usernames are case sensitive, so if there are capitalizations we also try a lowercase variation - profiles_to_check = set([profile_name, profile_name.lower()]) + profiles_to_check = {profile_name, profile_name.lower()} for p in profiles_to_check: api_url = f"{self.api_url}/users/{p}" api_result = await self.helpers.request(api_url, follow_redirects=True) diff --git a/bbot/modules/dotnetnuke.py b/bbot/modules/dotnetnuke.py index d36b4a014f..eb2485fecd 100644 --- a/bbot/modules/dotnetnuke.py +++ b/bbot/modules/dotnetnuke.py @@ -31,8 +31,7 @@ async def setup(self): self.interactsh_subdomain_tags = {} self.interactsh_instance = None - if self.scan.config.get("interactsh_disable", False) == False: - + if self.scan.config.get("interactsh_disable", False) is False: try: self.interactsh_instance = self.helpers.interactsh() self.interactsh_domain = await self.interactsh_instance.register(callback=self.interactsh_callback) @@ -94,7 +93,7 @@ async def handle_event(self, event): detected = True break - if detected == True: + if detected is True: # DNNPersonalization Deserialization Detection for probe_url in [f'{event.data["url"]}/__', f'{event.data["url"]}/', f'{event.data["url"]}']: result = await self.helpers.request(probe_url, cookies=self.exploit_probe) @@ -114,7 +113,6 @@ async def handle_event(self, event): ) if "endpoint" not in event.tags: - # NewsArticlesSlider ImageHandler.ashx File Read result = await self.helpers.request( f'{event.data["url"]}/DesktopModules/dnnUI_NewsArticlesSlider/ImageHandler.ashx?img=~/web.config' diff --git a/bbot/modules/extractous.py b/bbot/modules/extractous.py index 471e2c07e2..63f4ecd6d1 100644 --- a/bbot/modules/extractous.py +++ b/bbot/modules/extractous.py @@ -28,6 +28,7 @@ class extractous(BaseModule): "ica", # Citrix Independent Computing Architecture File "indd", # Adobe InDesign Document "ini", # Initialization File + "json", # JSON File "key", # Private Key File "pub", # Public Key File "log", # Log File @@ -45,6 +46,7 @@ class extractous(BaseModule): "pptx", # Microsoft PowerPoint Presentation "ps1", # PowerShell Script "rdp", # Remote Desktop Protocol File + "rsa", # RSA Private Key File "sh", # Shell Script "sql", # SQL Database Dump "swp", # Swap File (temporary file, often Vim) @@ -67,7 +69,7 @@ class extractous(BaseModule): scope_distance_modifier = 1 async def setup(self): - self.extensions = list(set([e.lower().strip(".") for e in self.config.get("extensions", [])])) + self.extensions = list({e.lower().strip(".") for e in self.config.get("extensions", [])}) return True async def filter_event(self, event): @@ -112,7 +114,7 @@ def extract_text(file_path): result = "" buffer = reader.read(4096) while len(buffer) > 0: - result += buffer.decode("utf-8") + result += buffer.decode("utf-8", errors="ignore") buffer = reader.read(4096) return result.strip() diff --git a/bbot/modules/filedownload.py b/bbot/modules/filedownload.py index 872a447a1f..b4fe3ef44f 100644 --- a/bbot/modules/filedownload.py +++ b/bbot/modules/filedownload.py @@ -38,6 +38,7 @@ class filedownload(BaseModule): "indd", # Adobe InDesign Document "ini", # Initialization File "jar", # Java Archive + "json", # JSON File "key", # Private Key File "log", # Log File "markdown", # Markdown File @@ -57,6 +58,7 @@ class filedownload(BaseModule): "pub", # Public Key File "raw", # Raw Image File Format "rdp", # Remote Desktop Protocol File + "rsa", # RSA Private Key File "sh", # Shell Script "sql", # SQL Database Dump "sqlite", # SQLite Database File @@ -87,7 +89,7 @@ class filedownload(BaseModule): scope_distance_modifier = 3 async def setup(self): - self.extensions = list(set([e.lower().strip(".") for e in self.config.get("extensions", [])])) + self.extensions = list({e.lower().strip(".") for e in self.config.get("extensions", [])}) self.max_filesize = self.config.get("max_filesize", "10MB") self.download_dir = self.scan.home / "filedownload" self.helpers.mkdir(self.download_dir) @@ -146,7 +148,8 @@ async def download_file(self, url, content_type=None, source_event=None): file_event = self.make_event( {"path": str(file_destination)}, "FILESYSTEM", tags=["filedownload", "file"], parent=source_event ) - await self.emit_event(file_event) + if file_event is not None: + await self.emit_event(file_event) self.urls_downloaded.add(hash(url)) def make_filename(self, url, content_type=None): @@ -177,7 +180,9 @@ def make_filename(self, url, content_type=None): if extension: filename = f"{filename}.{extension}" orig_filename = f"{orig_filename}.{extension}" - return orig_filename, self.download_dir / filename, base_url + file_destination = self.download_dir / filename + file_destination = self.helpers.truncate_filename(file_destination) + return orig_filename, file_destination, base_url async def report(self): if self.files_downloaded > 0: diff --git a/bbot/modules/fullhunt.py b/bbot/modules/fullhunt.py index 5736053e39..85106e5827 100644 --- a/bbot/modules/fullhunt.py +++ b/bbot/modules/fullhunt.py @@ -35,5 +35,5 @@ async def request_url(self, query): response = await self.api_request(url) return response - def parse_results(self, r, query): + async def parse_results(self, r, query): return r.json().get("hosts", []) diff --git a/bbot/modules/generic_ssrf.py b/bbot/modules/generic_ssrf.py index c6bd38544f..f486c7d978 100644 --- a/bbot/modules/generic_ssrf.py +++ b/bbot/modules/generic_ssrf.py @@ -163,7 +163,7 @@ async def setup(self): self.severity = None self.generic_only = self.config.get("generic_only", False) - if self.scan.config.get("interactsh_disable", False) == False: + if self.scan.config.get("interactsh_disable", False) is False: try: self.interactsh_instance = self.helpers.interactsh() self.interactsh_domain = await self.interactsh_instance.register(callback=self.interactsh_callback) @@ -216,7 +216,7 @@ async def interactsh_callback(self, r): self.debug("skipping result because subdomain tag was missing") async def cleanup(self): - if self.scan.config.get("interactsh_disable", False) == False: + if self.scan.config.get("interactsh_disable", False) is False: try: await self.interactsh_instance.deregister() self.debug( @@ -226,7 +226,7 @@ async def cleanup(self): self.warning(f"Interactsh failure: {e}") async def finish(self): - if self.scan.config.get("interactsh_disable", False) == False: + if self.scan.config.get("interactsh_disable", False) is False: await self.helpers.sleep(5) try: for r in await self.interactsh_instance.poll(): diff --git a/bbot/modules/github_codesearch.py b/bbot/modules/github_codesearch.py index 39e1ee7b48..4c838a1c5b 100644 --- a/bbot/modules/github_codesearch.py +++ b/bbot/modules/github_codesearch.py @@ -50,9 +50,10 @@ async def query(self, query): break status_code = getattr(r, "status_code", 0) if status_code == 429: - "Github is rate-limiting us (HTTP status: 429)" + self.info("Github is rate-limiting us (HTTP status: 429)") break if status_code != 200: + self.info(f"Unexpected response (HTTP status: {status_code})") break try: j = r.json() diff --git a/bbot/modules/github_org.py b/bbot/modules/github_org.py index 5b8571874c..5417a4e2d9 100644 --- a/bbot/modules/github_org.py +++ b/bbot/modules/github_org.py @@ -206,11 +206,7 @@ async def validate_org(self, org): for k, v in json.items(): if ( isinstance(v, str) - and ( - self.helpers.is_dns_name(v, include_local=False) - or self.helpers.is_url(v) - or self.helpers.is_email(v) - ) + and (self.helpers.is_dns_name(v) and "." in v or self.helpers.is_url(v) or self.helpers.is_email(v)) and self.scan.in_scope(v) ): self.verbose(f'Found in-scope key "{k}": "{v}" for {org}, it appears to be in-scope') diff --git a/bbot/modules/github_workflows.py b/bbot/modules/github_workflows.py index 369b337420..9ba34d155d 100644 --- a/bbot/modules/github_workflows.py +++ b/bbot/modules/github_workflows.py @@ -166,7 +166,7 @@ async def download_run_logs(self, owner, repo, run_id): main_logs = [] with zipfile.ZipFile(file_destination, "r") as logzip: for name in logzip.namelist(): - if fnmatch.fnmatch(name, "*.txt") and not "/" in name: + if fnmatch.fnmatch(name, "*.txt") and "/" not in name: logzip.extract(name, folder) main_logs.append(folder / name) return main_logs diff --git a/bbot/modules/gowitness.py b/bbot/modules/gowitness.py index 08edfaaf31..d1b152cfd1 100644 --- a/bbot/modules/gowitness.py +++ b/bbot/modules/gowitness.py @@ -88,7 +88,7 @@ async def setup(self): self.screenshot_path = self.base_path / "screenshots" self.command = self.construct_command() self.prepped = False - self.screenshots_taken = dict() + self.screenshots_taken = {} self.connections_logged = set() self.technologies_found = set() return True @@ -142,6 +142,9 @@ async def handle_batch(self, *events): url = screenshot["url"] final_url = screenshot["final_url"] filename = self.screenshot_path / screenshot["filename"] + filename = filename.relative_to(self.scan.home) + # NOTE: this prevents long filenames from causing problems in BBOT, but gowitness will still fail to save it. + filename = self.helpers.truncate_filename(filename) webscreenshot_data = {"path": str(filename), "url": final_url} parent_event = event_dict[url] await self.emit_event( @@ -172,7 +175,7 @@ async def handle_batch(self, *events): # emit technologies new_technologies = await self.get_new_technologies() - for _, row in new_technologies.items(): + for row in new_technologies.values(): parent_id = row["url_id"] parent_url = self.screenshots_taken[parent_id] parent_event = event_dict[parent_url] @@ -227,7 +230,7 @@ async def get_new_screenshots(self): return screenshots async def get_new_network_logs(self): - network_logs = dict() + network_logs = {} if self.db_path.is_file(): async with aiosqlite.connect(str(self.db_path)) as con: con.row_factory = aiosqlite.Row @@ -241,7 +244,7 @@ async def get_new_network_logs(self): return network_logs async def get_new_technologies(self): - technologies = dict() + technologies = {} if self.db_path.is_file(): async with aiosqlite.connect(str(self.db_path)) as con: con.row_factory = aiosqlite.Row @@ -264,8 +267,8 @@ async def cur_execute(self, cur, query): async def report(self): if self.screenshots_taken: self.success(f"{len(self.screenshots_taken):,} web screenshots captured. To view:") - self.success(f" - Start gowitness") + self.success(" - Start gowitness") self.success(f" - cd {self.base_path} && ./gowitness server") - self.success(f" - Browse to http://localhost:7171") + self.success(" - Browse to http://localhost:7171") else: - self.info(f"No web screenshots captured") + self.info("No web screenshots captured") diff --git a/bbot/modules/hackertarget.py b/bbot/modules/hackertarget.py index adfa54458d..b42352d473 100644 --- a/bbot/modules/hackertarget.py +++ b/bbot/modules/hackertarget.py @@ -18,12 +18,14 @@ async def request_url(self, query): response = await self.api_request(url) return response - def parse_results(self, r, query): + async def parse_results(self, r, query): + results = set() for line in r.text.splitlines(): host = line.split(",")[0] try: self.helpers.validators.validate_host(host) - yield host + results.add(host) except ValueError: self.debug(f"Error validating API result: {line}") continue + return results diff --git a/bbot/modules/host_header.py b/bbot/modules/host_header.py index 00dd640baf..a60967b8b4 100644 --- a/bbot/modules/host_header.py +++ b/bbot/modules/host_header.py @@ -19,7 +19,7 @@ class host_header(BaseModule): async def setup(self): self.subdomain_tags = {} - if self.scan.config.get("interactsh_disable", False) == False: + if self.scan.config.get("interactsh_disable", False) is False: try: self.interactsh_instance = self.helpers.interactsh() self.domain = await self.interactsh_instance.register(callback=self.interactsh_callback) @@ -60,7 +60,7 @@ async def interactsh_callback(self, r): self.debug("skipping results because subdomain tag was missing") async def finish(self): - if self.scan.config.get("interactsh_disable", False) == False: + if self.scan.config.get("interactsh_disable", False) is False: await self.helpers.sleep(5) try: for r in await self.interactsh_instance.poll(): @@ -69,7 +69,7 @@ async def finish(self): self.debug(f"Error in interact.sh: {e}") async def cleanup(self): - if self.scan.config.get("interactsh_disable", False) == False: + if self.scan.config.get("interactsh_disable", False) is False: try: await self.interactsh_instance.deregister() self.debug( @@ -84,7 +84,7 @@ async def handle_event(self, event): added_cookies = {} - for header, header_values in event.data["header-dict"].items(): + for header_values in event.data["header-dict"].values(): for header_value in header_values: if header_value.lower() == "set-cookie": header_split = header_value.split("=") @@ -136,7 +136,7 @@ async def handle_event(self, event): split_output = output.split("\n") if " 4" in split_output: - description = f"Duplicate Host Header Tolerated" + description = "Duplicate Host Header Tolerated" await self.emit_event( { "host": str(event.host), diff --git a/bbot/modules/httpx.py b/bbot/modules/httpx.py index 2cd2c0504c..8edc4e1d69 100644 --- a/bbot/modules/httpx.py +++ b/bbot/modules/httpx.py @@ -1,5 +1,5 @@ import re -import json +import orjson import tempfile import subprocess from pathlib import Path @@ -90,7 +90,7 @@ def make_url_metadata(self, event): else: url = str(event.data) url_hash = hash((event.host, event.port, has_spider_max)) - if url_hash == None: + if url_hash is None: url_hash = hash((url, has_spider_max)) return url, url_hash @@ -142,11 +142,11 @@ async def handle_batch(self, *events): proxy = self.scan.http_proxy if proxy: command += ["-http-proxy", proxy] - async for line in self.run_process_live(command, input=list(stdin), stderr=subprocess.DEVNULL): + async for line in self.run_process_live(command, text=False, input=list(stdin), stderr=subprocess.DEVNULL): try: - j = json.loads(line) - except json.decoder.JSONDecodeError: - self.debug(f"Failed to decode line: {line}") + j = await self.helpers.run_in_executor(orjson.loads, line) + except orjson.JSONDecodeError: + self.warning(f"httpx failed to decode line: {line}") continue url = j.get("url", "") diff --git a/bbot/modules/iis_shortnames.py b/bbot/modules/iis_shortnames.py index 6a173a929a..8c19f642d2 100644 --- a/bbot/modules/iis_shortnames.py +++ b/bbot/modules/iis_shortnames.py @@ -39,7 +39,7 @@ async def detect(self, target): test_url = f"{target}*~1*/a.aspx" for method in ["GET", "POST", "OPTIONS", "DEBUG", "HEAD", "TRACE"]: - kwargs = dict(method=method, allow_redirects=False, timeout=10) + kwargs = {"method": method, "allow_redirects": False, "timeout": 10} confirmations = 0 iterations = 5 # one failed detection is tolerated, as long as its not the first run while iterations > 0: @@ -128,7 +128,7 @@ async def solve_valid_chars(self, method, target, affirmative_status_code): suffix = "/a.aspx" urls_and_kwargs = [] - kwargs = dict(method=method, allow_redirects=False, retries=2, timeout=10) + kwargs = {"method": method, "allow_redirects": False, "retries": 2, "timeout": 10} for c in valid_chars: for file_part in ("stem", "ext"): payload = encode_all(f"*{c}*~1*") @@ -160,7 +160,7 @@ async def solve_shortname_recursive( url_hint_list = [] found_results = False - cl = ext_char_list if extension_mode == True else char_list + cl = ext_char_list if extension_mode is True else char_list urls_and_kwargs = [] @@ -169,7 +169,7 @@ async def solve_shortname_recursive( wildcard = "*" if extension_mode else "*~1*" payload = encode_all(f"{prefix}{c}{wildcard}") url = f"{target}{payload}{suffix}" - kwargs = dict(method=method) + kwargs = {"method": method} urls_and_kwargs.append((url, kwargs, c)) async for url, kwargs, c, response in self.helpers.request_custom_batch(urls_and_kwargs): @@ -209,7 +209,7 @@ async def solve_shortname_recursive( extension_mode, node_count=node_count, ) - if len(prefix) > 0 and found_results == False: + if len(prefix) > 0 and found_results is False: url_hint_list.append(f"{prefix}") self.verbose(f"Found new (possibly partial) URL_HINT: {prefix} from node {target}") return url_hint_list @@ -234,7 +234,7 @@ class safety_counter_obj: {"severity": "LOW", "host": str(event.host), "url": normalized_url, "description": description}, "VULNERABILITY", event, - context=f"{{module}} detected low {{event.type}}: IIS shortname enumeration", + context="{module} detected low {event.type}: IIS shortname enumeration", ) if not self.config.get("detect_only"): for detection in detections: diff --git a/bbot/modules/internal/cloudcheck.py b/bbot/modules/internal/cloudcheck.py index 392c8e0c5a..82f4e164f4 100644 --- a/bbot/modules/internal/cloudcheck.py +++ b/bbot/modules/internal/cloudcheck.py @@ -1,9 +1,15 @@ +from contextlib import suppress + from bbot.modules.base import BaseInterceptModule class CloudCheck(BaseInterceptModule): watched_events = ["*"] - meta = {"description": "Tag events by cloud provider, identify cloud resources like storage buckets"} + meta = { + "description": "Tag events by cloud provider, identify cloud resources like storage buckets", + "created_date": "2024-07-07", + "author": "@TheTechromancer", + } scope_distance_modifier = 1 _priority = 3 @@ -13,7 +19,7 @@ async def setup(self): def make_dummy_modules(self): self.dummy_modules = {} - for provider_name, provider in self.helpers.cloud.providers.items(): + for provider_name in self.helpers.cloud.providers.keys(): module = self.scan._make_dummy_module(f"cloud_{provider_name}", _type="scan") module.default_discovery_context = "{module} derived {event.type}: {event.host}" self.dummy_modules[provider_name] = module @@ -28,22 +34,42 @@ async def handle_event(self, event, **kwargs): if self.dummy_modules is None: self.make_dummy_modules() # cloud tagging by hosts - hosts_to_check = set(str(s) for s in event.resolved_hosts) - # we use the original host, since storage buckets hostnames might be collapsed to _wildcard - hosts_to_check.add(str(event.host_original)) - for host in hosts_to_check: - for provider, provider_type, subnet in self.helpers.cloudcheck(host): + hosts_to_check = set(event.resolved_hosts) + with suppress(KeyError): + hosts_to_check.remove(event.host_original) + hosts_to_check = [event.host_original] + list(hosts_to_check) + + for i, host in enumerate(hosts_to_check): + host_is_ip = self.helpers.is_ip(host) + try: + cloudcheck_results = self.helpers.cloudcheck(host) + except Exception as e: + self.trace(f"Error running cloudcheck against {event} (host: {host}): {e}") + continue + for provider, provider_type, subnet in cloudcheck_results: if provider: event.add_tag(f"{provider_type}-{provider}") + if host_is_ip: + event.add_tag(f"{provider_type}-ip") + else: + # if the original hostname is a cloud domain, tag it as such + if i == 0: + event.add_tag(f"{provider_type}-domain") + # any children are tagged as CNAMEs + else: + event.add_tag(f"{provider_type}-cname") found = set() + str_hosts_to_check = [str(host) for host in hosts_to_check] # look for cloud assets in hosts, http responses # loop through each provider for provider in self.helpers.cloud.providers.values(): provider_name = provider.name.lower() - base_kwargs = dict( - parent=event, tags=[f"{provider.provider_type}-{provider_name}"], _provider=provider_name - ) + base_kwargs = { + "parent": event, + "tags": [f"{provider.provider_type}-{provider_name}"], + "_provider": provider_name, + } # loop through the provider's regex signatures, if any for event_type, sigs in provider.signatures.items(): if event_type != "STORAGE_BUCKET": @@ -51,15 +77,16 @@ async def handle_event(self, event, **kwargs): base_kwargs["event_type"] = event_type for sig in sigs: matches = [] - if event.type == "HTTP_RESPONSE": - matches = await self.helpers.re.findall(sig, event.data.get("body", "")) - elif event.type.startswith("DNS_NAME"): - for host in hosts_to_check: + # TODO: convert this to an excavate YARA hook + # if event.type == "HTTP_RESPONSE": + # matches = await self.helpers.re.findall(sig, event.data.get("body", "")) + if event.type.startswith("DNS_NAME"): + for host in str_hosts_to_check: match = sig.match(host) if match: matches.append(match.groups()) for match in matches: - if not match in found: + if match not in found: found.add(match) _kwargs = dict(base_kwargs) diff --git a/bbot/modules/internal/dnsresolve.py b/bbot/modules/internal/dnsresolve.py index 38cea5dd1f..3dddd289a4 100644 --- a/bbot/modules/internal/dnsresolve.py +++ b/bbot/modules/internal/dnsresolve.py @@ -9,6 +9,8 @@ class DNSResolve(BaseInterceptModule): watched_events = ["*"] + produced_events = ["DNS_NAME", "IP_ADDRESS", "RAW_DNS_RECORD"] + meta = {"description": "Perform DNS resolution", "created_date": "2022-04-08", "author": "@TheTechromancer"} _priority = 1 scope_distance_modifier = None @@ -73,13 +75,28 @@ async def handle_event(self, event, **kwargs): if blacklisted: return False, "it has a blacklisted DNS record" + # DNS resolution for hosts that aren't IPs if not event_is_ip: # if the event is within our dns search distance, resolve the rest of our records if main_host_event.scope_distance < self._dns_search_distance: await self.resolve_event(main_host_event, types=non_minimal_rdtypes) # check for wildcards if the event is within the scan's search distance if new_event and main_host_event.scope_distance <= self.scan.scope_search_distance: - await self.handle_wildcard_event(main_host_event) + event_data_changed = await self.handle_wildcard_event(main_host_event) + if event_data_changed: + # since data has changed, we check again whether it's a duplicate + if event.type == "DNS_NAME" and self.scan.ingress_module.is_incoming_duplicate( + event, add=True + ): + if not event._graph_important: + return ( + False, + "it's a DNS wildcard, and its module already emitted a similar wildcard event", + ) + else: + self.debug( + f"Event {event} was already emitted by its module, but it's graph-important so it gets a pass" + ) # if there weren't any DNS children and it's not an IP address, tag as unresolved if not main_host_event.raw_dns_records and not event_is_ip: @@ -122,9 +139,9 @@ async def handle_wildcard_event(self, event): event.host, rdtypes=rdtypes, raw_dns_records=event.raw_dns_records ) for rdtype, (is_wildcard, wildcard_host) in wildcard_rdtypes.items(): - if is_wildcard == False: + if is_wildcard is False: continue - elif is_wildcard == True: + elif is_wildcard is True: event.add_tag("wildcard") wildcard_tag = "wildcard" else: @@ -133,16 +150,16 @@ async def handle_wildcard_event(self, event): event.add_tag(f"{rdtype}-{wildcard_tag}") # wildcard event modification (www.evilcorp.com --> _wildcard.evilcorp.com) - if wildcard_rdtypes and not "target" in event.tags: + if wildcard_rdtypes and "target" not in event.tags: # these are the rdtypes that have wildcards wildcard_rdtypes_set = set(wildcard_rdtypes) # consider the event a full wildcard if all its records are wildcards event_is_wildcard = False if wildcard_rdtypes_set: - event_is_wildcard = all(r[0] == True for r in wildcard_rdtypes.values()) + event_is_wildcard = all(r[0] is True for r in wildcard_rdtypes.values()) if event_is_wildcard: - if event.type in ("DNS_NAME",) and not "_wildcard" in event.data.split("."): + if event.type in ("DNS_NAME",) and "_wildcard" not in event.data.split("."): wildcard_parent = self.helpers.parent_domain(event.host) for rdtype, (_is_wildcard, _parent_domain) in wildcard_rdtypes.items(): if _is_wildcard: @@ -152,6 +169,8 @@ async def handle_wildcard_event(self, event): if wildcard_data != event.data: self.debug(f'Wildcard detected, changing event.data "{event.data}" --> "{wildcard_data}"') event.data = wildcard_data + return True + return False async def emit_dns_children(self, event): for rdtype, children in event.dns_children.items(): @@ -262,7 +281,7 @@ async def resolve_event(self, event, types): # tag event with errors for rdtype, errors in dns_errors.items(): # only consider it an error if there weren't any results for that rdtype - if errors and not rdtype in event.dns_children: + if errors and rdtype not in event.dns_children: event.add_tag(f"{rdtype}-error") def get_dns_parent(self, event): @@ -295,9 +314,7 @@ def get_dns_parent(self, event): @property def emit_raw_records(self): if self._emit_raw_records is None: - watching_raw_records = any( - ["RAW_DNS_RECORD" in m.get_watched_events() for m in self.scan.modules.values()] - ) + watching_raw_records = any("RAW_DNS_RECORD" in m.get_watched_events() for m in self.scan.modules.values()) omitted_event_types = self.scan.config.get("omit_event_types", []) omit_raw_records = "RAW_DNS_RECORD" in omitted_event_types self._emit_raw_records = watching_raw_records or not omit_raw_records diff --git a/bbot/modules/internal/excavate.py b/bbot/modules/internal/excavate.py index bc777e66c2..5fb0fee245 100644 --- a/bbot/modules/internal/excavate.py +++ b/bbot/modules/internal/excavate.py @@ -62,7 +62,6 @@ def _exclude_key(original_dict, key_to_exclude): def extract_params_url(parsed_url): - params = parse_qs(parsed_url.query) flat_params = {k: v[0] for k, v in params.items()} @@ -94,7 +93,6 @@ def extract_params_location(location_header_value, original_parsed_url): class YaraRuleSettings: - def __init__(self, description, tags, emit_match): self.description = description self.tags = tags @@ -155,7 +153,7 @@ async def preprocess(self, r, event, discovery_context): yara_results = {} for h in r.strings: yara_results[h.identifier.lstrip("$")] = sorted( - set([i.matched_data.decode("utf-8", errors="ignore") for i in h.instances]) + {i.matched_data.decode("utf-8", errors="ignore") for i in h.instances} ) await self.process(yara_results, event, yara_rule_settings, discovery_context) @@ -182,7 +180,7 @@ async def process(self, yara_results, event, yara_rule_settings, discovery_conte Returns: None """ - for identifier, results in yara_results.items(): + for results in yara_results.values(): for result in results: event_data = {"description": f"{discovery_context} {yara_rule_settings.description}"} if yara_rule_settings.emit_match: @@ -263,7 +261,6 @@ async def report( class CustomExtractor(ExcavateRule): - def __init__(self, excavate): super().__init__(excavate) @@ -317,7 +314,7 @@ class excavateTestRule(ExcavateRule): _module_threads = 8 - parameter_blacklist = set( + parameter_blacklist = { p.lower() for p in [ "__VIEWSTATE", @@ -332,7 +329,7 @@ class excavateTestRule(ExcavateRule): "JSESSIONID", "PHPSESSID", ] - ) + } yara_rule_name_regex = re.compile(r"rule\s(\w+)\s{") yara_rule_regex = re.compile(r"(?s)((?:rule\s+\w+\s*{[^{}]*(?:{[^{}]*}[^{}]*)*[^{}]*(?:/\S*?}[^/]*?/)*)*})") @@ -358,7 +355,6 @@ def url_unparse(self, param_type, parsed_url): ) class ParameterExtractor(ExcavateRule): - yara_rules = {} class ParameterExtractorRule: @@ -372,7 +368,6 @@ def __init__(self, excavate, result): self.result = result class GetJquery(ParameterExtractorRule): - name = "GET jquery" discovery_regex = r"/\$.get\([^\)].+\)/ nocase" extraction_regex = re.compile(r"\$.get\([\'\"](.+)[\'\"].+(\{.+\})\)") @@ -393,8 +388,12 @@ def extract(self): for action, extracted_parameters in extracted_results: extracted_parameters_dict = self.convert_to_dict(extracted_parameters) for parameter_name, original_value in extracted_parameters_dict.items(): - yield self.output_type, parameter_name, original_value, action, _exclude_key( - extracted_parameters_dict, parameter_name + yield ( + self.output_type, + parameter_name, + original_value, + action, + _exclude_key(extracted_parameters_dict, parameter_name), ) class PostJquery(GetJquery): @@ -418,8 +417,12 @@ def extract(self): k: v[0] if isinstance(v, list) and len(v) == 1 else v for k, v in query_strings.items() } for parameter_name, original_value in query_strings_dict.items(): - yield self.output_type, parameter_name, original_value, url, _exclude_key( - query_strings_dict, parameter_name + yield ( + self.output_type, + parameter_name, + original_value, + url, + _exclude_key(query_strings_dict, parameter_name), ) class GetForm(ParameterExtractorRule): @@ -444,8 +447,12 @@ def extract(self): form_parameters[parameter_name] = original_value for parameter_name, original_value in form_parameters.items(): - yield self.output_type, parameter_name, original_value, form_action, _exclude_key( - form_parameters, parameter_name + yield ( + self.output_type, + parameter_name, + original_value, + form_action, + _exclude_key(form_parameters, parameter_name), ) class PostForm(GetForm): @@ -464,7 +471,7 @@ def __init__(self, excavate): self.parameterExtractorCallbackDict[r.__name__] = r regexes_component_list.append(f"${r.__name__} = {r.discovery_regex}") regexes_component = " ".join(regexes_component_list) - self.yara_rules[f"parameter_extraction"] = ( + self.yara_rules["parameter_extraction"] = ( rf'rule parameter_extraction {{meta: description = "contains POST form" strings: {regexes_component} condition: any of them}}' ) @@ -485,7 +492,6 @@ async def process(self, yara_results, event, yara_rule_settings, discovery_conte endpoint, additional_params, ) in extracted_params: - self.excavate.debug( f"Found Parameter [{parameter_name}] in [{parameterExtractorSubModule.name}] ParameterExtractor Submodule" ) @@ -497,9 +503,13 @@ async def process(self, yara_results, event, yara_rule_settings, discovery_conte ) if self.excavate.helpers.validate_parameter(parameter_name, parameter_type): - - if self.excavate.in_bl(parameter_name) == False: + if self.excavate.in_bl(parameter_name) is False: parsed_url = urlparse(url) + if not parsed_url.hostname: + self.excavate.warning( + f"Error Parsing reconstructed URL [{url}] during parameter extraction, missing hostname" + ) + continue description = f"HTTP Extracted Parameter [{parameter_name}] ({parameterExtractorSubModule.name} Submodule)" data = { "host": parsed_url.hostname, @@ -527,13 +537,11 @@ class CSPExtractor(ExcavateRule): async def process(self, yara_results, event, yara_rule_settings, discovery_context): for identifier in yara_results.keys(): for csp_str in yara_results[identifier]: - domains = await self.helpers.re.findall(bbot_regexes.dns_name_regex, csp_str) - unique_domains = set(domains) - for domain in unique_domains: + domains = await self.excavate.scan.extract_in_scope_hostnames(csp_str) + for domain in domains: await self.report(domain, event, yara_rule_settings, discovery_context, event_type="DNS_NAME") class EmailExtractor(ExcavateRule): - yara_rules = { "email": 'rule email { meta: description = "contains email address" strings: $email = /[^\\W_][\\w\\-\\.\\+\']{0,100}@[a-zA-Z0-9\\-]{1,100}(\\.[a-zA-Z0-9\\-]{1,100})*\\.[a-zA-Z]{2,63}/ nocase fullword condition: $email }', } @@ -552,7 +560,6 @@ class JWTExtractor(ExcavateRule): } class ErrorExtractor(ExcavateRule): - signatures = { "PHP_1": r"/\.php on line [0-9]+/", "PHP_2": r"/\.php<\/b> on line [0-9]+/", @@ -577,7 +584,7 @@ def __init__(self, excavate): for signature_name, signature in self.signatures.items(): signature_component_list.append(rf"${signature_name} = {signature}") signature_component = " ".join(signature_component_list) - self.yara_rules[f"error_detection"] = ( + self.yara_rules["error_detection"] = ( f'rule error_detection {{meta: description = "contains a verbose error message" strings: {signature_component} condition: any of them}}' ) @@ -590,7 +597,6 @@ async def process(self, yara_results, event, yara_rule_settings, discovery_conte await self.report(event_data, event, yara_rule_settings, discovery_context, event_type="FINDING") class SerializationExtractor(ExcavateRule): - regexes = { "Java": re.compile(r"[^a-zA-Z0-9\/+]rO0[a-zA-Z0-9+\/]+={0,2}"), "DOTNET": re.compile(r"[^a-zA-Z0-9\/+]AAEAAAD\/\/[a-zA-Z0-9\/+]+={0,2}"), @@ -607,7 +613,7 @@ def __init__(self, excavate): for regex_name, regex in self.regexes.items(): regexes_component_list.append(rf"${regex_name} = /\b{regex.pattern}/ nocase") regexes_component = " ".join(regexes_component_list) - self.yara_rules[f"serialization_detection"] = ( + self.yara_rules["serialization_detection"] = ( f'rule serialization_detection {{meta: description = "contains a possible serialized object" strings: {regexes_component} condition: any of them}}' ) @@ -620,7 +626,6 @@ async def process(self, yara_results, event, yara_rule_settings, discovery_conte await self.report(event_data, event, yara_rule_settings, discovery_context, event_type="FINDING") class FunctionalityExtractor(ExcavateRule): - yara_rules = { "File_Upload_Functionality": r'rule File_Upload_Functionality { meta: description = "contains file upload functionality" strings: $fileuploadfunc = /]+type=["\']?file["\']?[^>]+>/ nocase condition: $fileuploadfunc }', "Web_Service_WSDL": r'rule Web_Service_WSDL { meta: emit_match = "True" description = "contains a web service WSDL URL" strings: $wsdl = /https?:\/\/[^\s]*\.(wsdl)/ nocase condition: $wsdl }', @@ -634,7 +639,7 @@ class NonHttpSchemeExtractor(ExcavateRule): scheme_blacklist = ["javascript", "mailto", "tel", "data", "vbscript", "about", "file"] async def process(self, yara_results, event, yara_rule_settings, discovery_context): - for identifier, results in yara_results.items(): + for results in yara_results.values(): for url_str in results: scheme = url_str.split("://")[0] if scheme in self.scheme_blacklist: @@ -656,7 +661,10 @@ async def process(self, yara_results, event, yara_rule_settings, discovery_conte continue if parsed_url.scheme in ["http", "https"]: continue - abort_if = lambda e: e.scope_distance > 0 + + def abort_if(e): + return e.scope_distance > 0 + finding_data = {"host": str(host), "description": f"Non-HTTP URI: {parsed_url.geturl()}"} await self.report(finding_data, event, yara_rule_settings, discovery_context, abort_if=abort_if) protocol_data = {"protocol": parsed_url.scheme, "host": str(host)} @@ -700,12 +708,11 @@ class URLExtractor(ExcavateRule): """ ), } - full_url_regex = re.compile(r"(https?)://((?:\w|\d)(?:[\d\w-]+\.?)+(?::\d{1,5})?(?:/[-\w\.\(\)]*[-\w\.]+)*/?)") + full_url_regex = re.compile(r"(https?)://(\w(?:[\w-]+\.?)+(?::\d{1,5})?(?:/[-\w\.\(\)]*[-\w\.]+)*/?)") full_url_regex_strict = re.compile(r"^(https?):\/\/([\w.-]+)(?::\d{1,5})?(\/[\w\/\.-]*)?(\?[^\s]+)?$") tag_attribute_regex = bbot_regexes.tag_attribute_regex async def process(self, yara_results, event, yara_rule_settings, discovery_context): - for identifier, results in yara_results.items(): urls_found = 0 final_url = "" @@ -770,7 +777,7 @@ class HostnameExtractor(ExcavateRule): def __init__(self, excavate): super().__init__(excavate) if excavate.scan.dns_yara_rules_uncompiled: - self.yara_rules[f"hostname_extraction"] = excavate.scan.dns_yara_rules_uncompiled + self.yara_rules["hostname_extraction"] = excavate.scan.dns_yara_rules_uncompiled async def process(self, yara_results, event, yara_rule_settings, discovery_context): for identifier in yara_results.keys(): @@ -818,7 +825,7 @@ async def setup(self): self.parameter_extraction = bool(modules_WEB_PARAMETER) self.retain_querystring = False - if self.config.get("retain_querystring", False) == True: + if self.config.get("retain_querystring", False) is True: self.retain_querystring = True for module in self.scan.modules.values(): @@ -848,7 +855,7 @@ async def setup(self): rules_content = f.read() self.debug(f"Successfully loaded custom yara rules file [{self.custom_yara_rules}]") else: - self.debug(f"Custom yara rules file is NOT a file. Will attempt to treat it as rule content") + self.debug("Custom yara rules file is NOT a file. Will attempt to treat it as rule content") rules_content = self.custom_yara_rules self.debug(f"Final combined yara rule contents: {rules_content}") @@ -861,7 +868,7 @@ async def setup(self): rule_match = await self.helpers.re.search(self.yara_rule_name_regex, rule_content) if not rule_match: - return False, f"Custom Yara formatted incorrectly: could not find rule name" + return False, "Custom Yara formatted incorrectly: could not find rule name" rule_name = rule_match.groups(1)[0] c = CustomExtractor(self) @@ -898,7 +905,6 @@ async def search(self, data, event, content_type, discovery_context="HTTP respon decoded_data = await self.helpers.re.recursive_decode(data) if self.parameter_extraction: - content_type_lower = content_type.lower() if content_type else "" extraction_map = { "json": self.helpers.extract_params_json, @@ -935,12 +941,11 @@ async def search(self, data, event, content_type, discovery_context="HTTP respon self.hugewarning(f"YARA Rule {rule_name} not found in pre-compiled rules") async def handle_event(self, event): - if event.type == "HTTP_RESPONSE": # Harvest GET parameters from URL, if it came directly from the target, and parameter extraction is enabled if ( - self.parameter_extraction == True - and self.url_querystring_remove == False + self.parameter_extraction is True + and self.url_querystring_remove is False and str(event.parent.parent.module) == "TARGET" ): self.debug(f"Processing target URL [{urlunparse(event.parsed_url)}] for GET parameters") @@ -952,7 +957,7 @@ async def handle_event(self, event): regex_name, additional_params, ) in extract_params_url(event.parsed_url): - if self.in_bl(parameter_name) == False: + if self.in_bl(parameter_name) is False: description = f"HTTP Extracted Parameter [{parameter_name}] (Target URL)" data = { "host": parsed_url.hostname, @@ -988,7 +993,7 @@ async def handle_event(self, event): cookie_name = header_value.split("=")[0] cookie_value = header_value.split("=")[1].split(";")[0] - if self.in_bl(cookie_value) == False: + if self.in_bl(cookie_value) is False: self.assigned_cookies[cookie_name] = cookie_value description = f"Set-Cookie Assigned Cookie [{cookie_name}]" data = { @@ -1024,7 +1029,6 @@ async def handle_event(self, event): # Try to extract parameters from the redirect URL if self.parameter_extraction: - for ( method, parsed_url, @@ -1033,7 +1037,7 @@ async def handle_event(self, event): regex_name, additional_params, ) in extract_params_location(header_value, event.parsed_url): - if self.in_bl(parameter_name) == False: + if self.in_bl(parameter_name) is False: description = f"HTTP Extracted Parameter [{parameter_name}] (Location Header)" data = { "host": parsed_url.hostname, diff --git a/bbot/modules/internal/speculate.py b/bbot/modules/internal/speculate.py index 0664f954a8..2555cd7d7e 100644 --- a/bbot/modules/internal/speculate.py +++ b/bbot/modules/internal/speculate.py @@ -32,10 +32,11 @@ class speculate(BaseInternalModule): "author": "@liquidsec", } - options = {"max_hosts": 65536, "ports": "80,443"} + options = {"max_hosts": 65536, "ports": "80,443", "essential_only": False} options_desc = { "max_hosts": "Max number of IP_RANGE hosts to convert into IP_ADDRESS events", "ports": "The set of ports to speculate on", + "essential_only": "Only enable essential speculate features (no extra discovery)", } scope_distance_modifier = 1 _priority = 4 @@ -44,14 +45,15 @@ class speculate(BaseInternalModule): async def setup(self): scan_modules = [m for m in self.scan.modules.values() if m._type == "scan"] - self.open_port_consumers = any(["OPEN_TCP_PORT" in m.watched_events for m in scan_modules]) + self.open_port_consumers = any("OPEN_TCP_PORT" in m.watched_events for m in scan_modules) # only consider active portscanners (still speculate if only passive ones are enabled) self.portscanner_enabled = any( - ["portscan" in m.flags and "active" in m.flags for m in self.scan.modules.values()] + "portscan" in m.flags and "active" in m.flags for m in self.scan.modules.values() ) self.emit_open_ports = self.open_port_consumers and not self.portscanner_enabled self.range_to_ip = True self.dns_disable = self.scan.config.get("dns", {}).get("disable", False) + self.essential_only = self.config.get("essential_only", False) self.org_stubs_seen = set() port_string = self.config.get("ports", "80,443") @@ -63,18 +65,26 @@ async def setup(self): if not self.portscanner_enabled: self.info(f"No portscanner enabled. Assuming open ports: {', '.join(str(x) for x in self.ports)}") - target_len = len(self.scan.target) + target_len = len(self.scan.target.seeds) if target_len > self.config.get("max_hosts", 65536): if not self.portscanner_enabled: self.hugewarning( f"Selected target ({target_len:,} hosts) is too large, skipping IP_RANGE --> IP_ADDRESS speculation" ) - self.hugewarning(f'Enabling the "portscan" module is highly recommended') + self.hugewarning('Enabling the "portscan" module is highly recommended') self.range_to_ip = False return True async def handle_event(self, event): + ### BEGIN ESSENTIAL SPECULATION ### + # These features are required for smooth operation of bbot + # I.e. they are not "osinty" or intended to discover anything, they only compliment other modules + + # we speculate on distance-1 stuff too, because distance-1 open ports are needed by certain modules like sslcert + event_in_scope_distance = event.scope_distance <= (self.scan.scope_search_distance + 1) + speculate_open_ports = self.emit_open_ports and event_in_scope_distance + # generate individual IP addresses from IP range if event.type == "IP_RANGE" and self.range_to_ip: net = ipaddress.ip_network(event.data) @@ -89,18 +99,36 @@ async def handle_event(self, event): context=f"speculate converted range into individual IP_ADDRESS: {ip}", ) + # IP_ADDRESS / DNS_NAME --> OPEN_TCP_PORT + if speculate_open_ports: + # don't act on unresolved DNS_NAMEs + usable_dns = False + if event.type == "DNS_NAME": + if self.dns_disable or event.resolved_hosts: + usable_dns = True + + if event.type == "IP_ADDRESS" or usable_dns: + for port in self.ports: + await self.emit_event( + self.helpers.make_netloc(event.data, port), + "OPEN_TCP_PORT", + parent=event, + internal=True, + context="speculated {event.type}: {event.data}", + ) + + ### END ESSENTIAL SPECULATION ### + if self.essential_only: + return + # parent domains if event.type.startswith("DNS_NAME"): parent = self.helpers.parent_domain(event.host_original) if parent != event.data: await self.emit_event( - parent, "DNS_NAME", parent=event, context=f"speculated parent {{event.type}}: {{event.data}}" + parent, "DNS_NAME", parent=event, context="speculated parent {event.type}: {event.data}" ) - # we speculate on distance-1 stuff too, because distance-1 open ports are needed by certain modules like sslcert - event_in_scope_distance = event.scope_distance <= (self.scan.scope_search_distance + 1) - speculate_open_ports = self.emit_open_ports and event_in_scope_distance - # URL --> OPEN_TCP_PORT event_is_url = event.type == "URL" if event_is_url or (event.type == "URL_UNVERIFIED" and self.open_port_consumers): @@ -144,24 +172,6 @@ async def handle_event(self, event): context="speculated {event.type}: {event.data}", ) - # IP_ADDRESS / DNS_NAME --> OPEN_TCP_PORT - if speculate_open_ports: - # don't act on unresolved DNS_NAMEs - usable_dns = False - if event.type == "DNS_NAME": - if self.dns_disable or ("a-record" in event.tags or "aaaa-record" in event.tags): - usable_dns = True - - if event.type == "IP_ADDRESS" or usable_dns: - for port in self.ports: - await self.emit_event( - self.helpers.make_netloc(event.data, port), - "OPEN_TCP_PORT", - parent=event, - internal=True, - context="speculated {event.type}: {event.data}", - ) - # ORG_STUB from TLD, SOCIAL, AZURE_TENANT org_stubs = set() if event.type == "DNS_NAME" and event.scope_distance == 0: diff --git a/bbot/modules/ipneighbor.py b/bbot/modules/ipneighbor.py index 6583832583..3bae28a37f 100644 --- a/bbot/modules/ipneighbor.py +++ b/bbot/modules/ipneighbor.py @@ -31,7 +31,7 @@ async def handle_event(self, event): netmask = main_ip.max_prefixlen - min(main_ip.max_prefixlen, self.num_bits) network = ipaddress.ip_network(f"{main_ip}/{netmask}", strict=False) subnet_hash = hash(network) - if not subnet_hash in self.processed: + if subnet_hash not in self.processed: self.processed.add(subnet_hash) for ip in network: if ip != main_ip: diff --git a/bbot/modules/jadx.py b/bbot/modules/jadx.py index d065310aed..d081171d1e 100644 --- a/bbot/modules/jadx.py +++ b/bbot/modules/jadx.py @@ -43,7 +43,7 @@ async def setup(self): async def filter_event(self, event): if "file" in event.tags: - if not event.data["magic_description"].lower() in self.allowed_file_types: + if event.data["magic_description"].lower() not in self.allowed_file_types: return False, f"Jadx is not able to decompile this file type: {event.data['magic_description']}" else: return False, "Event is not a file" diff --git a/bbot/modules/leakix.py b/bbot/modules/leakix.py index 22be7513d1..ac9e81f87b 100644 --- a/bbot/modules/leakix.py +++ b/bbot/modules/leakix.py @@ -15,20 +15,19 @@ class leakix(subdomain_enum_apikey): } base_url = "https://leakix.net" - ping_url = f"{base_url}/host/1.2.3.4.5" + ping_url = f"{base_url}/host/1.1.1.1" async def setup(self): ret = await super(subdomain_enum_apikey, self).setup() - self.headers = {"Accept": "application/json"} self.api_key = self.config.get("api_key", "") if self.api_key: - self.headers["api-key"] = self.api_key return await self.require_api_key() return ret def prepare_api_request(self, url, kwargs): if self.api_key: kwargs["headers"]["api-key"] = self.api_key + kwargs["headers"]["Accept"] = "application/json" return url, kwargs async def request_url(self, query): @@ -36,10 +35,12 @@ async def request_url(self, query): response = await self.api_request(url) return response - def parse_results(self, r, query=None): + async def parse_results(self, r, query=None): + results = set() json = r.json() if json: for entry in json: subdomain = entry.get("subdomain", "") if subdomain: - yield subdomain + results.add(subdomain) + return results diff --git a/bbot/modules/myssl.py b/bbot/modules/myssl.py index 5c4a8021bb..1a04364bcf 100644 --- a/bbot/modules/myssl.py +++ b/bbot/modules/myssl.py @@ -17,7 +17,7 @@ async def request_url(self, query): url = f"{self.base_url}?domain={self.helpers.quote(query)}" return await self.api_request(url) - def parse_results(self, r, query): + async def parse_results(self, r, query): results = set() json = r.json() if json and isinstance(json, dict): diff --git a/bbot/modules/newsletters.py b/bbot/modules/newsletters.py index 5f2bac729e..114f7d66fd 100644 --- a/bbot/modules/newsletters.py +++ b/bbot/modules/newsletters.py @@ -46,11 +46,11 @@ async def handle_event(self, event): body = _event.data["body"] soup = self.helpers.beautifulsoup(body, "html.parser") if soup is False: - self.debug(f"BeautifulSoup returned False") + self.debug("BeautifulSoup returned False") return result = self.find_type(soup) if result: - description = f"Found a Newsletter Submission Form that could be used for email bombing attacks" + description = "Found a Newsletter Submission Form that could be used for email bombing attacks" data = {"host": str(_event.host), "description": description, "url": _event.data["url"]} await self.emit_event( data, diff --git a/bbot/modules/otx.py b/bbot/modules/otx.py index 01b65eff50..f0075bfc1c 100644 --- a/bbot/modules/otx.py +++ b/bbot/modules/otx.py @@ -17,10 +17,12 @@ def request_url(self, query): url = f"{self.base_url}/api/v1/indicators/domain/{self.helpers.quote(query)}/passive_dns" return self.api_request(url) - def parse_results(self, r, query): + async def parse_results(self, r, query): + results = set() j = r.json() if isinstance(j, dict): for entry in j.get("passive_dns", []): subdomain = entry.get("hostname", "") if subdomain: - yield subdomain + results.add(subdomain) + return results diff --git a/bbot/modules/output/asset_inventory.py b/bbot/modules/output/asset_inventory.py index a150c029d3..ce94a56ea9 100644 --- a/bbot/modules/output/asset_inventory.py +++ b/bbot/modules/output/asset_inventory.py @@ -91,15 +91,15 @@ async def handle_event(self, event): self.assets[hostkey].absorb_event(event) async def report(self): - stats = dict() - totals = dict() + stats = {} + totals = {} def increment_stat(stat, value): try: totals[stat] += 1 except KeyError: totals[stat] = 1 - if not stat in stats: + if stat not in stats: stats[stat] = {} try: stats[stat][value] += 1 @@ -259,17 +259,17 @@ def absorb_csv_row(self, row): # ips self.ip_addresses = set(_make_ip_list(row.get("IP (External)", ""))) self.ip_addresses.update(set(_make_ip_list(row.get("IP (Internal)", "")))) - # If user reqests a recheck dont import the following fields to force them to be rechecked + # If user requests a recheck dont import the following fields to force them to be rechecked if not self.recheck: # ports ports = [i.strip() for i in row.get("Open Ports", "").split(",")] - self.ports.update(set(i for i in ports if i and is_port(i))) + self.ports.update({i for i in ports if i and is_port(i)}) # findings findings = [i.strip() for i in row.get("Findings", "").splitlines()] - self.findings.update(set(i for i in findings if i)) + self.findings.update({i for i in findings if i}) # technologies technologies = [i.strip() for i in row.get("Technologies", "").splitlines()] - self.technologies.update(set(i for i in technologies if i)) + self.technologies.update({i for i in technologies if i}) # risk rating risk_rating = row.get("Risk Rating", "").strip() if risk_rating and risk_rating.isdigit() and int(risk_rating) > self.risk_rating: diff --git a/bbot/modules/output/base.py b/bbot/modules/output/base.py index 0f6e7ac783..16fa4443bc 100644 --- a/bbot/modules/output/base.py +++ b/bbot/modules/output/base.py @@ -24,7 +24,7 @@ def _event_precheck(self, event): if event.type in ("FINISHED",): return True, "its type is FINISHED" if self.errored: - return False, f"module is in error state" + return False, "module is in error state" # exclude non-watched types if not any(t in self.get_watched_events() for t in ("*", event.type)): return False, "its type is not in watched_events" diff --git a/bbot/modules/output/csv.py b/bbot/modules/output/csv.py index 3141713fa5..9b7d4b4bd9 100644 --- a/bbot/modules/output/csv.py +++ b/bbot/modules/output/csv.py @@ -64,7 +64,7 @@ async def handle_event(self, event): ), "Source Module": str(getattr(event, "module_sequence", "")), "Scope Distance": str(getattr(event, "scope_distance", "")), - "Event Tags": ",".join(sorted(list(getattr(event, "tags", [])))), + "Event Tags": ",".join(sorted(getattr(event, "tags", []))), "Discovery Path": " --> ".join(discovery_path), } ) diff --git a/bbot/modules/output/mysql.py b/bbot/modules/output/mysql.py new file mode 100644 index 0000000000..8d9a1f7f4c --- /dev/null +++ b/bbot/modules/output/mysql.py @@ -0,0 +1,55 @@ +from bbot.modules.templates.sql import SQLTemplate + + +class MySQL(SQLTemplate): + watched_events = ["*"] + meta = { + "description": "Output scan data to a MySQL database", + "created_date": "2024-11-13", + "author": "@TheTechromancer", + } + options = { + "username": "root", + "password": "bbotislife", + "host": "localhost", + "port": 3306, + "database": "bbot", + } + options_desc = { + "username": "The username to connect to MySQL", + "password": "The password to connect to MySQL", + "host": "The server running MySQL", + "port": "The port to connect to MySQL", + "database": "The database name to connect to", + } + deps_pip = ["sqlmodel", "aiomysql"] + protocol = "mysql+aiomysql" + + async def create_database(self): + from sqlalchemy import text + from sqlalchemy.ext.asyncio import create_async_engine + + # Create the engine for the initial connection to the server + initial_engine = create_async_engine(self.connection_string().rsplit("/", 1)[0]) + + async with initial_engine.connect() as conn: + # Check if the database exists + result = await conn.execute(text(f"SHOW DATABASES LIKE '{self.database}'")) + database_exists = result.scalar() is not None + + # Create the database if it does not exist + if not database_exists: + # Use aiomysql directly to create the database + import aiomysql + + raw_conn = await aiomysql.connect( + user=self.username, + password=self.password, + host=self.host, + port=self.port, + ) + try: + async with raw_conn.cursor() as cursor: + await cursor.execute(f"CREATE DATABASE {self.database}") + finally: + await raw_conn.ensure_closed() diff --git a/bbot/modules/output/nmap_xml.py b/bbot/modules/output/nmap_xml.py new file mode 100644 index 0000000000..52698e0de8 --- /dev/null +++ b/bbot/modules/output/nmap_xml.py @@ -0,0 +1,171 @@ +import sys +from xml.dom import minidom +from datetime import datetime +from xml.etree.ElementTree import Element, SubElement, tostring + +from bbot import __version__ +from bbot.modules.output.base import BaseOutputModule + + +class NmapHost: + __slots__ = ["hostnames", "open_ports"] + + def __init__(self): + self.hostnames = set() + # a dict of {port: {protocol: banner}} + self.open_ports = dict() + + +class Nmap_XML(BaseOutputModule): + watched_events = ["OPEN_TCP_PORT", "DNS_NAME", "IP_ADDRESS", "PROTOCOL", "HTTP_RESPONSE"] + meta = {"description": "Output to Nmap XML", "created_date": "2024-11-16", "author": "@TheTechromancer"} + output_filename = "output.nmap.xml" + in_scope_only = True + + async def setup(self): + self.hosts = {} + self._prep_output_dir(self.output_filename) + return True + + async def handle_event(self, event): + event_host = event.host + + # we always record by IP + ips = [] + for ip in event.resolved_hosts: + try: + ips.append(self.helpers.make_ip_type(ip)) + except ValueError: + continue + if not ips and self.helpers.is_ip(event_host): + ips = [event_host] + + for ip in ips: + try: + nmap_host = self.hosts[ip] + except KeyError: + nmap_host = NmapHost() + self.hosts[ip] = nmap_host + + event_port = getattr(event, "port", None) + if event.type == "OPEN_TCP_PORT": + if event_port not in nmap_host.open_ports: + nmap_host.open_ports[event.port] = {} + elif event.type in ("PROTOCOL", "HTTP_RESPONSE"): + if event_port is not None: + try: + existing_services = nmap_host.open_ports[event.port] + except KeyError: + existing_services = {} + nmap_host.open_ports[event.port] = existing_services + if event.type == "PROTOCOL": + protocol = event.data["protocol"].lower() + banner = event.data.get("banner", None) + elif event.type == "HTTP_RESPONSE": + protocol = event.parsed_url.scheme.lower() + banner = event.http_title + if protocol not in existing_services: + existing_services[protocol] = banner + + if self.helpers.is_ip(event_host): + if str(event.module) == "PTR": + nmap_host.hostnames.add(event.parent.data) + else: + nmap_host.hostnames.add(event_host) + + async def report(self): + scan_start_time = str(int(self.scan.start_time.timestamp())) + scan_start_time_str = self.scan.start_time.strftime("%a %b %d %H:%M:%S %Y") + scan_end_time = datetime.now() + scan_end_time_str = scan_end_time.strftime("%a %b %d %H:%M:%S %Y") + scan_end_time_timestamp = str(scan_end_time.timestamp()) + scan_duration = scan_end_time - self.scan.start_time + num_hosts_up = len(self.hosts) + + # Create the root element + nmaprun = Element( + "nmaprun", + { + "scanner": "bbot", + "args": " ".join(sys.argv), + "start": scan_start_time, + "startstr": scan_start_time_str, + "version": str(__version__), + "xmloutputversion": "1.05", + }, + ) + + ports_scanned = [] + speculate_module = self.scan.modules.get("speculate", None) + if speculate_module is not None: + ports_scanned = speculate_module.ports + portscan_module = self.scan.modules.get("portscan", None) + if portscan_module is not None: + ports_scanned = self.helpers.parse_port_string(str(portscan_module.ports)) + num_ports_scanned = len(sorted(ports_scanned)) + ports_scanned = ",".join(str(x) for x in sorted(ports_scanned)) + + # Add scaninfo + SubElement( + nmaprun, + "scaninfo", + {"type": "syn", "protocol": "tcp", "numservices": str(num_ports_scanned), "services": ports_scanned}, + ) + + # Add host information + for ip, nmap_host in self.hosts.items(): + hostnames = sorted(nmap_host.hostnames) + ports = sorted(nmap_host.open_ports) + + host_elem = SubElement(nmaprun, "host") + SubElement(host_elem, "status", {"state": "up", "reason": "user-set", "reason_ttl": "0"}) + SubElement(host_elem, "address", {"addr": str(ip), "addrtype": f"ipv{ip.version}"}) + + if hostnames: + hostnames_elem = SubElement(host_elem, "hostnames") + for hostname in hostnames: + SubElement(hostnames_elem, "hostname", {"name": hostname, "type": "user"}) + + ports = SubElement(host_elem, "ports") + for port, protocols in nmap_host.open_ports.items(): + port_elem = SubElement(ports, "port", {"protocol": "tcp", "portid": str(port)}) + SubElement(port_elem, "state", {"state": "open", "reason": "syn-ack", "reason_ttl": "0"}) + # + for protocol, banner in protocols.items(): + attrs = {"name": protocol, "method": "probed", "conf": "10"} + if banner is not None: + attrs["product"] = banner + attrs["extrainfo"] = banner + SubElement(port_elem, "service", attrs) + + # Add runstats + runstats = SubElement(nmaprun, "runstats") + SubElement( + runstats, + "finished", + { + "time": scan_end_time_timestamp, + "timestr": scan_end_time_str, + "summary": f"BBOT done at {scan_end_time_str}; {num_hosts_up} scanned in {scan_duration} seconds", + "elapsed": str(scan_duration.total_seconds()), + "exit": "success", + }, + ) + SubElement(runstats, "hosts", {"up": str(num_hosts_up), "down": "0", "total": str(num_hosts_up)}) + + # make backup of the file + self.helpers.backup_file(self.output_file) + + # Pretty-format the XML + rough_string = tostring(nmaprun, encoding="utf-8") + reparsed = minidom.parseString(rough_string) + + # Create a new document with the doctype + doctype = minidom.DocumentType("nmaprun") + reparsed.insertBefore(doctype, reparsed.documentElement) + + pretty_xml = reparsed.toprettyxml(indent=" ") + + with open(self.output_file, "w") as f: + f.write(pretty_xml) + self.info(f"Saved Nmap XML output to {self.output_file}") diff --git a/bbot/modules/output/postgres.py b/bbot/modules/output/postgres.py new file mode 100644 index 0000000000..45beb7c7bc --- /dev/null +++ b/bbot/modules/output/postgres.py @@ -0,0 +1,53 @@ +from bbot.modules.templates.sql import SQLTemplate + + +class Postgres(SQLTemplate): + watched_events = ["*"] + meta = { + "description": "Output scan data to a SQLite database", + "created_date": "2024-11-08", + "author": "@TheTechromancer", + } + options = { + "username": "postgres", + "password": "bbotislife", + "host": "localhost", + "port": 5432, + "database": "bbot", + } + options_desc = { + "username": "The username to connect to Postgres", + "password": "The password to connect to Postgres", + "host": "The server running Postgres", + "port": "The port to connect to Postgres", + "database": "The database name to connect to", + } + deps_pip = ["sqlmodel", "asyncpg"] + protocol = "postgresql+asyncpg" + + async def create_database(self): + import asyncpg + from sqlalchemy import text + from sqlalchemy.ext.asyncio import create_async_engine + + # Create the engine for the initial connection to the server + initial_engine = create_async_engine(self.connection_string().rsplit("/", 1)[0]) + + async with initial_engine.connect() as conn: + # Check if the database exists + result = await conn.execute(text(f"SELECT 1 FROM pg_database WHERE datname = '{self.database}'")) + database_exists = result.scalar() is not None + + # Create the database if it does not exist + if not database_exists: + # Use asyncpg directly to create the database + raw_conn = await asyncpg.connect( + user=self.username, + password=self.password, + host=self.host, + port=self.port, + ) + try: + await raw_conn.execute(f"CREATE DATABASE {self.database}") + finally: + await raw_conn.close() diff --git a/bbot/modules/output/sqlite.py b/bbot/modules/output/sqlite.py index 68ac60dafd..261b13b6e2 100644 --- a/bbot/modules/output/sqlite.py +++ b/bbot/modules/output/sqlite.py @@ -5,14 +5,18 @@ class SQLite(SQLTemplate): watched_events = ["*"] - meta = {"description": "Output scan data to a SQLite database"} + meta = { + "description": "Output scan data to a SQLite database", + "created_date": "2024-11-07", + "author": "@TheTechromancer", + } options = { "database": "", } options_desc = { "database": "The path to the sqlite database file", } - deps_pip = ["sqlmodel", "sqlalchemy-utils", "aiosqlite"] + deps_pip = ["sqlmodel", "aiosqlite"] async def setup(self): db_file = self.config.get("database", "") diff --git a/bbot/modules/output/stdout.py b/bbot/modules/output/stdout.py index 6e4ccf5bea..59a121bd47 100644 --- a/bbot/modules/output/stdout.py +++ b/bbot/modules/output/stdout.py @@ -6,7 +6,7 @@ class Stdout(BaseOutputModule): watched_events = ["*"] - meta = {"description": "Output to text"} + meta = {"description": "Output to text", "created_date": "2024-04-03", "author": "@TheTechromancer"} options = {"format": "text", "event_types": [], "event_fields": [], "in_scope_only": False, "accept_dupes": True} options_desc = { "format": "Which text format to display, choices: text,json", @@ -20,7 +20,7 @@ class Stdout(BaseOutputModule): async def setup(self): self.text_format = self.config.get("format", "text").strip().lower() - if not self.text_format in self.format_choices: + if self.text_format not in self.format_choices: return ( False, f'Invalid text format choice, "{self.text_format}" (choices: {",".join(self.format_choices)})', @@ -33,7 +33,7 @@ async def setup(self): async def filter_event(self, event): if self.accept_event_types: - if not event.type in self.accept_event_types: + if event.type not in self.accept_event_types: return False, f'Event type "{event.type}" is not in the allowed event_types' return True diff --git a/bbot/modules/output/txt.py b/bbot/modules/output/txt.py index 68f86864da..2dfb14c106 100644 --- a/bbot/modules/output/txt.py +++ b/bbot/modules/output/txt.py @@ -5,7 +5,7 @@ class TXT(BaseOutputModule): watched_events = ["*"] - meta = {"description": "Output to text"} + meta = {"description": "Output to text", "created_date": "2024-04-03", "author": "@TheTechromancer"} options = {"output_file": ""} options_desc = {"output_file": "Output to file"} diff --git a/bbot/modules/paramminer_headers.py b/bbot/modules/paramminer_headers.py index 56090f6a28..723bffc2e0 100644 --- a/bbot/modules/paramminer_headers.py +++ b/bbot/modules/paramminer_headers.py @@ -82,7 +82,6 @@ class paramminer_headers(BaseModule): header_regex = re.compile(r"^[!#$%&\'*+\-.^_`|~0-9a-zA-Z]+: [^\r\n]+$") async def setup(self): - self.recycle_words = self.config.get("recycle_words", True) self.event_dict = {} self.already_checked = set() @@ -90,11 +89,11 @@ async def setup(self): if not wordlist: wordlist = f"{self.helpers.wordlist_dir}/{self.default_wordlist}" self.debug(f"Using wordlist: [{wordlist}]") - self.wl = set( + self.wl = { h.strip().lower() for h in self.helpers.read_file(await self.helpers.wordlist(wordlist)) if len(h) > 0 and "%" not in h - ) + } # check against the boring list (if the option is set) if self.config.get("skip_boring_words", True): @@ -157,7 +156,6 @@ async def process_results(self, event, results): ) async def handle_event(self, event): - # If recycle words is enabled, we will collect WEB_PARAMETERS we find to build our list in finish() # We also collect any parameters of type "SPECULATIVE" if event.type == "WEB_PARAMETER": @@ -174,7 +172,7 @@ async def handle_event(self, event): self.debug(f"Error initializing compare helper: {e}") return batch_size = await self.count_test(url) - if batch_size == None or batch_size <= 0: + if batch_size is None or batch_size <= 0: self.debug(f"Failed to get baseline max {self.compare_mode} count, aborting") return self.debug(f"Resolved batch_size at {str(batch_size)}") @@ -197,11 +195,11 @@ async def count_test(self, url): baseline = await self.helpers.request(url) if baseline is None: return - if str(baseline.status_code)[0] in ("4", "5"): + if str(baseline.status_code)[0] in {"4", "5"}: return for count, args, kwargs in self.gen_count_args(url): r = await self.helpers.request(*args, **kwargs) - if r is not None and not ((str(r.status_code)[0] in ("4", "5"))): + if r is not None and str(r.status_code)[0] not in {"4", "5"}: return count def gen_count_args(self, url): @@ -224,7 +222,7 @@ async def binary_search(self, compare_helper, url, group, reasons=None, reflecti elif len(group) > 1 or (len(group) == 1 and len(reasons) == 0): for group_slice in self.helpers.split_list(group): match, reasons, reflection, subject_response = await self.check_batch(compare_helper, url, group_slice) - if match == False: + if match is False: async for r in self.binary_search(compare_helper, url, group_slice, reasons, reflection): yield r else: @@ -240,8 +238,7 @@ async def check_batch(self, compare_helper, url, header_list): return await compare_helper.compare(url, headers=test_headers, check_reflection=(len(header_list) == 1)) async def finish(self): - - untested_matches = sorted(list(self.extracted_words_master.copy())) + untested_matches = sorted(self.extracted_words_master.copy()) for url, (event, batch_size) in list(self.event_dict.items()): try: compare_helper = self.helpers.http_compare(url) diff --git a/bbot/modules/passivetotal.py b/bbot/modules/passivetotal.py index eb895b0ea4..b20c7bbac0 100644 --- a/bbot/modules/passivetotal.py +++ b/bbot/modules/passivetotal.py @@ -11,36 +11,36 @@ class passivetotal(subdomain_enum_apikey): "author": "@TheTechromancer", "auth_required": True, } - options = {"username": "", "api_key": ""} - options_desc = {"username": "RiskIQ Username", "api_key": "RiskIQ API Key"} + options = {"api_key": ""} + options_desc = {"api_key": "PassiveTotal API Key in the format of 'username:api_key'"} base_url = "https://api.passivetotal.org/v2" async def setup(self): - self.username = self.config.get("username", "") - self.api_key = self.config.get("api_key", "") - self.auth = (self.username, self.api_key) return await super().setup() async def ping(self): url = f"{self.base_url}/account/quota" - j = (await self.api_request(url, auth=self.auth)).json() + j = (await self.api_request(url)).json() limit = j["user"]["limits"]["search_api"] used = j["user"]["counts"]["search_api"] assert used < limit, "No quota remaining" + def prepare_api_request(self, url, kwargs): + api_username, api_key = self.api_key.split(":", 1) + kwargs["auth"] = (api_username, api_key) + return url, kwargs + async def abort_if(self, event): # RiskIQ is famous for their junk data return await super().abort_if(event) or "unresolved" in event.tags async def request_url(self, query): url = f"{self.base_url}/enrichment/subdomains?query={self.helpers.quote(query)}" - return await self.api_request(url, auth=self.auth) + return await self.api_request(url) - def parse_results(self, r, query): + async def parse_results(self, r, query): + results = set() for subdomain in r.json().get("subdomains", []): - yield f"{subdomain}.{query}" - - @property - def auth_secret(self): - return self.username and self.api_key + results.add(f"{subdomain}.{query}") + return results diff --git a/bbot/modules/portscan.py b/bbot/modules/portscan.py index 5ff23dc7bb..a04bec2fdb 100644 --- a/bbot/modules/portscan.py +++ b/bbot/modules/portscan.py @@ -6,6 +6,9 @@ from bbot.modules.base import BaseModule +# TODO: this module is getting big. It should probably be two modules: one for ping and one for SYN. + + class portscan(BaseModule): flags = ["active", "portscan", "safe"] watched_events = ["IP_ADDRESS", "IP_RANGE", "DNS_NAME"] @@ -27,6 +30,8 @@ class portscan(BaseModule): "adapter_ip": "", "adapter_mac": "", "router_mac": "", + "cdn_tags": "cdn-", + "allowed_cdn_ports": None, } options_desc = { "top_ports": "Top ports to scan (default 100) (to override, specify 'ports')", @@ -39,6 +44,8 @@ class portscan(BaseModule): "adapter_ip": "Send packets using this IP address. Not needed unless masscan's autodetection fails", "adapter_mac": "Send packets using this as the source MAC address. Not needed unless masscan's autodetection fails", "router_mac": "Send packets to this MAC address as the destination. Not needed unless masscan's autodetection fails", + "cdn_tags": "Comma-separated list of tags to skip, e.g. 'cdn,cloud'", + "allowed_cdn_ports": "Comma-separated list of ports that are allowed to be scanned for CDNs", } deps_common = ["masscan"] batch_size = 1000000 @@ -60,7 +67,15 @@ async def setup(self): try: self.helpers.parse_port_string(self.ports) except ValueError as e: - return False, f"Error parsing ports: {e}" + return False, f"Error parsing ports '{self.ports}': {e}" + self.cdn_tags = [t.strip() for t in self.config.get("cdn_tags", "").split(",")] + self.allowed_cdn_ports = self.config.get("allowed_cdn_ports", None) + if self.allowed_cdn_ports is not None: + try: + self.allowed_cdn_ports = [int(p.strip()) for p in self.allowed_cdn_ports.split(",")] + except Exception as e: + return False, f"Error parsing allowed CDN ports '{self.allowed_cdn_ports}': {e}" + # whether we've finished scanning our original scan targets self.scanned_initial_targets = False # keeps track of individual scanned IPs and their open ports @@ -84,17 +99,17 @@ async def setup(self): return False, "Masscan failed to run" returncode = getattr(ipv6_result, "returncode", 0) if returncode and "failed to detect IPv6 address" in ipv6_result.stderr: - self.warning(f"It looks like you are not set up for IPv6. IPv6 targets will not be scanned.") + self.warning("It looks like you are not set up for IPv6. IPv6 targets will not be scanned.") self.ipv6_support = False return True async def handle_batch(self, *events): - # on our first run, we automatically include all our intial scan targets + # on our first run, we automatically include all our initial scan targets if not self.scanned_initial_targets: self.scanned_initial_targets = True events = set(events) events.update( - set([e for e in self.scan.target.seeds.events if e.type in ("DNS_NAME", "IP_ADDRESS", "IP_RANGE")]) + {e for e in self.scan.target.seeds.events if e.type in ("DNS_NAME", "IP_ADDRESS", "IP_RANGE")} ) # ping scan @@ -227,9 +242,20 @@ async def emit_open_port(self, ip, port, parent_event): parent=parent_event, context=f"{{module}} executed a {scan_type} scan against {parent_event.data} and found: {{event.type}}: {{event.data}}", ) - await self.emit_event(event) + + await self.emit_event(event, abort_if=self.abort_if) return event + def abort_if(self, event): + if self.allowed_cdn_ports is not None: + # if the host is a CDN + for cdn_tag in self.cdn_tags: + if any(t.startswith(str(cdn_tag)) for t in event.tags): + # and if its port isn't in the list of allowed CDN ports + if event.port not in self.allowed_cdn_ports: + return True, "event is a CDN and port is not in the allowed list" + return False + def parse_json_line(self, line): try: j = json.loads(line) @@ -308,7 +334,7 @@ def log_masscan_status(self, s): if "FAIL" in s: self.warning(s) self.warning( - f'Masscan failed to detect interface. Recommend passing "adapter_ip", "adapter_mac", and "router_mac" config options to portscan module.' + 'Masscan failed to detect interface. Recommend passing "adapter_ip", "adapter_mac", and "router_mac" config options to portscan module.' ) else: self.verbose(s) diff --git a/bbot/modules/rapiddns.py b/bbot/modules/rapiddns.py index ad680131ae..150728eca3 100644 --- a/bbot/modules/rapiddns.py +++ b/bbot/modules/rapiddns.py @@ -18,11 +18,6 @@ async def request_url(self, query): response = await self.api_request(url, timeout=self.http_timeout + 10) return response - def parse_results(self, r, query): - results = set() + async def parse_results(self, r, query): text = getattr(r, "text", "") - for match in self.helpers.regexes.dns_name_regex.findall(text): - match = match.lower() - if match.endswith(query): - results.add(match) - return results + return await self.scan.extract_in_scope_hostnames(text) diff --git a/bbot/modules/report/asn.py b/bbot/modules/report/asn.py index ba5e1e39a4..3b3c488d15 100644 --- a/bbot/modules/report/asn.py +++ b/bbot/modules/report/asn.py @@ -38,7 +38,7 @@ async def filter_event(self, event): async def handle_event(self, event): host = event.host - if self.cache_get(host) == False: + if self.cache_get(host) is False: asns, source = await self.get_asn(host) if not asns: self.cache_put(self.unknown_asn) @@ -96,7 +96,7 @@ def cache_get(self, ip): for p in self.helpers.ip_network_parents(ip): try: self.asn_counts[p] += 1 - if ret == False: + if ret is False: ret = p except KeyError: continue @@ -112,7 +112,7 @@ async def get_asn(self, ip, retries=1): for i, source in enumerate(list(self.sources)): get_asn_fn = getattr(self, f"get_asn_{source}") res = await get_asn_fn(ip) - if res == False: + if res is False: # demote the current source to lowest priority since it just failed self.sources.append(self.sources.pop(i)) self.verbose(f"Failed to contact {source}, retrying") @@ -125,7 +125,7 @@ async def get_asn_ripe(self, ip): url = f"https://stat.ripe.net/data/network-info/data.json?resource={ip}" response = await self.get_url(url, "ASN") asns = [] - if response == False: + if response is False: return False data = response.get("data", {}) if not data: @@ -138,7 +138,7 @@ async def get_asn_ripe(self, ip): asn_numbers = [] for number in asn_numbers: asn = await self.get_asn_metadata_ripe(number) - if asn == False: + if asn is False: return False asn["subnet"] = prefix asns.append(asn) @@ -155,7 +155,7 @@ async def get_asn_metadata_ripe(self, asn_number): } url = f"https://stat.ripe.net/data/whois/data.json?resource={asn_number}" response = await self.get_url(url, "ASN Metadata", cache=True) - if response == False: + if response is False: return False data = response.get("data", {}) if not data: @@ -187,7 +187,7 @@ async def get_asn_bgpview(self, ip): data = await self.get_url(url, "ASN") asns = [] asns_tried = set() - if data == False: + if data is False: return False data = data.get("data", {}) prefixes = data.get("prefixes", []) @@ -201,13 +201,20 @@ async def get_asn_bgpview(self, ip): description = details.get("description") or prefix.get("description") or "" country = details.get("country_code") or prefix.get("country_code") or "" emails = [] - if not asn in asns_tried: + if asn not in asns_tried: emails = await self.get_emails_bgpview(asn) - if emails == False: + if emails is False: return False asns_tried.add(asn) asns.append( - dict(asn=asn, subnet=subnet, name=name, description=description, country=country, emails=emails) + { + "asn": asn, + "subnet": subnet, + "name": name, + "description": description, + "country": country, + "emails": emails, + } ) if not asns: self.debug(f'No results for "{ip}"') @@ -217,7 +224,7 @@ async def get_emails_bgpview(self, asn): contacts = [] url = f"https://api.bgpview.io/asn/{asn}" data = await self.get_url(url, "ASN metadata", cache=True) - if data == False: + if data is False: return False data = data.get("data", {}) if not data: diff --git a/bbot/modules/robots.py b/bbot/modules/robots.py index fc76920517..e41b3119fb 100644 --- a/bbot/modules/robots.py +++ b/bbot/modules/robots.py @@ -33,14 +33,14 @@ async def handle_event(self, event): for l in lines: if len(l) > 0: split_l = l.split(": ") - if (split_l[0].lower() == "allow" and self.config.get("include_allow") == True) or ( - split_l[0].lower() == "disallow" and self.config.get("include_disallow") == True + if (split_l[0].lower() == "allow" and self.config.get("include_allow") is True) or ( + split_l[0].lower() == "disallow" and self.config.get("include_disallow") is True ): unverified_url = f"{host}{split_l[1].lstrip('/')}".replace( "*", self.helpers.rand_string(4) ) - elif split_l[0].lower() == "sitemap" and self.config.get("include_sitemap") == True: + elif split_l[0].lower() == "sitemap" and self.config.get("include_sitemap") is True: unverified_url = split_l[1] else: continue diff --git a/bbot/modules/secretsdb.py b/bbot/modules/secretsdb.py deleted file mode 100644 index 2d70e538d2..0000000000 --- a/bbot/modules/secretsdb.py +++ /dev/null @@ -1,78 +0,0 @@ -import re -import yaml - -from .base import BaseModule - - -class secretsdb(BaseModule): - watched_events = ["HTTP_RESPONSE"] - produced_events = ["FINDING"] - flags = ["active", "safe", "web-basic"] - meta = { - "description": "Detect common secrets with secrets-patterns-db", - "created_date": "2023-03-17", - "author": "@TheTechromancer", - } - options = { - "min_confidence": 99, - "signatures": "https://raw.githubusercontent.com/blacklanternsecurity/secrets-patterns-db/master/db/rules-stable.yml", - } - options_desc = { - "min_confidence": "Only use signatures with this confidence score or higher", - "signatures": "File path or URL to YAML signatures", - } - deps_pip = ["pyyaml~=6.0"] - # accept any HTTP_RESPONSE including out-of-scope ones (such as from github_codesearch) - scope_distance_modifier = 3 - - async def setup(self): - self.rules = [] - self.min_confidence = self.config.get("min_confidence", 99) - self.sig_file = await self.helpers.wordlist(self.config.get("signatures", "")) - with open(self.sig_file) as f: - rules_yaml = yaml.safe_load(f).get("patterns", []) - for r in rules_yaml: - r = r.get("pattern", {}) - if not r: - continue - name = r.get("name", "").lower() - confidence = r.get("confidence", "") - if name and confidence >= self.min_confidence: - regex = r.get("regex", "") - try: - compiled_regex = re.compile(regex) - r["regex"] = compiled_regex - self.rules.append(r) - except Exception: - self.debug(f"Error compiling regex: r'{regex}'") - return True - - async def handle_event(self, event): - resp_body = event.data.get("body", "") - resp_headers = event.data.get("raw_header", "") - all_matches = await self.helpers.run_in_executor(self.search_data, resp_body, resp_headers) - for matches, name in all_matches: - matches = [m.string[m.start() : m.end()] for m in matches] - description = f"Possible secret ({name}): {matches}" - event_data = {"host": str(event.host), "description": description} - parsed_url = getattr(event, "parsed_url", None) - if parsed_url: - event_data["url"] = parsed_url.geturl() - await self.emit_event( - event_data, - "FINDING", - parent=event, - context=f"{{module}} searched HTTP response and found {{event.type}}: {description}", - ) - - def search_data(self, resp_body, resp_headers): - all_matches = [] - for r in self.rules: - regex = r["regex"] - name = r["name"] - for text in (resp_body, resp_headers): - if text: - matches = list(regex.finditer(text)) - if matches: - all_matches.append((matches, name)) - return all_matches diff --git a/bbot/modules/securitytrails.py b/bbot/modules/securitytrails.py index c74450307d..b92ac07dc1 100644 --- a/bbot/modules/securitytrails.py +++ b/bbot/modules/securitytrails.py @@ -26,8 +26,10 @@ async def request_url(self, query): response = await self.api_request(url) return response - def parse_results(self, r, query): + async def parse_results(self, r, query): + results = set() j = r.json() if isinstance(j, dict): for host in j.get("subdomains", []): - yield f"{host}.{query}" + results.add(f"{host}.{query}") + return results diff --git a/bbot/modules/securitytxt.py b/bbot/modules/securitytxt.py index 681519e37b..880865c9b8 100644 --- a/bbot/modules/securitytxt.py +++ b/bbot/modules/securitytxt.py @@ -121,7 +121,7 @@ async def handle_event(self, event): start, end = match.span() found_url = v[start:end] - if found_url != url and self._urls == True: + if found_url != url and self._urls is True: await self.emit_event(found_url, "URL_UNVERIFIED", parent=event, tags=tags) diff --git a/bbot/modules/shodan_dns.py b/bbot/modules/shodan_dns.py index 21140831eb..2ad0bc5057 100644 --- a/bbot/modules/shodan_dns.py +++ b/bbot/modules/shodan_dns.py @@ -22,5 +22,5 @@ async def handle_event(self, event): def make_url(self, query): return f"{self.base_url}/dns/domain/{self.helpers.quote(query)}?key={{api_key}}&page={{page}}" - def parse_results(self, json, query): + async def parse_results(self, json, query): return [f"{sub}.{query}" for sub in json.get("subdomains", [])] diff --git a/bbot/modules/sitedossier.py b/bbot/modules/sitedossier.py index fe90150271..659f159608 100644 --- a/bbot/modules/sitedossier.py +++ b/bbot/modules/sitedossier.py @@ -52,5 +52,5 @@ async def query(self, query, parse_fn=None, request_fn=None): results.add(hostname) yield hostname if '= (self.max_pages - 1): @@ -70,6 +70,8 @@ async def query(self, query): agen.aclose() return results - def parse_results(self, r): + async def parse_results(self, r): + results = set() for entry in r.get("list", []): - yield entry["name"] + results.add(entry["name"]) + return results diff --git a/bbot/presets/fast.yml b/bbot/presets/fast.yml new file mode 100644 index 0000000000..675082b2a7 --- /dev/null +++ b/bbot/presets/fast.yml @@ -0,0 +1,16 @@ +description: Scan only the provided targets as fast as possible - no extra discovery + +exclude_modules: + - excavate + +config: + # only scan the exact targets specified + scope: + strict: true + # speed up dns resolution by doing A/AAAA only - not MX/NS/SRV/etc + dns: + minimal: true + # essential speculation only + modules: + speculate: + essential_only: true diff --git a/bbot/presets/kitchen-sink.yml b/bbot/presets/kitchen-sink.yml index 43057bf44a..073f480bb2 100644 --- a/bbot/presets/kitchen-sink.yml +++ b/bbot/presets/kitchen-sink.yml @@ -16,5 +16,3 @@ config: modules: baddns: enable_references: True - - diff --git a/bbot/presets/spider.yml b/bbot/presets/spider.yml index 0ffb495c48..9e98ff4539 100644 --- a/bbot/presets/spider.yml +++ b/bbot/presets/spider.yml @@ -3,6 +3,10 @@ description: Recursive web spider modules: - httpx +blacklist: + # Prevent spider from invalidating sessions by logging out + - "RE:/.*(sign|log)[_-]?out" + config: web: # how many links to follow in a row diff --git a/bbot/presets/web/dotnet-audit.yml b/bbot/presets/web/dotnet-audit.yml index bbc5e201e0..b1cd8e9cac 100644 --- a/bbot/presets/web/dotnet-audit.yml +++ b/bbot/presets/web/dotnet-audit.yml @@ -19,4 +19,3 @@ config: extensions: asp,aspx,ashx,asmx,ascx telerik: exploit_RAU_crypto: True - diff --git a/bbot/scanner/manager.py b/bbot/scanner/manager.py index 8cbe098a5f..4b129d5243 100644 --- a/bbot/scanner/manager.py +++ b/bbot/scanner/manager.py @@ -38,7 +38,7 @@ async def init_events(self, events=None): - It also marks the Scan object as finished with initialization by setting `_finished_init` to True. """ if events is None: - events = self.scan.target.events + events = self.scan.target.seeds.events async with self.scan._acatch(self.init_events), self._task_counter.count(self.init_events): sorted_events = sorted(events, key=lambda e: len(e.data)) for event in [self.scan.root_event] + sorted_events: @@ -49,7 +49,6 @@ async def init_events(self, events=None): event.parent = self.scan.root_event if event.module is None: event.module = self.scan._make_dummy_module(name="TARGET", _type="TARGET") - event.add_tag("target") if event != self.scan.root_event: event.discovery_context = f"Scan {self.scan.name} seeded with " + "{event.type}: {event.data}" self.verbose(f"Target: {event}") diff --git a/bbot/scanner/preset/args.py b/bbot/scanner/preset/args.py index 986fd909f5..13723ea01d 100644 --- a/bbot/scanner/preset/args.py +++ b/bbot/scanner/preset/args.py @@ -10,7 +10,6 @@ class BBOTArgs: - # module config options to exclude from validation exclude_from_validation = re.compile(r".*modules\.[a-z0-9_]+\.(?:batch_size|module_threads)$") @@ -53,6 +52,11 @@ class BBOTArgs: "", "bbot -l", ), + ( + "List output modules", + "", + "bbot -lo", + ), ( "List presets", "", @@ -91,7 +95,6 @@ def preset_from_args(self): *self.parsed.targets, whitelist=self.parsed.whitelist, blacklist=self.parsed.blacklist, - strict_scope=self.parsed.strict_scope, name="args_preset", ) @@ -132,14 +135,17 @@ def preset_from_args(self): args_preset.core.merge_custom({"modules": {"stdout": {"event_types": self.parsed.event_types}}}) # dependencies + deps_config = args_preset.core.custom_config.get("deps", {}) if self.parsed.retry_deps: - args_preset.core.custom_config["deps_behavior"] = "retry_failed" + deps_config["behavior"] = "retry_failed" elif self.parsed.force_deps: - args_preset.core.custom_config["deps_behavior"] = "force_install" + deps_config["behavior"] = "force_install" elif self.parsed.no_deps: - args_preset.core.custom_config["deps_behavior"] = "disable" + deps_config["behavior"] = "disable" elif self.parsed.ignore_failed_deps: - args_preset.core.custom_config["deps_behavior"] = "ignore_failed" + deps_config["behavior"] = "ignore_failed" + if deps_config: + args_preset.core.merge_custom({"deps": deps_config}) # other scan options if self.parsed.name is not None: @@ -149,6 +155,9 @@ def preset_from_args(self): if self.parsed.force: args_preset.force_start = self.parsed.force + if self.parsed.proxy: + args_preset.core.merge_custom({"web": {"http_proxy": self.parsed.proxy}}) + if self.parsed.custom_headers: args_preset.core.merge_custom({"web": {"http_headers": self.parsed.custom_headers}}) @@ -165,13 +174,19 @@ def preset_from_args(self): except Exception as e: raise BBOTArgumentError(f'Error parsing command-line config option: "{config_arg}": {e}') + # strict scope + if self.parsed.strict_scope: + args_preset.core.merge_custom({"scope": {"strict": True}}) + return args_preset def create_parser(self, *args, **kwargs): kwargs.update( - dict( - description="Bighuge BLS OSINT Tool", formatter_class=argparse.RawTextHelpFormatter, epilog=self.epilog - ) + { + "description": "Bighuge BLS OSINT Tool", + "formatter_class": argparse.RawTextHelpFormatter, + "epilog": self.epilog, + } ) p = argparse.ArgumentParser(*args, **kwargs) @@ -209,7 +224,7 @@ def create_parser(self, *args, **kwargs): metavar="CONFIG", default=[], ) - presets.add_argument("-lp", "--list-presets", action="store_true", help=f"List available presets.") + presets.add_argument("-lp", "--list-presets", action="store_true", help="List available presets.") modules = p.add_argument_group(title="Modules") modules.add_argument( @@ -217,31 +232,31 @@ def create_parser(self, *args, **kwargs): "--modules", nargs="+", default=[], - help=f'Modules to enable. Choices: {",".join(self.preset.module_loader.scan_module_choices)}', + help=f'Modules to enable. Choices: {",".join(sorted(self.preset.module_loader.scan_module_choices))}', metavar="MODULE", ) - modules.add_argument("-l", "--list-modules", action="store_true", help=f"List available modules.") + modules.add_argument("-l", "--list-modules", action="store_true", help="List available modules.") modules.add_argument( "-lmo", "--list-module-options", action="store_true", help="Show all module config options" ) modules.add_argument( - "-em", "--exclude-modules", nargs="+", default=[], help=f"Exclude these modules.", metavar="MODULE" + "-em", "--exclude-modules", nargs="+", default=[], help="Exclude these modules.", metavar="MODULE" ) modules.add_argument( "-f", "--flags", nargs="+", default=[], - help=f'Enable modules by flag. Choices: {",".join(self.preset.module_loader.flag_choices)}', + help=f'Enable modules by flag. Choices: {",".join(sorted(self.preset.module_loader.flag_choices))}', metavar="FLAG", ) - modules.add_argument("-lf", "--list-flags", action="store_true", help=f"List available flags.") + modules.add_argument("-lf", "--list-flags", action="store_true", help="List available flags.") modules.add_argument( "-rf", "--require-flags", nargs="+", default=[], - help=f"Only enable modules with these flags (e.g. -rf passive)", + help="Only enable modules with these flags (e.g. -rf passive)", metavar="FLAG", ) modules.add_argument( @@ -249,7 +264,7 @@ def create_parser(self, *args, **kwargs): "--exclude-flags", nargs="+", default=[], - help=f"Disable modules with these flags. (e.g. -ef aggressive)", + help="Disable modules with these flags. (e.g. -ef aggressive)", metavar="FLAG", ) modules.add_argument("--allow-deadly", action="store_true", help="Enable the use of highly aggressive modules") @@ -265,7 +280,12 @@ def create_parser(self, *args, **kwargs): help="Run scan even in the case of condition violations or failed module setups", ) scan.add_argument("-y", "--yes", action="store_true", help="Skip scan confirmation prompt") - scan.add_argument("--dry-run", action="store_true", help=f"Abort before executing scan") + scan.add_argument( + "--fast-mode", + action="store_true", + help="Scan only the provided targets as fast as possible, with no extra discovery", + ) + scan.add_argument("--dry-run", action="store_true", help="Abort before executing scan") scan.add_argument( "--current-preset", action="store_true", @@ -289,9 +309,10 @@ def create_parser(self, *args, **kwargs): "--output-modules", nargs="+", default=[], - help=f'Output module(s). Choices: {",".join(self.preset.module_loader.output_module_choices)}', + help=f'Output module(s). Choices: {",".join(sorted(self.preset.module_loader.output_module_choices))}', metavar="MODULE", ) + output.add_argument("-lo", "--list-output-modules", action="store_true", help="List available output modules") output.add_argument("--json", "-j", action="store_true", help="Output scan data in JSON format") output.add_argument("--brief", "-br", action="store_true", help="Output only the data itself") output.add_argument("--event-types", nargs="+", default=[], help="Choose which event types to display") @@ -310,6 +331,7 @@ def create_parser(self, *args, **kwargs): misc = p.add_argument_group(title="Misc") misc.add_argument("--version", action="store_true", help="show BBOT version and exit") + misc.add_argument("--proxy", help="Use this proxy for all HTTP requests", metavar="HTTP_PROXY") misc.add_argument( "-H", "--custom-headers", @@ -359,6 +381,10 @@ def sanitize_args(self): custom_headers_dict[k] = v self.parsed.custom_headers = custom_headers_dict + # --fast-mode + if self.parsed.fast_mode: + self.parsed.preset += ["fast"] + def validate(self): # validate config options sentinel = object() diff --git a/bbot/scanner/preset/environ.py b/bbot/scanner/preset/environ.py index 66253de320..6dc5d8adae 100644 --- a/bbot/scanner/preset/environ.py +++ b/bbot/scanner/preset/environ.py @@ -65,7 +65,6 @@ def add_to_path(v, k="PATH", environ=None): class BBOTEnviron: - def __init__(self, preset): self.preset = preset diff --git a/bbot/scanner/preset/path.py b/bbot/scanner/preset/path.py index 730b16e637..9b84566124 100644 --- a/bbot/scanner/preset/path.py +++ b/bbot/scanner/preset/path.py @@ -33,7 +33,9 @@ def find(self, filename): if "/" in str(filename): if filename_path.parent not in paths_to_search: paths_to_search.append(filename_path.parent) - log.debug(f"Searching for preset in {paths_to_search}, file candidates: {file_candidates_str}") + log.debug( + f"Searching for preset in {[str(p) for p in paths_to_search]}, file candidates: {file_candidates_str}" + ) for path in paths_to_search: for candidate in file_candidates: for file in path.rglob(candidate): diff --git a/bbot/scanner/preset/preset.py b/bbot/scanner/preset/preset.py index 386dbbe182..1ea9ebb2cf 100644 --- a/bbot/scanner/preset/preset.py +++ b/bbot/scanner/preset/preset.py @@ -17,7 +17,7 @@ log = logging.getLogger("bbot.presets") -_preset_cache = dict() +_preset_cache = {} # cache default presets to prevent having to reload from disk @@ -47,7 +47,6 @@ class Preset: target (Target): Target(s) of scan. whitelist (Target): Scan whitelist (by default this is the same as `target`). blacklist (Target): Scan blacklist (this takes ultimate precedence). - strict_scope (bool): If True, subdomains of targets are not considered to be in-scope. helpers (ConfigAwareHelper): Helper containing various reusable functions, regexes, etc. output_dir (pathlib.Path): Output directory for scan. scan_name (str): Name of scan. Defaults to random value, e.g. "demonic_jimmy". @@ -87,7 +86,6 @@ def __init__( *targets, whitelist=None, blacklist=None, - strict_scope=False, modules=None, output_modules=None, exclude_modules=None, @@ -117,7 +115,6 @@ def __init__( *targets (str): Target(s) to scan. Types supported: hostnames, IPs, CIDRs, emails, open ports. whitelist (list, optional): Whitelisted target(s) to scan. Defaults to the same as `targets`. blacklist (list, optional): Blacklisted target(s). Takes ultimate precedence. Defaults to empty. - strict_scope (bool, optional): If True, subdomains of targets are not in-scope. modules (list[str], optional): List of scan modules to enable for the scan. Defaults to empty list. output_modules (list[str], optional): List of output modules to use. Defaults to csv, human, and json. exclude_modules (list[str], optional): List of modules to exclude from the scan. @@ -234,7 +231,6 @@ def __init__( self.module_dirs = module_dirs # target / whitelist / blacklist - self.strict_scope = strict_scope # these are temporary receptacles until they all get .baked() together self._seeds = set(targets if targets else []) self._whitelist = set(whitelist) if whitelist else whitelist @@ -245,7 +241,7 @@ def __init__( # "presets" is alias to "include" if presets and include: raise ValueError( - 'Cannot use both "presets" and "include" args at the same time (presets is only an alias to include). Please pick only one :)' + 'Cannot use both "presets" and "include" args at the same time (presets is an alias to include). Please pick one or the other :)' ) if presets and not include: include = presets @@ -274,6 +270,12 @@ def target(self): raise ValueError("Cannot access target before preset is baked (use ._seeds instead)") return self._target + @property + def seeds(self): + if self._seeds is None: + raise ValueError("Cannot access target before preset is baked (use ._seeds instead)") + return self.target.seeds + @property def whitelist(self): if self._target is None: @@ -353,7 +355,6 @@ def merge(self, other): else: self._whitelist.update(other._whitelist) self._blacklist.update(other._blacklist) - self.strict_scope = self.strict_scope or other.strict_scope # module dirs self.module_dirs = self.module_dirs.union(other.module_dirs) @@ -442,7 +443,7 @@ def bake(self, scan=None): # disable internal modules if requested for internal_module in baked_preset.internal_modules: - if baked_preset.config.get(internal_module, True) == False: + if baked_preset.config.get(internal_module, True) is False: baked_preset.exclude_modules.add(internal_module) # enable modules by flag @@ -537,6 +538,14 @@ def config(self): def web_config(self): return self.core.config.get("web", {}) + @property + def scope_config(self): + return self.config.get("scope", {}) + + @property + def strict_scope(self): + return self.scope_config.get("strict", False) + def apply_log_level(self, apply_core=False): # silent takes precedence if self.silent: @@ -635,7 +644,6 @@ def from_dict(cls, preset_dict, name=None, _exclude=None, _log=False): debug=preset_dict.get("debug", False), silent=preset_dict.get("silent", False), config=preset_dict.get("config"), - strict_scope=preset_dict.get("strict_scope", False), module_dirs=preset_dict.get("module_dirs", []), include=list(preset_dict.get("include", [])), scan_name=preset_dict.get("scan_name"), @@ -753,19 +761,17 @@ def to_dict(self, include_target=False, full_config=False, redact_secrets=False) # scope if include_target: - target = sorted(str(t.data) for t in self.target.seeds) + target = sorted(self.target.seeds.inputs) whitelist = [] if self.target.whitelist is not None: - whitelist = sorted(str(t.data) for t in self.target.whitelist) - blacklist = sorted(str(t.data) for t in self.target.blacklist) + whitelist = sorted(self.target.whitelist.inputs) + blacklist = sorted(self.target.blacklist.inputs) if target: preset_dict["target"] = target if whitelist and whitelist != target: preset_dict["whitelist"] = whitelist if blacklist: preset_dict["blacklist"] = blacklist - if self.strict_scope: - preset_dict["strict_scope"] = True # flags + modules if self.require_flags: @@ -792,7 +798,7 @@ def to_dict(self, include_target=False, full_config=False, redact_secrets=False) # misc scan options if self.scan_name: preset_dict["scan_name"] = self.scan_name - if self.scan_name: + if self.scan_name and self.output_dir is not None: preset_dict["output_dir"] = self.output_dir # conditions @@ -834,7 +840,7 @@ def _is_valid_module(self, module, module_type, name_only=False, raise_error=Tru else: raise ValidationError(f'Unknown module type "{module}"') - if not module in module_choices: + if module not in module_choices: raise ValidationError(get_closest_match(module, module_choices, msg=f"{module_type} module")) try: @@ -877,21 +883,21 @@ def validate(self): # validate excluded modules for excluded_module in self.exclude_modules: - if not excluded_module in self.module_loader.all_module_choices: + if excluded_module not in self.module_loader.all_module_choices: raise ValidationError( get_closest_match(excluded_module, self.module_loader.all_module_choices, msg="module") ) # validate excluded flags for excluded_flag in self.exclude_flags: - if not excluded_flag in self.module_loader.flag_choices: + if excluded_flag not in self.module_loader.flag_choices: raise ValidationError(get_closest_match(excluded_flag, self.module_loader.flag_choices, msg="flag")) # validate required flags for required_flag in self.require_flags: - if not required_flag in self.module_loader.flag_choices: + if required_flag not in self.module_loader.flag_choices: raise ValidationError(get_closest_match(required_flag, self.module_loader.flag_choices, msg="flag")) # validate flags for flag in self.flags: - if not flag in self.module_loader.flag_choices: + if flag not in self.module_loader.flag_choices: raise ValidationError(get_closest_match(flag, self.module_loader.flag_choices, msg="flag")) @property @@ -910,7 +916,7 @@ def all_presets(self): global DEFAULT_PRESETS if DEFAULT_PRESETS is None: - presets = dict() + presets = {} for ext in ("yml", "yaml"): for preset_path in PRESET_PATH: # for every yaml file @@ -961,7 +967,7 @@ def presets_table(self, include_modules=True): header = ["Preset", "Category", "Description", "# Modules"] if include_modules: header.append("Modules") - for yaml_file, (loaded_preset, category, preset_path, original_file) in self.all_presets.items(): + for loaded_preset, category, preset_path, original_file in self.all_presets.values(): loaded_preset = loaded_preset.bake() num_modules = f"{len(loaded_preset.scan_modules):,}" row = [loaded_preset.name, category, loaded_preset.description, num_modules] diff --git a/bbot/scanner/scanner.py b/bbot/scanner/scanner.py index 6db56d1a33..3817f26b21 100644 --- a/bbot/scanner/scanner.py +++ b/bbot/scanner/scanner.py @@ -10,11 +10,11 @@ from collections import OrderedDict from bbot import __version__ - from bbot.core.event import make_event from .manager import ScanIngress, ScanEgress from bbot.core.helpers.misc import sha1, rand_string from bbot.core.helpers.names_generator import random_name +from bbot.core.multiprocess import SHARED_INTERPRETER_STATE from bbot.core.helpers.async_helpers import async_to_sync_gen from bbot.errors import BBOTError, ScanError, ValidationError @@ -124,6 +124,7 @@ def __init__( self.duration_seconds = None self._success = False + self._scan_finish_status_message = None if scan_id is not None: self.id = str(scan_id) @@ -214,8 +215,8 @@ def __init__( ) # url file extensions - self.url_extension_blacklist = set(e.lower() for e in self.config.get("url_extension_blacklist", [])) - self.url_extension_httpx_only = set(e.lower() for e in self.config.get("url_extension_httpx_only", [])) + self.url_extension_blacklist = {e.lower() for e in self.config.get("url_extension_blacklist", [])} + self.url_extension_httpx_only = {e.lower() for e in self.config.get("url_extension_httpx_only", [])} # url querystring behavior self.url_querystring_remove = self.config.get("url_querystring_remove", True) @@ -259,6 +260,9 @@ async def _prep(self): Creates the scan's output folder, loads its modules, and calls their .setup() methods. """ + # update the master PID + SHARED_INTERPRETER_STATE.update_scan_pid() + self.helpers.mkdir(self.home) if not self._prepped: # save scan preset @@ -266,7 +270,7 @@ async def _prep(self): f.write(self.preset.to_yaml()) # log scan overview - start_msg = f"Scan with {len(self.preset.scan_modules):,} modules seeded with {len(self.target):,} targets" + start_msg = f"Scan seeded with {len(self.seeds):,} targets" details = [] if self.whitelist != self.target: details.append(f"{len(self.whitelist):,} in whitelist") @@ -335,7 +339,7 @@ async def async_start(self): self.trace(f"Preset: {self.preset.to_dict(redact_secrets=True)}") if not self.target: - self.warning(f"No scan targets specified") + self.warning("No scan targets specified") # start status ticker self.ticker_task = asyncio.create_task( @@ -345,7 +349,7 @@ async def async_start(self): self.status = "STARTING" if not self.modules: - self.error(f"No modules loaded") + self.error("No modules loaded") self.status = "FAILED" return else: @@ -359,7 +363,8 @@ async def async_start(self): # distribute seed events self.init_events_task = asyncio.create_task( - self.ingress_module.init_events(self.target.events), name=f"{self.name}.ingress_module.init_events()" + self.ingress_module.init_events(self.target.seeds.events), + name=f"{self.name}.ingress_module.init_events()", ) # main scan loop @@ -421,14 +426,19 @@ async def async_start(self): self._stop_log_handlers() + if self._scan_finish_status_message: + log_fn = self.hugesuccess + if self.status.startswith("ABORT"): + log_fn = self.hugewarning + elif not self._success: + log_fn = self.critical + log_fn(self._scan_finish_status_message) + async def _mark_finished(self): - log_fn = self.hugesuccess if self.status == "ABORTING": status = "ABORTED" - log_fn = self.hugewarning elif not self._success: status = "FAILED" - log_fn = self.critical else: status = "FINISHED" @@ -437,9 +447,9 @@ async def _mark_finished(self): self.duration_seconds = self.duration.total_seconds() self.duration_human = self.helpers.human_timedelta(self.duration) - status_message = f"Scan {self.name} completed in {self.duration_human} with status {status}" + self._scan_finish_status_message = f"Scan {self.name} completed in {self.duration_human} with status {status}" - scan_finish_event = self.finish_event(status_message, status) + scan_finish_event = self.finish_event(self._scan_finish_status_message, status) # queue final scan event with output modules output_modules = [m for m in self.modules.values() if m._type == "output" and m.name != "python"] @@ -447,17 +457,16 @@ async def _mark_finished(self): await m.queue_event(scan_finish_event) # wait until output modules are flushed while 1: - modules_finished = all([m.finished for m in output_modules]) + modules_finished = all(m.finished for m in output_modules) if modules_finished: break await asyncio.sleep(0.05) self.status = status - log_fn(status_message) return scan_finish_event def _start_modules(self): - self.verbose(f"Starting module worker loops") + self.verbose("Starting module worker loops") for module in self.modules.values(): module.start() @@ -481,17 +490,17 @@ async def setup_modules(self, remove_failed=True): Soft-failed modules are not set to an error state but are also removed if `remove_failed` is True. """ await self.load_modules() - self.verbose(f"Setting up modules") + self.verbose("Setting up modules") succeeded = [] hard_failed = [] soft_failed = [] async for task in self.helpers.as_completed([m._setup() for m in self.modules.values()]): module, status, msg = await task - if status == True: + if status is True: self.debug(f"Setup succeeded for {module.name} ({msg})") succeeded.append(module.name) - elif status == False: + elif status is False: self.warning(f"Setup hard-failed for {module.name}: {msg}") self.modules[module.name].set_error_state() hard_failed.append(module.name) @@ -533,11 +542,11 @@ async def load_modules(self): """ if not self._modules_loaded: if not self.preset.modules: - self.warning(f"No modules to load") + self.warning("No modules to load") return if not self.preset.scan_modules: - self.warning(f"No scan modules to load") + self.warning("No scan modules to load") # install module dependencies succeeded, failed = await self.helpers.depsinstaller.install(*self.preset.modules) @@ -681,7 +690,7 @@ def modules_status(self, _log=False): if modules_errored: self.verbose( - f'{self.name}: Modules errored: {len(modules_errored):,} ({", ".join([m for m in modules_errored])})' + f'{self.name}: Modules errored: {len(modules_errored):,} ({", ".join(list(modules_errored))})' ) num_queued_events = self.num_queued_events @@ -718,7 +727,7 @@ def modules_status(self, _log=False): memory_usage = module.memory_usage module_memory_usage.append((module.name, memory_usage)) module_memory_usage.sort(key=lambda x: x[-1], reverse=True) - self.debug(f"MODULE MEMORY USAGE:") + self.debug("MODULE MEMORY USAGE:") for module_name, usage in module_memory_usage: self.debug(f" - {module_name}: {self.helpers.bytes_to_human(usage)}") @@ -765,7 +774,7 @@ async def finish(self): # Trigger .finished() on every module and start over log.info("Finishing scan") for module in self.modules.values(): - finished_event = self.make_event(f"FINISHED", "FINISHED", dummy=True, tags={module.name}) + finished_event = self.make_event("FINISHED", "FINISHED", dummy=True, tags={module.name}) await module.queue_event(finished_event) self.verbose("Completed finish()") return True @@ -893,6 +902,10 @@ def config(self): def target(self): return self.preset.target + @property + def seeds(self): + return self.preset.seeds + @property def whitelist(self): return self.preset.whitelist @@ -1016,7 +1029,7 @@ def dns_strings(self): A list of DNS hostname strings generated from the scan target """ if self._dns_strings is None: - dns_whitelist = set(t.host for t in self.whitelist if t.host and isinstance(t.host, str)) + dns_whitelist = {t.host for t in self.whitelist if t.host and isinstance(t.host, str)} dns_whitelist = sorted(dns_whitelist, key=len) dns_whitelist_set = set() dns_strings = [] @@ -1113,7 +1126,7 @@ def json(self): """ A dictionary representation of the scan including its name, ID, targets, whitelist, blacklist, and modules """ - j = dict() + j = {} for i in ("id", "name"): v = getattr(self, i, "") if v: @@ -1283,7 +1296,7 @@ def _handle_exception(self, e, context="scan", finally_callback=None, unhandled_ context = f"{context.__qualname__}()" filename, lineno, funcname = self.helpers.get_traceback_details(e) if self.helpers.in_exception_chain(e, (KeyboardInterrupt,)): - log.debug(f"Interrupted") + log.debug("Interrupted") self.stop() elif isinstance(e, BrokenPipeError): log.debug(f"BrokenPipeError in {filename}:{lineno}:{funcname}(): {e}") diff --git a/bbot/scanner/target.py b/bbot/scanner/target.py index aff8b3227f..bdd9edd107 100644 --- a/bbot/scanner/target.py +++ b/bbot/scanner/target.py @@ -1,112 +1,270 @@ -import re -import copy import logging -import ipaddress -import traceback +import regex as re from hashlib import sha1 -from contextlib import suppress from radixtarget import RadixTarget +from radixtarget.helpers import host_size_key from bbot.errors import * -from bbot.modules.base import BaseModule -from bbot.core.helpers.misc import make_ip_type from bbot.core.event import make_event, is_event +from bbot.core.helpers.misc import is_dns_name, is_ip + log = logging.getLogger("bbot.core.target") -class BBOTTarget: +def special_target_type(regex_pattern): + def decorator(func): + func._regex = re.compile(regex_pattern, re.IGNORECASE) + return func + + return decorator + + +class BaseTarget(RadixTarget): """ - A convenient abstraction of a scan target that includes whitelisting and blacklisting + A collection of BBOT events that represent a scan target. - Provides high-level functions like in_scope(), which includes both whitelist and blacklist checks. + Based on radixtarget, which allows extremely fast IP and DNS lookups. + + This class is inherited by all three components of the BBOT target: + - Whitelist + - Blacklist + - Seeds """ - def __init__(self, *targets, whitelist=None, blacklist=None, strict_scope=False, scan=None): - self.strict_scope = strict_scope + special_target_types = { + # regex-callback pairs for handling special target types + # these aren't defined explicitly; instead they are decorated with @special_target_type + # the function must return a list of events + } + tags = [] + + def __init__(self, *targets, scan=None, **kwargs): self.scan = scan - if len(targets) > 0: - log.verbose(f"Creating events from {len(targets):,} targets") - self.seeds = Target(*targets, strict_scope=self.strict_scope, scan=scan) - if whitelist is None: - whitelist = set([e.host for e in self.seeds if e.host]) + self.events = set() + self.inputs = set() + # Register decorated methods + for method in dir(self): + if callable(getattr(self, method, None)): + func = getattr(self, method) + if hasattr(func, "_regex"): + self.special_target_types[func._regex] = func + + super().__init__(*targets, **kwargs) + + def get(self, event, **kwargs): + """ + Override default .get() to accept events + """ + if is_event(event): + host = event.host + # save resources by checking if the event is an IP or DNS name + elif is_ip(event, include_network=True) or is_dns_name(event): + host = event + elif isinstance(event, str): + event = self.make_event(event) + host = event.host else: - log.verbose(f"Creating events from {len(whitelist):,} whitelist entries") - self.whitelist = Target(*whitelist, strict_scope=self.strict_scope, scan=scan, acl_mode=True) - if blacklist is None: - blacklist = [] - if blacklist: - log.verbose(f"Creating events from {len(blacklist):,} blacklist entries") - self.blacklist = Target(*blacklist, scan=scan, acl_mode=True) - self._hash = None + raise ValueError(f"Invalid host/event: {event} ({type(event)})") + if not host: + if kwargs.get("raise_error", False): + raise KeyError(f"Host not found: '{event}'") + return None + results = super().get(host, **kwargs) + return results + + def make_event(self, *args, **kwargs): + # if it's already an event, return it + if args and is_event(args[0]): + return args[0] + # otherwise make a new one + if "tags" not in kwargs: + kwargs["tags"] = set() + kwargs["tags"].update(self.tags) + return make_event(*args, dummy=True, scan=self.scan, **kwargs) + + def add(self, targets): + if not isinstance(targets, (list, set, tuple)): + targets = [targets] + events = set() + for target in targets: + _events = [] + special_target_type, _events = self.check_special_target_types(str(target)) + if special_target_type: + self.inputs.add(str(target)) + else: + event = self.make_event(target) + if event: + self.inputs.add(target) + _events = [event] + for event in _events: + events.add(event) + + # sort by host size to ensure consistency + events = sorted(events, key=lambda e: ((0, 0) if not e.host else host_size_key(e.host))) + for event in events: + self.events.add(event) + self._add(event.host, data=event) + + def check_special_target_types(self, target): + for regex, callback in self.special_target_types.items(): + match = regex.match(target) + if match: + return True, callback(match) + return False, [] - def add(self, *args, **kwargs): - self.seeds.add(*args, **kwargs) - self._hash = None + def __iter__(self): + yield from self.events - def get(self, host): - return self.seeds.get(host) - def get_host(self, host): - return self.seeds.get(host) +class ScanSeeds(BaseTarget): + """ + Initial events used to seed a scan. - def __iter__(self): - return iter(self.seeds) + These are the targets specified by the user, e.g. via `-t` on the CLI. + """ - def __len__(self): - return len(self.seeds) + tags = ["target"] + + @special_target_type(r"^(?:ORG|ORG_STUB):(.*)") + def handle_org_stub(self, match): + org_stub_event = self.make_event(match.group(1), event_type="ORG_STUB") + if org_stub_event: + return [org_stub_event] + return [] + + @special_target_type(r"^(?:USER|USERNAME):(.*)") + def handle_username(self, match): + username_event = self.make_event(match.group(1), event_type="USERNAME") + if username_event: + return [username_event] + return [] + + @special_target_type(r"^(?:FILESYSTEM|FILE|FOLDER|DIR|PATH):(.*)") + def handle_filesystem(self, match): + filesystem_event = self.make_event({"path": match.group(1)}, event_type="FILESYSTEM") + if filesystem_event: + return [filesystem_event] + return [] + + @special_target_type(r"^(?:MOBILE_APP|APK|IPA|APP):(.*)") + def handle_mobile_app(self, match): + mobile_app_event = self.make_event({"url": match.group(1)}, event_type="MOBILE_APP") + if mobile_app_event: + return [mobile_app_event] + return [] + + def get(self, event, single=True, **kwargs): + results = super().get(event, **kwargs) + if results and single: + return next(iter(results)) + return results + + def _add(self, host, data): + """ + Overrides the base method to enable having multiple events for the same host. - def __contains__(self, other): - if isinstance(other, self.__class__): - other = other.seeds - return other in self.seeds + The "data" attribute of the node is now a set of events. + """ + if host: + try: + event_set = self.get(host, raise_error=True, single=False) + event_set.add(data) + except KeyError: + event_set = {data} + super()._add(host, data=event_set) - def __bool__(self): - return bool(self.seeds) + def _hash_value(self): + # seeds get hashed by event data + return sorted(str(e.data).encode() for e in self.events) - def __eq__(self, other): - return self.hash == other.hash - @property - def hash(self): - """ - A sha1 hash representing a BBOT target and all three of its components (seeds, whitelist, blacklist) +class ACLTarget(BaseTarget): + def __init__(self, *args, **kwargs): + # ACL mode dedupes by host (and skips adding already-contained hosts) for efficiency + kwargs["acl_mode"] = True + super().__init__(*args, **kwargs) - This can be used to compare targets. - Examples: - >>> target1 = BBOTTarget("evilcorp.com", blacklist=["prod.evilcorp.com"], whitelist=["test.evilcorp.com"]) - >>> target2 = BBOTTarget("evilcorp.com", blacklist=["prod.evilcorp.com"], whitelist=["test.evilcorp.com"]) - >>> target3 = BBOTTarget("evilcorp.com", blacklist=["prod.evilcorp.com"]) - >>> target1 == target2 - True - >>> target1 == target3 - False - """ - if self._hash is None: - # Create a new SHA-1 hash object - sha1_hash = sha1() - # Update the SHA-1 object with the hash values of each object - for target_hash in [t.hash for t in (self.seeds, self.whitelist, self.blacklist)]: - # Convert the hash value to bytes and update the SHA-1 object - sha1_hash.update(target_hash) - self._hash = sha1_hash.digest() - return self._hash +class ScanWhitelist(ACLTarget): + """ + A collection of BBOT events that represent a scan's whitelist. + """ - @property - def scope_hash(self): - """ - A sha1 hash representing only the whitelist and blacklist + pass + + +class ScanBlacklist(ACLTarget): + """ + A collection of BBOT events that represent a scan's blacklist. + """ + + def __init__(self, *args, **kwargs): + self.blacklist_regexes = set() + super().__init__(*args, **kwargs) - This is used to record the scope of a scan. + @special_target_type(r"^(?:RE|REGEX):(.*)") + def handle_regex(self, match): + pattern = match.group(1) + blacklist_regex = re.compile(pattern, re.IGNORECASE) + self.blacklist_regexes.add(blacklist_regex) + return [] + + def get(self, event, **kwargs): """ - # Create a new SHA-1 hash object - sha1_hash = sha1() - # Update the SHA-1 object with the hash values of each object - for target_hash in [t.hash for t in (self.whitelist, self.blacklist)]: - # Convert the hash value to bytes and update the SHA-1 object - sha1_hash.update(target_hash) - return sha1_hash.digest() + Here, for the blacklist, we modify this method to also consider any special regex patterns specified by the user + """ + event = self.make_event(event) + # first, check event's host against blacklist + try: + event_result = super().get(event, raise_error=True) + except KeyError: + event_result = None + if event_result is not None: + return event_result + # next, check event's host against regexes + host_or_url = event.host_filterable + if host_or_url: + for regex in self.blacklist_regexes: + if regex.search(str(host_or_url)): + return event + if kwargs.get("raise_error", False): + raise KeyError(f"Host not found: '{event.data}'") + return None + + def _hash_value(self): + # regexes are included in blacklist hash + regex_patterns = [str(r.pattern).encode() for r in self.blacklist_regexes] + hosts = [str(h).encode() for h in self.sorted_hosts] + return hosts + regex_patterns + + def __len__(self): + return super().__len__() + len(self.blacklist_regexes) + + def __bool__(self): + return bool(len(self)) + + +class BBOTTarget: + """ + A convenient abstraction of a scan target that contains three subtargets: + - seeds + - whitelist + - blacklist + + Provides high-level functions like in_scope(), which includes both whitelist and blacklist checks. + """ + + def __init__(self, *seeds, whitelist=None, blacklist=None, strict_scope=False, scan=None): + self.scan = scan + self.strict_scope = strict_scope + self.seeds = ScanSeeds(*seeds, strict_dns_scope=strict_scope, scan=scan) + if whitelist is None: + whitelist = self.seeds.hosts + self.whitelist = ScanWhitelist(*whitelist, strict_dns_scope=strict_scope, scan=scan) + if blacklist is None: + blacklist = [] + self.blacklist = ScanBlacklist(*blacklist, scan=scan) @property def json(self): @@ -122,16 +280,20 @@ def json(self): "scope_hash": self.scope_hash.hex(), } - def copy(self): - self_copy = copy.copy(self) - self_copy.seeds = self.seeds.copy() - self_copy.whitelist = self.whitelist.copy() - self_copy.blacklist = self.blacklist.copy() - return self_copy + @property + def hash(self): + sha1_hash = sha1() + for target_hash in [t.hash for t in (self.seeds, self.whitelist, self.blacklist)]: + sha1_hash.update(target_hash) + return sha1_hash.digest() @property - def events(self): - return self.seeds.events + def scope_hash(self): + sha1_hash = sha1() + # Consider only the hash values of the whitelist and blacklist + for target_hash in [t.hash for t in (self.whitelist, self.blacklist)]: + sha1_hash.update(target_hash) + return sha1_hash.digest() def in_scope(self, host): """ @@ -167,8 +329,7 @@ def blacklisted(self, host): >>> preset.blacklisted("http://www.evilcorp.com") True """ - e = make_event(host, dummy=True) - return e in self.blacklist + return host in self.blacklist def whitelisted(self, host): """ @@ -184,360 +345,20 @@ def whitelisted(self, host): >>> preset.whitelisted("http://www.evilcorp.com") True """ - e = make_event(host, dummy=True) - whitelist = self.whitelist - if whitelist is None: - whitelist = self.seeds - return e in whitelist + return host in self.whitelist @property - def radix_only(self): + def minimal(self): """ A slimmer, serializable version of the target designed for simple scope checks - This version doesn't have the events, only their hosts. + This version doesn't have the events, only their hosts. This allows it to be passed across process boundaries. """ return self.__class__( - *[e.host for e in self.seeds if e.host], - whitelist=None if self.whitelist is None else [e for e in self.whitelist], - blacklist=[e for e in self.blacklist], + whitelist=self.whitelist.inputs, + blacklist=self.blacklist.inputs, strict_scope=self.strict_scope, ) - -class Target: - """ - A class representing a target. Can contain an unlimited number of hosts, IP or IP ranges, URLs, etc. - - Attributes: - strict_scope (bool): Flag indicating whether to consider child domains in-scope. - If set to True, only the exact hosts specified and not their children are considered part of the target. - - _radix (RadixTree): Radix tree for quick IP/DNS lookups. - _events (set): Flat set of contained events. - - Examples: - Basic usage - >>> target = Target(scan, "evilcorp.com", "1.2.3.0/24") - >>> len(target) - 257 - >>> list(t.events) - [ - DNS_NAME("evilcorp.com", module=TARGET, tags={'domain', 'distance-1', 'target'}), - IP_RANGE("1.2.3.0/24", module=TARGET, tags={'ipv4', 'distance-1', 'target'}) - ] - >>> "www.evilcorp.com" in target - True - >>> "1.2.3.4" in target - True - >>> "4.3.2.1" in target - False - >>> "https://admin.evilcorp.com" in target - True - >>> "bob@evilcorp.com" in target - True - - Event correlation - >>> target.get("www.evilcorp.com") - DNS_NAME("evilcorp.com", module=TARGET, tags={'domain', 'distance-1', 'target'}) - >>> target.get("1.2.3.4") - IP_RANGE("1.2.3.0/24", module=TARGET, tags={'ipv4', 'distance-1', 'target'}) - - Target comparison - >>> target2 = Targets(scan, "www.evilcorp.com") - >>> target2 == target - False - >>> target2 in target - True - >>> target in target2 - False - - Notes: - - Targets are only precise down to the individual host. Ports and protocols are not considered in scope calculations. - - If you specify "https://evilcorp.com:8443" as a target, all of evilcorp.com (including subdomains and other ports and protocols) will be considered part of the target - - If you do not want to include child subdomains, use `strict_scope=True` - """ - - def __init__(self, *targets, strict_scope=False, scan=None, acl_mode=False): - """ - Initialize a Target object. - - Args: - *targets: One or more targets (e.g., domain names, IP ranges) to be included in this Target. - strict_scope (bool): Whether to consider subdomains of target domains in-scope - scan (Scan): Reference to the Scan object that instantiated the Target. - acl_mode (bool): Stricter deduplication for more efficient checks - - Notes: - - If you are instantiating a target from within a BBOT module, use `self.helpers.make_target()` instead. (this removes the need to pass in a scan object.) - - The strict_scope flag can be set to restrict scope calculation to only exactly-matching hosts and not their child subdomains. - - Each target is processed and stored as an `Event` in the '_events' dictionary. - """ - self.scan = scan - self.strict_scope = strict_scope - self.acl_mode = acl_mode - self.special_event_types = { - "ORG_STUB": re.compile(r"^(?:ORG|ORG_STUB):(.*)", re.IGNORECASE), - "USERNAME": re.compile(r"^(?:USER|USERNAME):(.*)", re.IGNORECASE), - } - self._events = set() - self._radix = RadixTarget() - - for target_event in self._make_events(targets): - self._add_event(target_event) - - self._hash = None - - def add(self, t, event_type=None): - """ - Add a target or merge events from another Target object into this Target. - - Args: - t: The target to be added. It can be either a string, an event object, or another Target object. - - Attributes Modified: - _events (dict): The dictionary is updated to include the new target's events. - - Examples: - >>> target.add('example.com') - - Notes: - - If `t` is of the same class as this Target, all its events are merged. - - If `t` is an event, it is directly added to `_events`. - """ - if not isinstance(t, (list, tuple, set)): - t = [t] - for single_target in t: - if isinstance(single_target, self.__class__): - for event in single_target.events: - self._add_event(event) - else: - if is_event(single_target): - event = single_target - else: - try: - event = make_event( - single_target, event_type=event_type, dummy=True, tags=["target"], scan=self.scan - ) - except ValidationError as e: - # allow commented lines - if not str(t).startswith("#"): - log.trace(traceback.format_exc()) - raise ValidationError(f'Could not add target "{t}": {e}') - self._add_event(event) - - @property - def events(self): - """ - Returns all events in the target. - - Yields: - Event object: One of the Event objects stored in the `_events` dictionary. - - Examples: - >>> target = Target(scan, "example.com") - >>> for event in target.events: - ... print(event) - - Notes: - - This property is read-only. - """ - return self._events - - @property - def hosts(self): - return [e.host for e in self.events] - - def copy(self): - """ - Creates and returns a copy of the Target object, including a shallow copy of the `_events` and `_radix` attributes. - - Returns: - Target: A new Target object with the sameattributes as the original. - A shallow copy of the `_events` dictionary is made. - - Examples: - >>> original_target = Target(scan, "example.com") - >>> copied_target = original_target.copy() - >>> copied_target is original_target - False - >>> copied_target == original_target - True - >>> copied_target in original_target - True - >>> original_target in copied_target - True - - Notes: - - The `scan` object reference is kept intact in the copied Target object. - """ - self_copy = self.__class__() - self_copy._events = set(self._events) - self_copy._radix = copy.copy(self._radix) - return self_copy - - def get(self, host, single=True): - """ - Gets the event associated with the specified host from the target's radix tree. - - Args: - host (Event, Target, or str): The hostname, IP, URL, or event to look for. - single (bool): Whether to return a single event. If False, return all events matching the host - - Returns: - Event or None: Returns the Event object associated with the given host if it exists, otherwise returns None. - - Examples: - >>> target = Target(scan, "evilcorp.com", "1.2.3.0/24") - >>> target.get("www.evilcorp.com") - DNS_NAME("evilcorp.com", module=TARGET, tags={'domain', 'distance-1', 'target'}) - >>> target.get("1.2.3.4") - IP_RANGE("1.2.3.0/24", module=TARGET, tags={'ipv4', 'distance-1', 'target'}) - - Notes: - - The method returns the first event that matches the given host. - - If `strict_scope` is False, it will also consider parent domains and IP ranges. - """ - try: - event = make_event(host, dummy=True) - except ValidationError: - return - if event.host: - return self.get_host(event.host, single=single) - - def get_host(self, host, single=True): - """ - A more efficient version of .get() that only accepts hostnames and IP addresses - """ - host = make_ip_type(host) - with suppress(KeyError, StopIteration): - result = self._radix.search(host) - if result is not None: - ret = set() - for event in result: - # if the result is a dns name and strict scope is enabled - if isinstance(event.host, str) and self.strict_scope: - # if the result doesn't exactly equal the host, abort - if event.host != host: - return - if single: - return event - else: - ret.add(event) - if ret and not single: - return ret - - def _sort_events(self, events): - return sorted(events, key=lambda x: x._host_size) - - def _make_events(self, targets): - events = [] - for target in targets: - event_type = None - for eventtype, regex in self.special_event_types.items(): - if isinstance(target, str): - match = regex.match(target) - if match: - target = match.groups()[0] - event_type = eventtype - break - events.append(make_event(target, event_type=event_type, dummy=True, scan=self.scan)) - return self._sort_events(events) - - def _add_event(self, event): - skip = False - if event.host: - radix_data = self._radix.search(event.host) - if self.acl_mode: - # skip if the hostname/IP/subnet (or its parent) has already been added - if radix_data is not None and not self.strict_scope: - skip = True - else: - event_type = "IP_RANGE" if event.type == "IP_RANGE" else "DNS_NAME" - event = make_event(event.host, event_type=event_type, dummy=True, scan=self.scan) - if not skip: - # if strict scope is enabled and it's not an exact host match, we add a whole new entry - if radix_data is None or (self.strict_scope and event.host not in radix_data): - radix_data = {event} - self._radix.insert(event.host, radix_data) - # otherwise, we add the event to the set - else: - radix_data.add(event) - # clear hash - self._hash = None - elif self.acl_mode and not self.strict_scope: - # skip if we're in ACL mode and there's no host - skip = True - if not skip: - self._events.add(event) - - def _contains(self, other): - if self.get(other) is not None: - return True - return False - - def __str__(self): - return ",".join([str(e.data) for e in self.events][:5]) - - def __iter__(self): - yield from self.events - - def __contains__(self, other): - # if "other" is a Target - if isinstance(other, self.__class__): - contained_in_self = [self._contains(e) for e in other.events] - return all(contained_in_self) - else: - return self._contains(other) - - def __bool__(self): - return bool(self._events) - def __eq__(self, other): return self.hash == other.hash - - @property - def hash(self): - if self._hash is None: - # Create a new SHA-1 hash object - sha1_hash = sha1() - # Update the SHA-1 object with the hash values of each object - for event_type, event_hash in sorted([(e.type.encode(), e.data_hash) for e in self.events]): - sha1_hash.update(event_type) - sha1_hash.update(event_hash) - if self.strict_scope: - sha1_hash.update(b"\x00") - self._hash = sha1_hash.digest() - return self._hash - - def __len__(self): - """ - Calculates and returns the total number of hosts within this target, not counting duplicate events. - - Returns: - int: The total number of unique hosts present within the target's `_events`. - - Examples: - >>> target = Target(scan, "evilcorp.com", "1.2.3.0/24") - >>> len(target) - 257 - - Notes: - - If a host is represented as an IP network, all individual IP addresses in that network are counted. - - For other types of hosts, each unique event is counted as one. - """ - num_hosts = 0 - for event in self._events: - if isinstance(event.host, (ipaddress.IPv4Network, ipaddress.IPv6Network)): - num_hosts += event.host.num_addresses - else: - num_hosts += 1 - return num_hosts - - -class TargetDummyModule(BaseModule): - _type = "TARGET" - name = "TARGET" - - def __init__(self, scan): - self.scan = scan diff --git a/bbot/scripts/docs.py b/bbot/scripts/docs.py index b1664b023c..708102f7ca 100755 --- a/bbot/scripts/docs.py +++ b/bbot/scripts/docs.py @@ -124,7 +124,7 @@ def find_replace_file(file, keyword, replace): content = f.read() new_content = find_replace_markdown(content, keyword, replace) if new_content != content: - if not "BBOT_TESTING" in os.environ: + if "BBOT_TESTING" not in os.environ: with open(file, "w") as f: f.write(new_content) diff --git a/bbot/test/bbot_fixtures.py b/bbot/test/bbot_fixtures.py index 0b2a0ec573..070df6e9a3 100644 --- a/bbot/test/bbot_fixtures.py +++ b/bbot/test/bbot_fixtures.py @@ -15,11 +15,11 @@ from bbot.errors import * # noqa: F401 from bbot.core import CORE from bbot.scanner import Preset -from bbot.core.helpers.misc import mkdir, rand_string from bbot.core.helpers.async_helpers import get_event_loop +from bbot.core.helpers.misc import mkdir, rand_string, get_python_constraints -log = logging.getLogger(f"bbot.test.fixtures") +log = logging.getLogger("bbot.test.fixtures") bbot_test_dir = Path("/tmp/.bbot_test") @@ -229,4 +229,7 @@ def install_all_python_deps(): deps_pip = set() for module in DEFAULT_PRESET.module_loader.preloaded().values(): deps_pip.update(set(module.get("deps", {}).get("pip", []))) - subprocess.run([sys.executable, "-m", "pip", "install"] + list(deps_pip)) + + constraint_file = tempwordlist(get_python_constraints()) + + subprocess.run([sys.executable, "-m", "pip", "install", "--constraint", constraint_file] + list(deps_pip)) diff --git a/bbot/test/conftest.py b/bbot/test/conftest.py index c2e8b3448a..f9807db819 100644 --- a/bbot/test/conftest.py +++ b/bbot/test/conftest.py @@ -16,7 +16,6 @@ # silence stdout + trace root_logger = logging.getLogger() pytest_debug_file = Path(__file__).parent.parent.parent / "pytest_debug.log" -print(f"pytest_debug_file: {pytest_debug_file}") debug_handler = logging.FileHandler(pytest_debug_file) debug_handler.setLevel(logging.DEBUG) debug_format = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s %(filename)s:%(lineno)s %(message)s") @@ -95,9 +94,21 @@ def bbot_httpserver_ssl(): server.clear() -@pytest.fixture -def non_mocked_hosts() -> list: - return ["127.0.0.1", "localhost", "raw.githubusercontent.com"] + interactsh_servers +def should_mock(request): + return request.url.host not in ["127.0.0.1", "localhost", "raw.githubusercontent.com"] + interactsh_servers + + +def pytest_collection_modifyitems(config, items): + # make sure all tests have the httpx_mock marker + for item in items: + item.add_marker( + pytest.mark.httpx_mock( + should_mock=should_mock, + assert_all_requests_were_expected=False, + assert_all_responses_were_requested=False, + can_send_already_matched_responses=True, + ) + ) @pytest.fixture @@ -240,80 +251,80 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): # pragma: no # BELOW: debugging for frozen/hung tests -# import psutil -# import traceback -# import inspect - - -# def _print_detailed_info(): # pragma: no cover -# """ -# Debugging pytests hanging -# """ -# print("=== Detailed Thread and Process Information ===\n") -# try: -# print("=== Threads ===") -# for thread in threading.enumerate(): -# print(f"Thread Name: {thread.name}") -# print(f"Thread ID: {thread.ident}") -# print(f"Is Alive: {thread.is_alive()}") -# print(f"Daemon: {thread.daemon}") - -# if hasattr(thread, "_target"): -# target = thread._target -# if target: -# qualname = ( -# f"{target.__module__}.{target.__qualname__}" -# if hasattr(target, "__qualname__") -# else str(target) -# ) -# print(f"Target Function: {qualname}") - -# if hasattr(thread, "_args"): -# args = thread._args -# kwargs = thread._kwargs if hasattr(thread, "_kwargs") else {} -# arg_spec = inspect.getfullargspec(target) - -# all_args = list(args) + [f"{k}={v}" for k, v in kwargs.items()] - -# if inspect.ismethod(target) and arg_spec.args[0] == "self": -# arg_spec.args.pop(0) - -# named_args = list(zip(arg_spec.args, all_args)) -# if arg_spec.varargs: -# named_args.extend((f"*{arg_spec.varargs}", arg) for arg in all_args[len(arg_spec.args) :]) - -# print("Arguments:") -# for name, value in named_args: -# print(f" {name}: {value}") -# else: -# print("Target Function: None") -# else: -# print("Target Function: Unknown") - -# print() - -# print("=== Processes ===") -# current_process = psutil.Process() -# for child in current_process.children(recursive=True): -# print(f"Process ID: {child.pid}") -# print(f"Name: {child.name()}") -# print(f"Status: {child.status()}") -# print(f"CPU Times: {child.cpu_times()}") -# print(f"Memory Info: {child.memory_info()}") -# print() - -# print("=== Current Process ===") -# print(f"Process ID: {current_process.pid}") -# print(f"Name: {current_process.name()}") -# print(f"Status: {current_process.status()}") -# print(f"CPU Times: {current_process.cpu_times()}") -# print(f"Memory Info: {current_process.memory_info()}") -# print() - -# except Exception as e: -# print(f"An error occurred: {str(e)}") -# print("Traceback:") -# traceback.print_exc() +import psutil +import traceback +import inspect + + +def _print_detailed_info(): # pragma: no cover + """ + Debugging pytests hanging + """ + print("=== Detailed Thread and Process Information ===\n") + try: + print("=== Threads ===") + for thread in threading.enumerate(): + print(f"Thread Name: {thread.name}") + print(f"Thread ID: {thread.ident}") + print(f"Is Alive: {thread.is_alive()}") + print(f"Daemon: {thread.daemon}") + + if hasattr(thread, "_target"): + target = thread._target + if target: + qualname = ( + f"{target.__module__}.{target.__qualname__}" + if hasattr(target, "__qualname__") + else str(target) + ) + print(f"Target Function: {qualname}") + + if hasattr(thread, "_args"): + args = thread._args + kwargs = thread._kwargs if hasattr(thread, "_kwargs") else {} + arg_spec = inspect.getfullargspec(target) + + all_args = list(args) + [f"{k}={v}" for k, v in kwargs.items()] + + if inspect.ismethod(target) and arg_spec.args[0] == "self": + arg_spec.args.pop(0) + + named_args = list(zip(arg_spec.args, all_args)) + if arg_spec.varargs: + named_args.extend((f"*{arg_spec.varargs}", arg) for arg in all_args[len(arg_spec.args) :]) + + print("Arguments:") + for name, value in named_args: + print(f" {name}: {value}") + else: + print("Target Function: None") + else: + print("Target Function: Unknown") + + print() + + print("=== Processes ===") + current_process = psutil.Process() + for child in current_process.children(recursive=True): + print(f"Process ID: {child.pid}") + print(f"Name: {child.name()}") + print(f"Status: {child.status()}") + print(f"CPU Times: {child.cpu_times()}") + print(f"Memory Info: {child.memory_info()}") + print() + + print("=== Current Process ===") + print(f"Process ID: {current_process.pid}") + print(f"Name: {current_process.name()}") + print(f"Status: {current_process.status()}") + print(f"CPU Times: {current_process.cpu_times()}") + print(f"Memory Info: {current_process.memory_info()}") + print() + + except Exception as e: + print(f"An error occurred: {str(e)}") + print("Traceback:") + traceback.print_exc() @pytest.hookimpl(tryfirst=True, hookwrapper=True) @@ -331,11 +342,11 @@ def pytest_sessionfinish(session, exitstatus): yield # temporarily suspend stdout capture and print detailed thread info - # capmanager = session.config.pluginmanager.get_plugin("capturemanager") - # if capmanager: - # capmanager.suspend_global_capture(in_=True) + capmanager = session.config.pluginmanager.get_plugin("capturemanager") + if capmanager: + capmanager.suspend_global_capture(in_=True) - # _print_detailed_info() + _print_detailed_info() - # if capmanager: - # capmanager.resume_global_capture() + if capmanager: + capmanager.resume_global_capture() diff --git a/bbot/test/fastapi_test.py b/bbot/test/fastapi_test.py new file mode 100644 index 0000000000..f0c7b2d789 --- /dev/null +++ b/bbot/test/fastapi_test.py @@ -0,0 +1,17 @@ +from typing import List +from bbot import Scanner +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/start") +async def start(targets: List[str] = Query(...)): + scanner = Scanner(*targets, modules=["httpx"]) + events = [e async for e in scanner.async_start()] + return [e.json() for e in events] + + +@app.get("/ping") +async def ping(): + return {"status": "ok"} diff --git a/bbot/test/run_tests.sh b/bbot/test/run_tests.sh index 39458dbf9f..55e7b430bb 100755 --- a/bbot/test/run_tests.sh +++ b/bbot/test/run_tests.sh @@ -3,14 +3,14 @@ bbot_dir="$( realpath "$(dirname "$(dirname "${BASH_SOURCE[0]}")")")" echo -e "[+] BBOT dir: $bbot_dir\n" -echo "[+] Checking code formatting with black" +echo "[+] Checking code formatting with ruff" echo "=======================================" -black --check "$bbot_dir" || exit 1 +ruff format "$bbot_dir" || exit 1 echo -echo "[+] Linting with flake8" +echo "[+] Linting with ruff" echo "=======================" -flake8 "$bbot_dir" || exit 1 +ruff check "$bbot_dir" || exit 1 echo if [ "${1}x" != "x" ] ; then diff --git a/bbot/test/test.conf b/bbot/test/test.conf index 63914fe655..1c6a19dbf7 100644 --- a/bbot/test/test.conf +++ b/bbot/test/test.conf @@ -36,6 +36,8 @@ dns: - example.com - evilcorp.com - one +deps: + behavior: retry_failed engine: debug: true agent_url: ws://127.0.0.1:8765 diff --git a/bbot/test/test_step_1/test__module__tests.py b/bbot/test/test_step_1/test__module__tests.py index 791e58f58a..e50f67a910 100644 --- a/bbot/test/test_step_1/test__module__tests.py +++ b/bbot/test/test_step_1/test__module__tests.py @@ -15,7 +15,6 @@ def test__module__tests(): - preset = Preset() # make sure each module has a .py file diff --git a/bbot/test/test_step_1/test_bbot_fastapi.py b/bbot/test/test_step_1/test_bbot_fastapi.py new file mode 100644 index 0000000000..1136963a3d --- /dev/null +++ b/bbot/test/test_step_1/test_bbot_fastapi.py @@ -0,0 +1,79 @@ +import time +import httpx +import multiprocessing +from pathlib import Path +from subprocess import Popen +from contextlib import suppress + +cwd = Path(__file__).parent.parent.parent + + +def run_bbot_multiprocess(queue): + from bbot import Scanner + + scan = Scanner("http://127.0.0.1:8888", "blacklanternsecurity.com", modules=["httpx"]) + events = [e.json() for e in scan.start()] + queue.put(events) + + +def test_bbot_multiprocess(bbot_httpserver): + bbot_httpserver.expect_request("/").respond_with_data("test@blacklanternsecurity.com") + + queue = multiprocessing.Queue() + events_process = multiprocessing.Process(target=run_bbot_multiprocess, args=(queue,)) + events_process.start() + events_process.join() + events = queue.get() + assert len(events) >= 3 + scan_events = [e for e in events if e["type"] == "SCAN"] + assert len(scan_events) == 2 + assert any(e["data"] == "test@blacklanternsecurity.com" for e in events) + + +def test_bbot_fastapi(bbot_httpserver): + bbot_httpserver.expect_request("/").respond_with_data("test@blacklanternsecurity.com") + fastapi_process = start_fastapi_server() + + try: + # wait for the server to start with a timeout of 60 seconds + start_time = time.time() + while True: + try: + response = httpx.get("http://127.0.0.1:8978/ping") + response.raise_for_status() + break + except httpx.HTTPError: + if time.time() - start_time > 60: + raise TimeoutError("Server did not start within 60 seconds.") + time.sleep(0.1) + continue + + # run a scan + response = httpx.get( + "http://127.0.0.1:8978/start", + params={"targets": ["http://127.0.0.1:8888", "blacklanternsecurity.com"]}, + timeout=100, + ) + events = response.json() + assert len(events) >= 3 + scan_events = [e for e in events if e["type"] == "SCAN"] + assert len(scan_events) == 2 + assert any(e["data"] == "test@blacklanternsecurity.com" for e in events) + + finally: + with suppress(Exception): + fastapi_process.terminate() + + +def start_fastapi_server(): + import os + import sys + + env = os.environ.copy() + with suppress(KeyError): + del env["BBOT_TESTING"] + python_executable = str(sys.executable) + process = Popen( + [python_executable, "-m", "uvicorn", "bbot.test.fastapi_test:app", "--port", "8978"], cwd=cwd, env=env + ) + return process diff --git a/bbot/test/test_step_1/test_bloom_filter.py b/bbot/test/test_step_1/test_bloom_filter.py index e57c56110b..f954bfbc6e 100644 --- a/bbot/test/test_step_1/test_bloom_filter.py +++ b/bbot/test/test_step_1/test_bloom_filter.py @@ -6,7 +6,6 @@ @pytest.mark.asyncio async def test_bloom_filter(): - def generate_random_strings(n, length=10): """Generate a list of n random strings.""" return ["".join(random.choices(string.ascii_letters + string.digits, k=length)) for _ in range(n)] @@ -66,4 +65,6 @@ def generate_random_strings(n, length=10): # ensure false positives are less than .02 percent assert false_positive_percent < 0.02 + bloom_filter.close() + await scan._cleanup() diff --git a/bbot/test/test_step_1/test_cli.py b/bbot/test/test_step_1/test_cli.py index f34b7c1474..c700cfaad8 100644 --- a/bbot/test/test_step_1/test_cli.py +++ b/bbot/test/test_step_1/test_cli.py @@ -1,3 +1,5 @@ +import yaml + from ..bbot_fixtures import * from bbot import cli @@ -17,11 +19,11 @@ async def test_cli_scope(monkeypatch, capsys): ) result = await cli._main() out, err = capsys.readouterr() - assert result == True + assert result is True lines = [json.loads(l) for l in out.splitlines()] dns_events = [l for l in lines if l["type"] == "DNS_NAME" and l["data"] == "one.one.one.one"] assert dns_events - assert all([l["scope_distance"] == 0 and "in-scope" in l["tags"] for l in dns_events]) + assert all(l["scope_distance"] == 0 and "in-scope" in l["tags"] for l in dns_events) assert 1 == len( [ l @@ -34,10 +36,10 @@ async def test_cli_scope(monkeypatch, capsys): ) ip_events = [l for l in lines if l["type"] == "IP_ADDRESS" and l["data"] == "1.1.1.1"] assert ip_events - assert all([l["scope_distance"] == 1 and "distance-1" in l["tags"] for l in ip_events]) + assert all(l["scope_distance"] == 1 and "distance-1" in l["tags"] for l in ip_events) ip_events = [l for l in lines if l["type"] == "IP_ADDRESS" and l["data"] == "1.0.0.1"] assert ip_events - assert all([l["scope_distance"] == 1 and "distance-1" in l["tags"] for l in ip_events]) + assert all(l["scope_distance"] == 1 and "distance-1" in l["tags"] for l in ip_events) # with whitelist monkeypatch.setattr( @@ -57,14 +59,14 @@ async def test_cli_scope(monkeypatch, capsys): ) result = await cli._main() out, err = capsys.readouterr() - assert result == True + assert result is True lines = [json.loads(l) for l in out.splitlines()] lines = [l for l in lines if l["type"] != "SCAN"] assert lines - assert not any([l["scope_distance"] == 0 for l in lines]) + assert not any(l["scope_distance"] == 0 for l in lines) dns_events = [l for l in lines if l["type"] == "DNS_NAME" and l["data"] == "one.one.one.one"] assert dns_events - assert all([l["scope_distance"] == 1 and "distance-1" in l["tags"] for l in dns_events]) + assert all(l["scope_distance"] == 1 and "distance-1" in l["tags"] for l in dns_events) assert 1 == len( [ l @@ -77,10 +79,10 @@ async def test_cli_scope(monkeypatch, capsys): ) ip_events = [l for l in lines if l["type"] == "IP_ADDRESS" and l["data"] == "1.1.1.1"] assert ip_events - assert all([l["scope_distance"] == 2 and "distance-2" in l["tags"] for l in ip_events]) + assert all(l["scope_distance"] == 2 and "distance-2" in l["tags"] for l in ip_events) ip_events = [l for l in lines if l["type"] == "IP_ADDRESS" and l["data"] == "1.0.0.1"] assert ip_events - assert all([l["scope_distance"] == 2 and "distance-2" in l["tags"] for l in ip_events]) + assert all(l["scope_distance"] == 2 and "distance-2" in l["tags"] for l in ip_events) @pytest.mark.asyncio @@ -97,7 +99,7 @@ async def test_cli_scan(monkeypatch): ["bbot", "-y", "-t", "127.0.0.1", "www.example.com", "-n", "test_cli_scan", "-c", "dns.disable=true"], ) result = await cli._main() - assert result == True + assert result is True scan_home = scans_home / "test_cli_scan" assert (scan_home / "preset.yml").is_file(), "preset.yml not found" @@ -139,22 +141,48 @@ async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): monkeypatch.setattr("sys.argv", ["bbot", "--version"]) result = await cli._main() out, err = capsys.readouterr() - assert result == None + assert result is None assert len(out.splitlines()) == 1 assert out.count(".") > 1 + # deps behavior + monkeypatch.setattr("sys.argv", ["bbot", "-n", "depstest", "--retry-deps", "--current-preset"]) + result = await cli._main() + assert result is None + out, err = capsys.readouterr() + print(out) + # parse YAML output + preset = yaml.safe_load(out) + assert preset == { + "description": "depstest", + "scan_name": "depstest", + "config": {"deps": {"behavior": "retry_failed"}}, + } + # list modules monkeypatch.setattr("sys.argv", ["bbot", "--list-modules"]) result = await cli._main() - assert result == None + assert result is None out, err = capsys.readouterr() # internal modules assert "| excavate " in out - # output modules - assert "| csv " in out + # no output modules + assert not "| csv " in out # scan modules assert "| wayback " in out + # list output modules + monkeypatch.setattr("sys.argv", ["bbot", "--list-output-modules"]) + result = await cli._main() + assert result == None + out, err = capsys.readouterr() + # no internal modules + assert not "| excavate " in out + # output modules + assert "| csv " in out + # no scan modules + assert not "| wayback " in out + # output dir and scan name output_dir = bbot_test_dir / "bbot_cli_args_output" scan_name = "bbot_cli_args_scan_name" @@ -162,7 +190,7 @@ async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): assert not output_dir.exists() monkeypatch.setattr("sys.argv", ["bbot", "-o", str(output_dir), "-n", scan_name, "-y"]) result = await cli._main() - assert result == True + assert result is True assert output_dir.is_dir() assert scan_dir.is_dir() assert "[SCAN]" in open(scan_dir / "output.txt").read() @@ -173,7 +201,7 @@ async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): monkeypatch.setattr("sys.argv", ["bbot", "--list-module-options"]) result = await cli._main() out, err = capsys.readouterr() - assert result == None + assert result is None assert "| modules.wayback.urls" in out assert "| bool" in out assert "| emit URLs in addition to DNS_NAMEs" in out @@ -185,36 +213,36 @@ async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): monkeypatch.setattr("sys.argv", ["bbot", "-f", "subdomain-enum", "--list-module-options"]) result = await cli._main() out, err = capsys.readouterr() - assert result == None + assert result is None assert "| modules.wayback.urls" in out assert "| bool" in out assert "| emit URLs in addition to DNS_NAMEs" in out assert "| False" in out assert "| modules.dnsbrute.wordlist" in out - assert not "| modules.robots.include_allow" in out + assert "| modules.robots.include_allow" not in out # list module options by module monkeypatch.setattr("sys.argv", ["bbot", "-m", "dnsbrute", "-lmo"]) result = await cli._main() out, err = capsys.readouterr() - assert result == None + assert result is None assert out.count("modules.") == out.count("modules.dnsbrute.") - assert not "| modules.wayback.urls" in out + assert "| modules.wayback.urls" not in out assert "| modules.dnsbrute.wordlist" in out - assert not "| modules.robots.include_allow" in out + assert "| modules.robots.include_allow" not in out # list output module options by module monkeypatch.setattr("sys.argv", ["bbot", "-om", "stdout", "-lmo"]) result = await cli._main() out, err = capsys.readouterr() - assert result == None + assert result is None assert out.count("modules.") == out.count("modules.stdout.") # list flags monkeypatch.setattr("sys.argv", ["bbot", "--list-flags"]) result = await cli._main() out, err = capsys.readouterr() - assert result == None + assert result is None assert "| safe " in out assert "| Non-intrusive, safe to run " in out assert "| active " in out @@ -224,32 +252,32 @@ async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): monkeypatch.setattr("sys.argv", ["bbot", "-f", "active", "--list-flags"]) result = await cli._main() out, err = capsys.readouterr() - assert result == None - assert not "| safe " in out + assert result is None + assert "| safe " not in out assert "| active " in out - assert not "| passive " in out + assert "| passive " not in out # list multiple flags monkeypatch.setattr("sys.argv", ["bbot", "-f", "active", "safe", "--list-flags"]) result = await cli._main() out, err = capsys.readouterr() - assert result == None + assert result is None assert "| safe " in out assert "| active " in out - assert not "| passive " in out + assert "| passive " not in out # no args monkeypatch.setattr("sys.argv", ["bbot"]) result = await cli._main() out, err = capsys.readouterr() - assert result == None + assert result is None assert "Target:\n -t TARGET [TARGET ...]" in out # list modules monkeypatch.setattr("sys.argv", ["bbot", "-l"]) result = await cli._main() out, err = capsys.readouterr() - assert result == None + assert result is None assert "| dnsbrute " in out assert "| httpx " in out assert "| robots " in out @@ -258,33 +286,33 @@ async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): monkeypatch.setattr("sys.argv", ["bbot", "-f", "subdomain-enum", "-l"]) result = await cli._main() out, err = capsys.readouterr() - assert result == None + assert result is None assert "| dnsbrute " in out assert "| httpx " in out - assert not "| robots " in out + assert "| robots " not in out # list modules by flag + required flag monkeypatch.setattr("sys.argv", ["bbot", "-f", "subdomain-enum", "-rf", "passive", "-l"]) result = await cli._main() out, err = capsys.readouterr() - assert result == None + assert result is None assert "| chaos " in out - assert not "| httpx " in out + assert "| httpx " not in out # list modules by flag + excluded flag monkeypatch.setattr("sys.argv", ["bbot", "-f", "subdomain-enum", "-ef", "active", "-l"]) result = await cli._main() out, err = capsys.readouterr() - assert result == None + assert result is None assert "| chaos " in out - assert not "| httpx " in out + assert "| httpx " not in out # list modules by flag + excluded module monkeypatch.setattr("sys.argv", ["bbot", "-f", "subdomain-enum", "-em", "dnsbrute", "-l"]) result = await cli._main() out, err = capsys.readouterr() - assert result == None - assert not "| dnsbrute " in out + assert result is None + assert "| dnsbrute " not in out assert "| httpx " in out # output modules override @@ -292,12 +320,12 @@ async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): assert not caplog.text monkeypatch.setattr("sys.argv", ["bbot", "-om", "csv,json", "-y"]) result = await cli._main() - assert result == True + assert result is True assert "Loaded 2/2 output modules, (csv,json)" in caplog.text caplog.clear() monkeypatch.setattr("sys.argv", ["bbot", "-em", "csv,json", "-y"]) result = await cli._main() - assert result == True + assert result is True assert "Loaded 3/3 output modules, (python,stdout,txt)" in caplog.text # output modules override @@ -305,7 +333,7 @@ async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): assert not caplog.text monkeypatch.setattr("sys.argv", ["bbot", "-om", "subdomains", "-y"]) result = await cli._main() - assert result == True + assert result is True assert "Loaded 6/6 output modules, (csv,json,python,stdout,subdomains,txt)" in caplog.text # internal modules override @@ -313,17 +341,17 @@ async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): assert not caplog.text monkeypatch.setattr("sys.argv", ["bbot", "-y"]) result = await cli._main() - assert result == True + assert result is True assert "Loaded 5/5 internal modules (aggregate,cloudcheck,dnsresolve,excavate,speculate)" in caplog.text caplog.clear() monkeypatch.setattr("sys.argv", ["bbot", "-em", "excavate", "speculate", "-y"]) result = await cli._main() - assert result == True + assert result is True assert "Loaded 3/3 internal modules (aggregate,cloudcheck,dnsresolve)" in caplog.text caplog.clear() monkeypatch.setattr("sys.argv", ["bbot", "-c", "speculate=false", "-y"]) result = await cli._main() - assert result == True + assert result is True assert "Loaded 4/4 internal modules (aggregate,cloudcheck,dnsresolve,excavate)" in caplog.text # custom target type @@ -331,7 +359,7 @@ async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): monkeypatch.setattr("sys.argv", ["bbot", "-t", "ORG:evilcorp", "-y"]) result = await cli._main() out, err = capsys.readouterr() - assert result == True + assert result is True assert "[ORG_STUB] evilcorp TARGET" in out # activate modules by flag @@ -339,64 +367,63 @@ async def test_cli_args(monkeypatch, caplog, capsys, clean_default_config): assert not caplog.text monkeypatch.setattr("sys.argv", ["bbot", "-f", "passive"]) result = await cli._main() - assert result == True + assert result is True # unconsoleable output module monkeypatch.setattr("sys.argv", ["bbot", "-om", "web_report"]) result = await cli._main() - assert result == True + assert result is True # unresolved dependency monkeypatch.setattr("sys.argv", ["bbot", "-m", "wappalyzer"]) result = await cli._main() - assert result == True + assert result is True # require flags monkeypatch.setattr("sys.argv", ["bbot", "-f", "active", "-rf", "passive"]) result = await cli._main() - assert result == True + assert result is True # excluded flags monkeypatch.setattr("sys.argv", ["bbot", "-f", "active", "-ef", "active"]) result = await cli._main() - assert result == True + assert result is True # slow modules monkeypatch.setattr("sys.argv", ["bbot", "-m", "bucket_digitalocean"]) result = await cli._main() - assert result == True + assert result is True # deadly modules caplog.clear() assert not caplog.text monkeypatch.setattr("sys.argv", ["bbot", "-m", "nuclei"]) result = await cli._main() - assert result == False, "-m nuclei ran without --allow-deadly" + assert result is False, "-m nuclei ran without --allow-deadly" assert "Please specify --allow-deadly to continue" in caplog.text # --allow-deadly monkeypatch.setattr("sys.argv", ["bbot", "-m", "nuclei", "--allow-deadly"]) result = await cli._main() - assert result == True, "-m nuclei failed to run with --allow-deadly" + assert result is True, "-m nuclei failed to run with --allow-deadly" # install all deps monkeypatch.setattr("sys.argv", ["bbot", "--install-all-deps"]) success = await cli._main() - assert success == True, "--install-all-deps failed for at least one module" + assert success is True, "--install-all-deps failed for at least one module" @pytest.mark.asyncio async def test_cli_customheaders(monkeypatch, caplog, capsys): monkeypatch.setattr(sys, "exit", lambda *args, **kwargs: True) monkeypatch.setattr(os, "_exit", lambda *args, **kwargs: True) - import yaml # test custom headers monkeypatch.setattr( "sys.argv", ["bbot", "--custom-headers", "foo=bar", "foo2=bar2", "foo3=bar=3", "--current-preset"] ) success = await cli._main() - assert success == None, "setting custom headers on command line failed" + assert success is None, "setting custom headers on command line failed" captured = capsys.readouterr() stdout_preset = yaml.safe_load(captured.out) assert stdout_preset["config"]["web"]["http_headers"] == {"foo": "bar", "foo2": "bar2", "foo3": "bar=3"} @@ -404,21 +431,21 @@ async def test_cli_customheaders(monkeypatch, caplog, capsys): # test custom headers invalid (no "=") monkeypatch.setattr("sys.argv", ["bbot", "--custom-headers", "justastring", "--current-preset"]) result = await cli._main() - assert result == None + assert result is None assert "Custom headers not formatted correctly (missing '=')" in caplog.text caplog.clear() # test custom headers invalid (missing key) monkeypatch.setattr("sys.argv", ["bbot", "--custom-headers", "=nokey", "--current-preset"]) result = await cli._main() - assert result == None + assert result is None assert "Custom headers not formatted correctly (missing header name or value)" in caplog.text caplog.clear() # test custom headers invalid (missing value) monkeypatch.setattr("sys.argv", ["bbot", "--custom-headers", "missingvalue=", "--current-preset"]) result = await cli._main() - assert result == None + assert result is None assert "Custom headers not formatted correctly (missing header name or value)" in caplog.text @@ -535,6 +562,13 @@ def test_cli_module_validation(monkeypatch, caplog): ] ) + # bad target + caplog.clear() + assert not caplog.text + monkeypatch.setattr("sys.argv", ["bbot", "-t", "asdf:::sdf"]) + cli.main() + assert 'Unable to autodetect event type from "asdf:::sdf"' in caplog.text + # incorrect flag caplog.clear() assert not caplog.text @@ -626,6 +660,35 @@ def test_cli_presets(monkeypatch, capsys, caplog): stdout_preset = yaml.safe_load(captured.out) assert stdout_preset["config"]["web"]["http_proxy"] == "http://proxy2" + # --fast-mode + monkeypatch.setattr("sys.argv", ["bbot", "--current-preset"]) + cli.main() + captured = capsys.readouterr() + stdout_preset = yaml.safe_load(captured.out) + assert list(stdout_preset) == ["description"] + + monkeypatch.setattr("sys.argv", ["bbot", "--fast", "--current-preset"]) + cli.main() + captured = capsys.readouterr() + stdout_preset = yaml.safe_load(captured.out) + stdout_preset.pop("description") + assert stdout_preset == { + "config": { + "scope": {"strict": True}, + "dns": {"minimal": True}, + "modules": {"speculate": {"essential_only": True}}, + }, + "exclude_modules": ["excavate"], + } + + # --proxy + monkeypatch.setattr("sys.argv", ["bbot", "--proxy", "http://127.0.0.1:8080", "--current-preset"]) + cli.main() + captured = capsys.readouterr() + stdout_preset = yaml.safe_load(captured.out) + stdout_preset.pop("description") + assert stdout_preset == {"config": {"web": {"http_proxy": "http://127.0.0.1:8080"}}} + # cli config overrides all presets monkeypatch.setattr( "sys.argv", diff --git a/bbot/test/test_step_1/test_dns.py b/bbot/test/test_step_1/test_dns.py index 4829125a4c..c032b44e48 100644 --- a/bbot/test/test_step_1/test_dns.py +++ b/bbot/test/test_step_1/test_dns.py @@ -23,7 +23,7 @@ async def test_dns_engine(bbot_scanner): ) result = await scan.helpers.resolve("one.one.one.one") assert "1.1.1.1" in result - assert not "2606:4700:4700::1111" in result + assert "2606:4700:4700::1111" not in result results = [_ async for _ in scan.helpers.resolve_batch(("one.one.one.one", "1.1.1.1"))] pass_1 = False @@ -85,12 +85,12 @@ async def test_dns_resolution(bbot_scanner): for answer in answers: responses += list(extract_targets(answer)) assert ("A", "1.1.1.1") in responses - assert not ("AAAA", "2606:4700:4700::1111") in responses + assert ("AAAA", "2606:4700:4700::1111") not in responses answers, errors = await dnsengine.resolve_raw("one.one.one.one", type="AAAA") responses = [] for answer in answers: responses += list(extract_targets(answer)) - assert not ("A", "1.1.1.1") in responses + assert ("A", "1.1.1.1") not in responses assert ("AAAA", "2606:4700:4700::1111") in responses answers, errors = await dnsengine.resolve_raw("1.1.1.1") responses = [] @@ -106,13 +106,14 @@ async def test_dns_resolution(bbot_scanner): assert "2606:4700:4700::1111" in await dnsengine.resolve("one.one.one.one", type="AAAA") assert "one.one.one.one" in await dnsengine.resolve("1.1.1.1") for rdtype in ("NS", "SOA", "MX", "TXT"): - assert len(await dnsengine.resolve("google.com", type=rdtype)) > 0 + results = await dnsengine.resolve("google.com", type=rdtype) + assert len(results) > 0 # batch resolution batch_results = [r async for r in dnsengine.resolve_batch(["1.1.1.1", "one.one.one.one"])] assert len(batch_results) == 2 batch_results = dict(batch_results) - assert any([x in batch_results["one.one.one.one"] for x in ("1.1.1.1", "1.0.0.1")]) + assert any(x in batch_results["one.one.one.one"] for x in ("1.1.1.1", "1.0.0.1")) assert "one.one.one.one" in batch_results["1.1.1.1"] # custom batch resolution @@ -140,11 +141,11 @@ async def test_dns_resolution(bbot_scanner): assert hash(("1.1.1.1", "PTR")) in dnsengine._dns_cache await dnsengine.resolve("one.one.one.one", type="A") assert hash(("one.one.one.one", "A")) in dnsengine._dns_cache - assert not hash(("one.one.one.one", "AAAA")) in dnsengine._dns_cache + assert hash(("one.one.one.one", "AAAA")) not in dnsengine._dns_cache dnsengine._dns_cache.clear() await dnsengine.resolve("one.one.one.one", type="AAAA") assert hash(("one.one.one.one", "AAAA")) in dnsengine._dns_cache - assert not hash(("one.one.one.one", "A")) in dnsengine._dns_cache + assert hash(("one.one.one.one", "A")) not in dnsengine._dns_cache await dnsengine._shutdown() @@ -164,7 +165,7 @@ async def test_dns_resolution(bbot_scanner): assert "A" in resolved_hosts_event1.raw_dns_records assert "AAAA" in resolved_hosts_event1.raw_dns_records assert "a-record" in resolved_hosts_event1.tags - assert not "a-record" in resolved_hosts_event2.tags + assert "a-record" not in resolved_hosts_event2.tags scan2 = bbot_scanner("evilcorp.com", config={"dns": {"minimal": False}}) await scan2.helpers.dns._mock_dns( @@ -184,7 +185,6 @@ async def test_dns_resolution(bbot_scanner): @pytest.mark.asyncio async def test_wildcards(bbot_scanner): - scan = bbot_scanner("1.1.1.1") helpers = scan.helpers @@ -197,7 +197,7 @@ async def test_wildcards(bbot_scanner): assert len(dnsengine._wildcard_cache) == len(all_rdtypes) + (len(all_rdtypes) - 2) for rdtype in all_rdtypes: assert hash(("github.io", rdtype)) in dnsengine._wildcard_cache - if not rdtype in ("A", "AAAA"): + if rdtype not in ("A", "AAAA"): assert hash(("asdf.github.io", rdtype)) in dnsengine._wildcard_cache assert "github.io" in wildcard_domains assert "A" in wildcard_domains["github.io"] @@ -632,8 +632,42 @@ def custom_lookup(query, rdtype): @pytest.mark.asyncio -async def test_dns_raw_records(bbot_scanner): +async def test_wildcard_deduplication(bbot_scanner): + custom_lookup = """ +def custom_lookup(query, rdtype): + if rdtype == "TXT" and query.strip(".").endswith("evilcorp.com"): + return {""} +""" + mock_data = { + "evilcorp.com": {"A": ["127.0.0.1"]}, + } + + from bbot.modules.base import BaseModule + + class DummyModule(BaseModule): + watched_events = ["DNS_NAME"] + per_domain_only = True + + async def handle_event(self, event): + for i in range(30): + await self.emit_event(f"www{i}.evilcorp.com", "DNS_NAME", parent=event) + + # scan without omitted event type + scan = bbot_scanner( + "evilcorp.com", config={"dns": {"minimal": False, "wildcard_ignore": []}, "omit_event_types": []} + ) + await scan.helpers.dns._mock_dns(mock_data, custom_lookup_fn=custom_lookup) + dummy_module = DummyModule(scan) + scan.modules["dummy_module"] = dummy_module + events = [e async for e in scan.async_start()] + dns_name_events = [e for e in events if e.type == "DNS_NAME"] + assert len(dns_name_events) == 2 + assert 1 == len([e for e in dns_name_events if e.data == "_wildcard.evilcorp.com"]) + + +@pytest.mark.asyncio +async def test_dns_raw_records(bbot_scanner): from bbot.modules.base import BaseModule class DummyModule(BaseModule): @@ -696,7 +730,7 @@ async def handle_event(self, event): dummy_module = DummyModule(scan) scan.modules["dummy_module"] = dummy_module events = [e async for e in scan.async_start()] - # no raw records should be ouptut + # no raw records should be output assert 0 == len([e for e in events if e.type == "RAW_DNS_RECORD"]) # but they should still make it to the module assert 1 == len( @@ -744,16 +778,16 @@ async def test_dns_graph_structure(bbot_scanner): @pytest.mark.asyncio async def test_dns_helpers(bbot_scanner): - assert service_record("") == False - assert service_record("localhost") == False - assert service_record("www.example.com") == False - assert service_record("www.example.com", "SRV") == True - assert service_record("_custom._service.example.com", "SRV") == True - assert service_record("_custom._service.example.com", "A") == False + assert service_record("") is False + assert service_record("localhost") is False + assert service_record("www.example.com") is False + assert service_record("www.example.com", "SRV") is True + assert service_record("_custom._service.example.com", "SRV") is True + assert service_record("_custom._service.example.com", "A") is False # top 100 most common SRV records for srv_record in common_srvs[:100]: hostname = f"{srv_record}.example.com" - assert service_record(hostname) == True + assert service_record(hostname) is True # make sure system nameservers are excluded from use by DNS brute force brute_nameservers = tempwordlist(["1.2.3.4", "8.8.4.4", "4.3.2.1", "8.8.8.8"]) diff --git a/bbot/test/test_step_1/test_engine.py b/bbot/test/test_step_1/test_engine.py index 1b44049c9b..653c3dcd6c 100644 --- a/bbot/test/test_step_1/test_engine.py +++ b/bbot/test/test_step_1/test_engine.py @@ -14,7 +14,6 @@ async def test_engine(): return_errored = False class TestEngineServer(EngineServer): - CMDS = { 0: "return_thing", 1: "yield_stuff", @@ -54,7 +53,6 @@ async def yield_stuff(self, n): raise class TestEngineClient(EngineClient): - SERVER_CLASS = TestEngineServer async def return_thing(self, n): @@ -72,7 +70,7 @@ async def yield_stuff(self, n): # test async generator assert counter == 0 - assert yield_cancelled == False + assert yield_cancelled is False yield_res = [r async for r in test_engine.yield_stuff(13)] assert yield_res == [f"thing{i}" for i in range(13)] assert len(yield_res) == 13 @@ -88,8 +86,8 @@ async def yield_stuff(self, n): await agen.aclose() break await asyncio.sleep(5) - assert yield_cancelled == True - assert yield_errored == False + assert yield_cancelled is True + assert yield_errored is False assert counter < 15 # test async generator with error @@ -99,8 +97,8 @@ async def yield_stuff(self, n): with pytest.raises(BBOTEngineError): async for _ in agen: pass - assert yield_cancelled == False - assert yield_errored == True + assert yield_cancelled is False + assert yield_errored is True # test return with cancellation return_started = False @@ -113,10 +111,10 @@ async def yield_stuff(self, n): with pytest.raises(asyncio.CancelledError): await task await asyncio.sleep(0.1) - assert return_started == True - assert return_finished == False - assert return_cancelled == True - assert return_errored == False + assert return_started is True + assert return_finished is False + assert return_cancelled is True + assert return_errored is False # test return with late cancellation return_started = False @@ -128,10 +126,10 @@ async def yield_stuff(self, n): task.cancel() result = await task assert result == "thing1" - assert return_started == True - assert return_finished == True - assert return_cancelled == False - assert return_errored == False + assert return_started is True + assert return_finished is True + assert return_cancelled is False + assert return_errored is False # test return with error return_started = False @@ -140,9 +138,9 @@ async def yield_stuff(self, n): return_errored = False with pytest.raises(BBOTEngineError): result = await test_engine.return_thing(None) - assert return_started == True - assert return_finished == False - assert return_cancelled == False - assert return_errored == True + assert return_started is True + assert return_finished is False + assert return_cancelled is False + assert return_errored is True await test_engine.shutdown() diff --git a/bbot/test/test_step_1/test_events.py b/bbot/test/test_step_1/test_events.py index 1ebb38fea3..a13bae9454 100644 --- a/bbot/test/test_step_1/test_events.py +++ b/bbot/test/test_step_1/test_events.py @@ -9,7 +9,6 @@ @pytest.mark.asyncio async def test_events(events, helpers): - scan = Scanner() await scan._prep() @@ -42,6 +41,7 @@ async def test_events(events, helpers): # ip tests assert events.ipv4 == scan.make_event("8.8.8.8", dummy=True) assert "8.8.8.8" in events.ipv4 + assert events.ipv4.host_filterable == "8.8.8.8" assert "8.8.8.8" == events.ipv4 assert "8.8.8.8" in events.netv4 assert "8.8.8.9" not in events.ipv4 @@ -59,11 +59,19 @@ async def test_events(events, helpers): assert events.emoji not in events.ipv4 assert events.emoji not in events.netv6 assert events.netv6 not in events.emoji - assert "dead::c0de" == scan.make_event(" [DEaD::c0De]:88", "DNS_NAME", dummy=True) + ipv6_event = scan.make_event(" [DEaD::c0De]:88", "DNS_NAME", dummy=True) + assert "dead::c0de" == ipv6_event + assert ipv6_event.host_filterable == "dead::c0de" + range_to_ip = scan.make_event("1.2.3.4/32", dummy=True) + assert range_to_ip.type == "IP_ADDRESS" + range_to_ip = scan.make_event("dead::beef/128", dummy=True) + assert range_to_ip.type == "IP_ADDRESS" # hostname tests assert events.domain.host == "publicapis.org" + assert events.domain.host_filterable == "publicapis.org" assert events.subdomain.host == "api.publicapis.org" + assert events.subdomain.host_filterable == "api.publicapis.org" assert events.domain.host_stem == "publicapis" assert events.subdomain.host_stem == "api.publicapis" assert "api.publicapis.org" in events.domain @@ -72,8 +80,8 @@ async def test_events(events, helpers): assert "fsocie.ty" not in events.subdomain assert events.subdomain in events.domain assert events.domain not in events.subdomain - assert not events.ipv4 in events.domain - assert not events.netv6 in events.domain + assert events.ipv4 not in events.domain + assert events.netv6 not in events.domain assert events.emoji not in events.domain assert events.domain not in events.emoji open_port_event = scan.make_event(" eViLcorp.COM.:88", "DNS_NAME", dummy=True) @@ -86,7 +94,11 @@ async def test_events(events, helpers): assert "port" not in e.json() # url tests - assert scan.make_event("http://evilcorp.com", dummy=True) == scan.make_event("http://evilcorp.com/", dummy=True) + url_no_trailing_slash = scan.make_event("http://evilcorp.com", dummy=True) + url_trailing_slash = scan.make_event("http://evilcorp.com/", dummy=True) + assert url_no_trailing_slash == url_trailing_slash + assert url_no_trailing_slash.host_filterable == "http://evilcorp.com/" + assert url_trailing_slash.host_filterable == "http://evilcorp.com/" assert events.url_unverified.host == "api.publicapis.org" assert events.url_unverified in events.domain assert events.url_unverified in events.subdomain @@ -129,6 +141,7 @@ async def test_events(events, helpers): assert events.http_response.port == 80 assert events.http_response.parsed_url.scheme == "http" assert events.http_response.with_port().geturl() == "http://example.com:80/" + assert events.http_response.host_filterable == "http://example.com/" http_response = scan.make_event( { @@ -136,6 +149,7 @@ async def test_events(events, helpers): "title": "HTTP%20RESPONSE", "url": "http://www.evilcorp.com:80", "input": "http://www.evilcorp.com:80", + "raw_header": "HTTP/1.1 301 Moved Permanently\r\nLocation: http://www.evilcorp.com/asdf\r\n\r\n", "location": "/asdf", "status_code": 301, }, @@ -148,7 +162,13 @@ async def test_events(events, helpers): # http response url validation http_response_2 = scan.make_event( - {"port": "80", "url": "http://evilcorp.com:80/asdf"}, "HTTP_RESPONSE", dummy=True + { + "port": "80", + "url": "http://evilcorp.com:80/asdf", + "raw_header": "HTTP/1.1 301 Moved Permanently\r\nLocation: http://www.evilcorp.com/asdf\r\n\r\n", + }, + "HTTP_RESPONSE", + dummy=True, ) assert http_response_2.data["url"] == "http://evilcorp.com/asdf" @@ -193,7 +213,7 @@ async def test_events(events, helpers): # scope distance event1 = scan.make_event("1.2.3.4", dummy=True) - assert event1._scope_distance == None + assert event1._scope_distance is None event1.scope_distance = 0 assert event1._scope_distance == 0 event2 = scan.make_event("2.3.4.5", parent=event1) @@ -214,7 +234,7 @@ async def test_events(events, helpers): org_stub_1 = scan.make_event("STUB1", "ORG_STUB", parent=scan.root_event) org_stub_1.scope_distance == 1 - assert org_stub_1.netloc == None + assert org_stub_1.netloc is None assert "netloc" not in org_stub_1.json() org_stub_2 = scan.make_event("STUB2", "ORG_STUB", parent=org_stub_1) org_stub_2.scope_distance == 2 @@ -223,7 +243,7 @@ async def test_events(events, helpers): root_event = scan.make_event("0.0.0.0", dummy=True) root_event.scope_distance = 0 internal_event1 = scan.make_event("1.2.3.4", parent=root_event, internal=True) - assert internal_event1._internal == True + assert internal_event1._internal is True assert "internal" in internal_event1.tags # tag inheritance @@ -255,8 +275,8 @@ async def test_events(events, helpers): # updating module event3 = scan.make_event("127.0.0.1", parent=scan.root_event) updated_event = scan.make_event(event3, internal=True) - assert event3.internal == False - assert updated_event.internal == True + assert event3.internal is False + assert updated_event.internal is True # event sorting parent1 = scan.make_event("127.0.0.1", parent=scan.root_event) @@ -476,7 +496,7 @@ async def test_events(events, helpers): assert db_event.discovery_context == "test context" assert db_event.discovery_path == ["test context"] assert len(db_event.parent_chain) == 1 - assert all([event_uuid_regex.match(u) for u in db_event.parent_chain]) + assert all(event_uuid_regex.match(u) for u in db_event.parent_chain) assert db_event.parent_chain[0] == str(db_event.uuid) assert db_event.parent.uuid == scan.root_event.uuid assert db_event.parent_uuid == scan.root_event.uuid @@ -484,7 +504,6 @@ async def test_events(events, helpers): json_event = db_event.json() assert isinstance(json_event["uuid"], str) assert json_event["uuid"] == str(db_event.uuid) - print(f"{json_event} / {db_event.uuid} / {db_event.parent_uuid} / {scan.root_event.uuid}") assert json_event["parent_uuid"] == str(scan.root_event.uuid) assert json_event["scope_distance"] == 1 assert json_event["data"] == "evilcorp.com:80" @@ -514,7 +533,7 @@ async def test_events(events, helpers): hostless_event_json = hostless_event.json() assert hostless_event_json["type"] == "ASDF" assert hostless_event_json["data"] == "asdf" - assert not "host" in hostless_event_json + assert "host" not in hostless_event_json # SIEM-friendly serialize/deserialize json_event_siemfriendly = db_event.json(siem_friendly=True) @@ -534,6 +553,10 @@ async def test_events(events, helpers): http_response = scan.make_event(httpx_response, "HTTP_RESPONSE", parent=scan.root_event) assert http_response.parent_id == scan.root_event.id assert http_response.data["input"] == "http://example.com:80" + assert ( + http_response.raw_response + == 'HTTP/1.1 200 OK\r\nConnection: close\r\nAge: 526111\r\nCache-Control: max-age=604800\r\nContent-Type: text/html; charset=UTF-8\r\nDate: Mon, 14 Nov 2022 17:14:27 GMT\r\nEtag: "3147526947+ident+gzip"\r\nExpires: Mon, 21 Nov 2022 17:14:27 GMT\r\nLast-Modified: Thu, 17 Oct 2019 07:18:26 GMT\r\nServer: ECS (agb/A445)\r\nVary: Accept-Encoding\r\nX-Cache: HIT\r\n\r\n\n\n\n Example Domain\n\n \n \n \n \n\n\n\n\n\n\n' + ) json_event = http_response.json(mode="graph") assert isinstance(json_event["data"], str) json_event = http_response.json() @@ -599,12 +622,15 @@ async def test_events(events, helpers): assert str(parent_event_3.module) == "mymodule" assert str(parent_event_3.module_sequence) == "mymodule->mymodule->mymodule" + # event with no data + with pytest.raises(ValidationError): + event = scan.make_event(None, "DNS_NAME", parent=scan.root_event) + await scan._cleanup() @pytest.mark.asyncio async def test_event_discovery_context(): - from bbot.modules.base import BaseModule scan = Scanner("evilcorp.com") @@ -792,7 +818,7 @@ async def test_event_web_spider_distance(bbot_scanner): ) assert url_event_3.web_spider_distance == 1 assert "spider-danger" in url_event_3.tags - assert not "spider-max" in url_event_3.tags + assert "spider-max" not in url_event_3.tags social_event = scan.make_event( {"platform": "github", "url": "http://www.evilcorp.com/test4"}, "SOCIAL", parent=url_event_3 ) @@ -815,42 +841,42 @@ async def test_event_web_spider_distance(bbot_scanner): url_event = scan.make_event("http://www.evilcorp.com", "URL_UNVERIFIED", parent=scan.root_event) assert url_event.web_spider_distance == 0 - assert not "spider-danger" in url_event.tags - assert not "spider-max" in url_event.tags + assert "spider-danger" not in url_event.tags + assert "spider-max" not in url_event.tags url_event_2 = scan.make_event( "http://www.evilcorp.com", "URL_UNVERIFIED", parent=scan.root_event, tags="spider-danger" ) # spider distance shouldn't increment because it's not the same host assert url_event_2.web_spider_distance == 0 assert "spider-danger" in url_event_2.tags - assert not "spider-max" in url_event_2.tags + assert "spider-max" not in url_event_2.tags url_event_3 = scan.make_event( "http://www.evilcorp.com/3", "URL_UNVERIFIED", parent=url_event_2, tags="spider-danger" ) assert url_event_3.web_spider_distance == 1 assert "spider-danger" in url_event_3.tags - assert not "spider-max" in url_event_3.tags + assert "spider-max" not in url_event_3.tags url_event_4 = scan.make_event("http://evilcorp.com", "URL_UNVERIFIED", parent=url_event_3) assert url_event_4.web_spider_distance == 0 - assert not "spider-danger" in url_event_4.tags - assert not "spider-max" in url_event_4.tags + assert "spider-danger" not in url_event_4.tags + assert "spider-max" not in url_event_4.tags url_event_4.add_tag("spider-danger") assert url_event_4.web_spider_distance == 0 assert "spider-danger" in url_event_4.tags - assert not "spider-max" in url_event_4.tags + assert "spider-max" not in url_event_4.tags url_event_4.remove_tag("spider-danger") assert url_event_4.web_spider_distance == 0 - assert not "spider-danger" in url_event_4.tags - assert not "spider-max" in url_event_4.tags + assert "spider-danger" not in url_event_4.tags + assert "spider-max" not in url_event_4.tags url_event_5 = scan.make_event("http://evilcorp.com/5", "URL_UNVERIFIED", parent=url_event_4) assert url_event_5.web_spider_distance == 0 - assert not "spider-danger" in url_event_5.tags - assert not "spider-max" in url_event_5.tags + assert "spider-danger" not in url_event_5.tags + assert "spider-max" not in url_event_5.tags url_event_5.add_tag("spider-danger") # if host is the same as parent, web spider distance should auto-increment after adding spider-danger tag assert url_event_5.web_spider_distance == 1 assert "spider-danger" in url_event_5.tags - assert not "spider-max" in url_event_5.tags + assert "spider-max" not in url_event_5.tags def test_event_confidence(): @@ -895,7 +921,12 @@ def test_event_closest_host(): assert event1.host == "evilcorp.com" # second event has a host + url event2 = scan.make_event( - {"method": "GET", "url": "http://www.evilcorp.com/asdf", "hash": {"header_mmh3": "1", "body_mmh3": "2"}}, + { + "method": "GET", + "url": "http://www.evilcorp.com/asdf", + "hash": {"header_mmh3": "1", "body_mmh3": "2"}, + "raw_header": "HTTP/1.1 301 Moved Permanently\r\nLocation: http://www.evilcorp.com/asdf\r\n\r\n", + }, "HTTP_RESPONSE", parent=event1, ) @@ -966,3 +997,74 @@ def test_event_magic(): assert event.tags == {"folder"} zip_file.unlink() + + +@pytest.mark.asyncio +async def test_mobile_app(): + scan = Scanner() + with pytest.raises(ValidationError): + scan.make_event("com.evilcorp.app", "MOBILE_APP", parent=scan.root_event) + with pytest.raises(ValidationError): + scan.make_event({"id": "com.evilcorp.app"}, "MOBILE_APP", parent=scan.root_event) + with pytest.raises(ValidationError): + scan.make_event({"url": "https://play.google.com/store/apps/details"}, "MOBILE_APP", parent=scan.root_event) + mobile_app = scan.make_event( + {"url": "https://play.google.com/store/apps/details?id=com.evilcorp.app"}, "MOBILE_APP", parent=scan.root_event + ) + assert sorted(mobile_app.data.items()) == [ + ("id", "com.evilcorp.app"), + ("url", "https://play.google.com/store/apps/details?id=com.evilcorp.app"), + ] + + scan = Scanner("MOBILE_APP:https://play.google.com/store/apps/details?id=com.evilcorp.app") + events = [e async for e in scan.async_start()] + assert len(events) == 3 + mobile_app_event = [e for e in events if e.type == "MOBILE_APP"][0] + assert mobile_app_event.type == "MOBILE_APP" + assert sorted(mobile_app_event.data.items()) == [ + ("id", "com.evilcorp.app"), + ("url", "https://play.google.com/store/apps/details?id=com.evilcorp.app"), + ] + + +@pytest.mark.asyncio +async def test_filesystem(): + scan = Scanner("FILESYSTEM:/tmp/asdf") + events = [e async for e in scan.async_start()] + assert len(events) == 3 + filesystem_events = [e for e in events if e.type == "FILESYSTEM"] + assert len(filesystem_events) == 1 + assert filesystem_events[0].type == "FILESYSTEM" + assert filesystem_events[0].data == {"path": "/tmp/asdf"} + + +def test_event_hashing(): + scan = Scanner("example.com") + url_event = scan.make_event("https://api.example.com/", "URL_UNVERIFIED", parent=scan.root_event) + host_event_1 = scan.make_event("www.example.com", "DNS_NAME", parent=url_event) + host_event_2 = scan.make_event("test.example.com", "DNS_NAME", parent=url_event) + finding_data = {"description": "Custom Yara Rule [find_string] Matched via identifier [str1]"} + finding1 = scan.make_event(finding_data, "FINDING", parent=host_event_1) + finding2 = scan.make_event(finding_data, "FINDING", parent=host_event_2) + finding3 = scan.make_event(finding_data, "FINDING", parent=host_event_2) + + assert finding1.data == { + "description": "Custom Yara Rule [find_string] Matched via identifier [str1]", + "host": "www.example.com", + } + assert finding2.data == { + "description": "Custom Yara Rule [find_string] Matched via identifier [str1]", + "host": "test.example.com", + } + assert finding3.data == { + "description": "Custom Yara Rule [find_string] Matched via identifier [str1]", + "host": "test.example.com", + } + assert finding1.id != finding2.id + assert finding2.id == finding3.id + assert finding1.data_id != finding2.data_id + assert finding2.data_id == finding3.data_id + assert finding1.data_hash != finding2.data_hash + assert finding2.data_hash == finding3.data_hash + assert hash(finding1) != hash(finding2) + assert hash(finding2) == hash(finding3) diff --git a/bbot/test/test_step_1/test_helpers.py b/bbot/test/test_step_1/test_helpers.py index d13f4f0aa1..9cec291941 100644 --- a/bbot/test/test_step_1/test_helpers.py +++ b/bbot/test/test_step_1/test_helpers.py @@ -64,8 +64,8 @@ async def test_helpers_misc(helpers, scan, bbot_scanner, bbot_httpserver): assert not helpers.is_subdomain("notreal") assert helpers.is_url("http://evilcorp.co.uk/asdf?a=b&c=d#asdf") assert helpers.is_url("https://evilcorp.co.uk/asdf?a=b&c=d#asdf") - assert helpers.is_uri("ftp://evilcorp.co.uk") == True - assert helpers.is_uri("http://evilcorp.co.uk") == True + assert helpers.is_uri("ftp://evilcorp.co.uk") is True + assert helpers.is_uri("http://evilcorp.co.uk") is True assert helpers.is_uri("evilcorp.co.uk", return_scheme=True) == "" assert helpers.is_uri("ftp://evilcorp.co.uk", return_scheme=True) == "ftp" assert helpers.is_uri("FTP://evilcorp.co.uk", return_scheme=True) == "ftp" @@ -93,8 +93,23 @@ async def test_helpers_misc(helpers, scan, bbot_scanner, bbot_httpserver): ipaddress.ip_network("0.0.0.0/0"), ] assert helpers.is_ip("127.0.0.1") + assert helpers.is_ip("127.0.0.1", include_network=True) + assert helpers.is_ip("127.0.0.1", version=4) + assert not helpers.is_ip("127.0.0.1", version=6) assert not helpers.is_ip("127.0.0.0.1") + assert helpers.is_ip("dead::beef") + assert helpers.is_ip("dead::beef", include_network=True) + assert not helpers.is_ip("dead::beef", version=4) + assert helpers.is_ip("dead::beef", version=6) + assert not helpers.is_ip("dead:::beef") + + assert not helpers.is_ip("1.2.3.4/24") + assert helpers.is_ip("1.2.3.4/24", include_network=True) + assert not helpers.is_ip("1.2.3.4/24", version=4) + assert helpers.is_ip("1.2.3.4/24", include_network=True, version=4) + assert not helpers.is_ip("1.2.3.4/24", include_network=True, version=6) + assert not helpers.is_ip_type("127.0.0.1") assert helpers.is_ip_type(ipaddress.ip_address("127.0.0.1")) assert not helpers.is_ip_type(ipaddress.ip_address("127.0.0.1"), network=True) @@ -104,8 +119,10 @@ async def test_helpers_misc(helpers, scan, bbot_scanner, bbot_httpserver): assert not helpers.is_ip_type(ipaddress.ip_network("127.0.0.0/8"), network=False) assert helpers.is_dns_name("evilcorp.com") + assert not helpers.is_dns_name("evilcorp.com:80") + assert not helpers.is_dns_name("http://evilcorp.com:80") assert helpers.is_dns_name("evilcorp") - assert not helpers.is_dns_name("evilcorp", include_local=False) + assert helpers.is_dns_name("evilcorp.") assert helpers.is_dns_name("ćƒ‰ćƒ”ć‚¤ćƒ³.ćƒ†ć‚¹ćƒˆ") assert not helpers.is_dns_name("127.0.0.1") assert not helpers.is_dns_name("dead::beef") @@ -266,7 +283,7 @@ async def test_helpers_misc(helpers, scan, bbot_scanner, bbot_httpserver): replaced = helpers.search_format_dict( {"asdf": [{"wat": {"here": "#{replaceme}!"}}, {500: True}]}, replaceme="asdf" ) - assert replaced["asdf"][1][500] == True + assert replaced["asdf"][1][500] is True assert replaced["asdf"][0]["wat"]["here"] == "asdf!" filtered_dict = helpers.filter_dict( @@ -298,7 +315,7 @@ async def test_helpers_misc(helpers, scan, bbot_scanner, bbot_httpserver): fuzzy=True, exclude_keys="modules", ) - assert not "secrets_db" in filtered_dict4["modules"] + assert "secrets_db" not in filtered_dict4["modules"] assert "ipneighbor" in filtered_dict4["modules"] assert "secret" in filtered_dict4["modules"]["ipneighbor"] assert "asdf" not in filtered_dict4["modules"]["ipneighbor"] @@ -391,15 +408,15 @@ async def test_helpers_misc(helpers, scan, bbot_scanner, bbot_httpserver): assert helpers.validators.validate_host("LOCALHOST ") == "localhost" assert helpers.validators.validate_host(" 192.168.1.1") == "192.168.1.1" assert helpers.validators.validate_host(" Dead::c0dE ") == "dead::c0de" - assert helpers.validators.soft_validate(" evilCorp.COM", "host") == True - assert helpers.validators.soft_validate("!@#$", "host") == False + assert helpers.validators.soft_validate(" evilCorp.COM", "host") is True + assert helpers.validators.soft_validate("!@#$", "host") is False with pytest.raises(ValueError): assert helpers.validators.validate_host("!@#$") # ports assert helpers.validators.validate_port(666) == 666 assert helpers.validators.validate_port(666666) == 65535 - assert helpers.validators.soft_validate(666, "port") == True - assert helpers.validators.soft_validate("!@#$", "port") == False + assert helpers.validators.soft_validate(666, "port") is True + assert helpers.validators.soft_validate("!@#$", "port") is False with pytest.raises(ValueError): helpers.validators.validate_port("asdf") # top tcp ports @@ -411,7 +428,7 @@ async def test_helpers_misc(helpers, scan, bbot_scanner, bbot_httpserver): assert top_tcp_ports[-10:] == [65526, 65527, 65528, 65529, 65530, 65531, 65532, 65533, 65534, 65535] assert len(top_tcp_ports) == 65535 assert len(set(top_tcp_ports)) == 65535 - assert all([isinstance(i, int) for i in top_tcp_ports]) + assert all(isinstance(i, int) for i in top_tcp_ports) top_tcp_ports = helpers.top_tcp_ports(10, as_string=True) assert top_tcp_ports == "80,23,443,21,22,25,3389,110,445,139" # urls @@ -420,20 +437,20 @@ async def test_helpers_misc(helpers, scan, bbot_scanner, bbot_httpserver): helpers.validators.validate_url_parsed(" httP://evilcorP.com/asdf?a=b&c=d#e").geturl() == "http://evilcorp.com/asdf" ) - assert helpers.validators.soft_validate(" httP://evilcorP.com/asdf?a=b&c=d#e", "url") == True - assert helpers.validators.soft_validate("!@#$", "url") == False + assert helpers.validators.soft_validate(" httP://evilcorP.com/asdf?a=b&c=d#e", "url") is True + assert helpers.validators.soft_validate("!@#$", "url") is False with pytest.raises(ValueError): helpers.validators.validate_url("!@#$") # severities assert helpers.validators.validate_severity(" iNfo") == "INFO" - assert helpers.validators.soft_validate(" iNfo", "severity") == True - assert helpers.validators.soft_validate("NOPE", "severity") == False + assert helpers.validators.soft_validate(" iNfo", "severity") is True + assert helpers.validators.soft_validate("NOPE", "severity") is False with pytest.raises(ValueError): helpers.validators.validate_severity("NOPE") # emails assert helpers.validators.validate_email(" bOb@eViLcorp.COM") == "bob@evilcorp.com" - assert helpers.validators.soft_validate(" bOb@eViLcorp.COM", "email") == True - assert helpers.validators.soft_validate("!@#$", "email") == False + assert helpers.validators.soft_validate(" bOb@eViLcorp.COM", "email") is True + assert helpers.validators.soft_validate("!@#$", "email") is False with pytest.raises(ValueError): helpers.validators.validate_email("!@#$") @@ -516,9 +533,9 @@ async def test_helpers_misc(helpers, scan, bbot_scanner, bbot_httpserver): truncated_filename.unlink() # misc DNS helpers - assert helpers.is_ptr("wsc-11-22-33-44-wat.evilcorp.com") == True - assert helpers.is_ptr("wsc-11-22-33-wat.evilcorp.com") == False - assert helpers.is_ptr("11wat.evilcorp.com") == False + assert helpers.is_ptr("wsc-11-22-33-44-wat.evilcorp.com") is True + assert helpers.is_ptr("wsc-11-22-33-wat.evilcorp.com") is False + assert helpers.is_ptr("11wat.evilcorp.com") is False ## NTLM testheader = "TlRMTVNTUAACAAAAHgAeADgAAAAVgorilwL+bvnVipUAAAAAAAAAAJgAmABWAAAACgBjRQAAAA9XAEkATgAtAFMANAAyAE4ATwBCAEQAVgBUAEsAOAACAB4AVwBJAE4ALQBTADQAMgBOAE8AQgBEAFYAVABLADgAAQAeAFcASQBOAC0AUwA0ADIATgBPAEIARABWAFQASwA4AAQAHgBXAEkATgAtAFMANAAyAE4ATwBCAEQAVgBUAEsAOAADAB4AVwBJAE4ALQBTADQAMgBOAE8AQgBEAFYAVABLADgABwAIAHUwOZlfoNgBAAAAAA==" @@ -596,8 +613,8 @@ async def test_helpers_misc(helpers, scan, bbot_scanner, bbot_httpserver): assert len(helpers.get_exception_chain(e)) == 2 assert len([_ for _ in helpers.get_exception_chain(e) if isinstance(_, KeyboardInterrupt)]) == 1 assert len([_ for _ in helpers.get_exception_chain(e) if isinstance(_, ValueError)]) == 1 - assert helpers.in_exception_chain(e, (KeyboardInterrupt, asyncio.CancelledError)) == True - assert helpers.in_exception_chain(e, (TypeError, OSError)) == False + assert helpers.in_exception_chain(e, (KeyboardInterrupt, asyncio.CancelledError)) is True + assert helpers.in_exception_chain(e, (TypeError, OSError)) is False test_ran = True assert test_ran test_ran = False @@ -610,9 +627,9 @@ async def test_helpers_misc(helpers, scan, bbot_scanner, bbot_httpserver): assert len(helpers.get_exception_chain(e)) == 2 assert len([_ for _ in helpers.get_exception_chain(e) if isinstance(_, AttributeError)]) == 1 assert len([_ for _ in helpers.get_exception_chain(e) if isinstance(_, ValueError)]) == 1 - assert helpers.in_exception_chain(e, (KeyboardInterrupt, asyncio.CancelledError)) == False - assert helpers.in_exception_chain(e, (KeyboardInterrupt, AttributeError)) == True - assert helpers.in_exception_chain(e, (AttributeError,)) == True + assert helpers.in_exception_chain(e, (KeyboardInterrupt, asyncio.CancelledError)) is False + assert helpers.in_exception_chain(e, (KeyboardInterrupt, AttributeError)) is True + assert helpers.in_exception_chain(e, (AttributeError,)) is True test_ran = True assert test_ran @@ -840,7 +857,6 @@ def test_liststring_invalidfnchars(helpers): # test parameter validation @pytest.mark.asyncio async def test_parameter_validation(helpers): - getparam_valid_params = { "name", "age", @@ -869,7 +885,7 @@ async def test_parameter_validation(helpers): if helpers.validate_parameter(p, "getparam"): assert p in getparam_valid_params and p not in getparam_invalid_params else: - assert p in getparam_invalid_params and not p in getparam_valid_params + assert p in getparam_invalid_params and p not in getparam_valid_params header_valid_params = { "name", @@ -900,7 +916,7 @@ async def test_parameter_validation(helpers): if helpers.validate_parameter(p, "header"): assert p in header_valid_params and p not in header_invalid_params else: - assert p in header_invalid_params and not p in header_valid_params + assert p in header_invalid_params and p not in header_valid_params cookie_valid_params = { "name", @@ -930,4 +946,4 @@ async def test_parameter_validation(helpers): if helpers.validate_parameter(p, "cookie"): assert p in cookie_valid_params and p not in cookie_invalid_params else: - assert p in cookie_invalid_params and not p in cookie_valid_params + assert p in cookie_invalid_params and p not in cookie_valid_params diff --git a/bbot/test/test_step_1/test_manager_scope_accuracy.py b/bbot/test/test_step_1/test_manager_scope_accuracy.py index 62a03c0ef1..f012b0e3e0 100644 --- a/bbot/test/test_step_1/test_manager_scope_accuracy.py +++ b/bbot/test/test_step_1/test_manager_scope_accuracy.py @@ -140,7 +140,7 @@ async def do_scan(*args, _config={}, _dns_mock={}, scan_callback=None, **kwargs) ) assert len(events) == 3 - assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66"]) assert 0 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notrealzies"]) assert 0 == len([e for e in events if e.type == "DNS_NAME" and e.data == "www.test.notreal"]) @@ -148,14 +148,14 @@ async def do_scan(*args, _config={}, _dns_mock={}, scan_callback=None, **kwargs) for _all_events in (all_events, all_events_nodups): assert len(_all_events) == 3 - assert 1 == len([e for e in _all_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in _all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal == True and e.scope_distance == 1]) + assert 1 == len([e for e in _all_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in _all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal is True and e.scope_distance == 1]) assert 0 == len([e for e in _all_events if e.type == "DNS_NAME" and e.data == "test.notrealzies"]) assert 0 == len([e for e in _all_events if e.type == "DNS_NAME" and e.data == "www.test.notreal"]) assert 0 == len([e for e in _all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77"]) assert len(graph_output_events) == 3 - assert 1 == len([e for e in graph_output_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in graph_output_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66"]) assert 0 == len([e for e in graph_output_events if e.type == "DNS_NAME" and e.data == "test.notrealzies"]) assert 0 == len([e for e in graph_output_events if e.type == "DNS_NAME" and e.data == "www.test.notreal"]) @@ -169,38 +169,38 @@ async def do_scan(*args, _config={}, _dns_mock={}, scan_callback=None, **kwargs) ) assert len(events) == 4 - assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66"]) assert 0 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notrealzies"]) - assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "www.test.notreal" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "www.test.notreal" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77"]) assert 0 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test2.notrealzies"]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88"]) assert len(all_events) == 9 - assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal == True and e.scope_distance == 1]) - assert 2 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "www.test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test2.notrealzies" and e.internal == True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal is True and e.scope_distance == 1]) + assert 2 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "www.test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test2.notrealzies" and e.internal is True and e.scope_distance == 2]) assert 0 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88"]) assert len(all_events_nodups) == 7 - assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "www.test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test2.notrealzies" and e.internal == True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "www.test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test2.notrealzies" and e.internal is True and e.scope_distance == 2]) assert 0 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.88"]) for _graph_output_events in (graph_output_events, graph_output_batch_events): assert len(_graph_output_events) == 6 - assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "www.test.notreal" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "www.test.notreal" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77"]) assert 0 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "test2.notrealzies"]) assert 0 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88"]) @@ -213,39 +213,39 @@ async def do_scan(*args, _config={}, _dns_mock={}, scan_callback=None, **kwargs) ) assert len(events) == 7 - assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "www.test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == False and e.scope_distance == 1]) + assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "www.test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test2.notrealzies"]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88"]) assert len(all_events) == 8 - assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal == False and e.scope_distance == 1]) - assert 2 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "www.test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test2.notrealzies" and e.internal == True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal is False and e.scope_distance == 1]) + assert 2 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "www.test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test2.notrealzies" and e.internal is True and e.scope_distance == 2]) assert 0 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88"]) assert len(all_events_nodups) == 7 - assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "www.test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test2.notrealzies" and e.internal == True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "www.test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test2.notrealzies" and e.internal is True and e.scope_distance == 2]) assert 0 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.88"]) for _graph_output_events in (graph_output_events, graph_output_batch_events): assert len(_graph_output_events) == 7 - assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "www.test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == False and e.scope_distance == 1]) + assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "www.test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "test2.notrealzies"]) assert 0 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88"]) @@ -284,33 +284,33 @@ def custom_setup(scan): ) assert len(events) == 5 - assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal == False and e.scope_distance == 1]) + assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notrealzies"]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77"]) - assert 1 == len([e for e in events if e.type == "VULNERABILITY" and e.data["host"] == "127.0.0.77" and e.internal == False and e.scope_distance == 3]) + assert 1 == len([e for e in events if e.type == "VULNERABILITY" and e.data["host"] == "127.0.0.77" and e.internal is False and e.scope_distance == 3]) assert len(all_events) == 8 - assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal == False and e.scope_distance == 1]) - assert 2 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal == True and e.scope_distance == 2]) - assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == True and e.scope_distance == 3]) - assert 1 == len([e for e in all_events if e.type == "VULNERABILITY" and e.data["host"] == "127.0.0.77" and e.internal == False and e.scope_distance == 3]) + assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal is False and e.scope_distance == 1]) + assert 2 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal is True and e.scope_distance == 2]) + assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is True and e.scope_distance == 3]) + assert 1 == len([e for e in all_events if e.type == "VULNERABILITY" and e.data["host"] == "127.0.0.77" and e.internal is False and e.scope_distance == 3]) assert len(all_events_nodups) == 6 - assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == True and e.scope_distance == 3]) - assert 1 == len([e for e in all_events_nodups if e.type == "VULNERABILITY" and e.data["host"] == "127.0.0.77" and e.internal == False and e.scope_distance == 3]) + assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is True and e.scope_distance == 3]) + assert 1 == len([e for e in all_events_nodups if e.type == "VULNERABILITY" and e.data["host"] == "127.0.0.77" and e.internal is False and e.scope_distance == 3]) for _graph_output_events in (graph_output_events, graph_output_batch_events): assert len(_graph_output_events) == 7 - assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == True and e.scope_distance == 3]) - assert 1 == len([e for e in _graph_output_events if e.type == "VULNERABILITY" and e.data["host"] == "127.0.0.77" and e.internal == False and e.scope_distance == 3]) + assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.66" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "test.notrealzies" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is True and e.scope_distance == 3]) + assert 1 == len([e for e in _graph_output_events if e.type == "VULNERABILITY" and e.data["host"] == "127.0.0.77" and e.internal is False and e.scope_distance == 3]) # httpx/speculate IP_RANGE --> IP_ADDRESS --> OPEN_TCP_PORT --> URL, search distance = 0 events, all_events, all_events_nodups, graph_output_events, graph_output_batch_events = await do_scan( @@ -328,56 +328,56 @@ def custom_setup(scan): ) assert len(events) == 7 - assert 1 == len([e for e in events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0"]) - assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888"]) - assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888"]) assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/"]) assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/"]) - assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == False and e.scope_distance == 1]) + assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888"]) assert len(all_events) == 14 - assert 1 == len([e for e in all_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal == True and e.scope_distance == 0]) - assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal == True and e.scope_distance == 0]) - assert 2 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1 and "spider-danger" in e.tags]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal == True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal is True and e.scope_distance == 0]) + assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal is True and e.scope_distance == 0]) + assert 2 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1 and "spider-danger" in e.tags]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal is True and e.scope_distance == 1]) assert len(all_events_nodups) == 12 - assert 1 == len([e for e in all_events_nodups if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1 and "spider-danger" in e.tags]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal == True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1 and "spider-danger" in e.tags]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal is True and e.scope_distance == 1]) for _graph_output_events in (graph_output_events, graph_output_batch_events): assert len(_graph_output_events) == 7 - assert 1 == len([e for e in _graph_output_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0"]) - assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888"]) - assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888"]) assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/"]) assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/"]) - assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == False and e.scope_distance == 1]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888"]) # httpx/speculate IP_RANGE --> IP_ADDRESS --> OPEN_TCP_PORT --> URL, search distance = 0, in_scope_only = False @@ -400,70 +400,70 @@ def custom_setup(scan): # before, this event was speculated off the URL_UNVERIFIED, and that's what was used by httpx to generate the URL. it was graph-important. # now for whatever reason, httpx is visiting the url directly and the open port isn't being used # I don't know what changed exactly, but it doesn't matter, either way is equally valid and bbot is meant to be flexible this way. - assert 1 == len([e for e in events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0"]) - assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888"]) - assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888"]) assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/"]) assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/"]) - assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == False and e.scope_distance == 1]) + assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888"]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1]) + assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.77:8888"]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88"]) assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/"]) assert len(all_events) == 18 - assert 1 == len([e for e in all_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal == True and e.scope_distance == 0]) - assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal == True and e.scope_distance == 0]) - assert 2 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.88:8888/" and e.internal == True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal is True and e.scope_distance == 0]) + assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal is True and e.scope_distance == 0]) + assert 2 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) assert len(all_events_nodups) == 16 - assert 1 == len([e for e in all_events_nodups if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1 and "spider-danger" in e.tags]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.88" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.88:8888/" and e.internal == True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1 and "spider-danger" in e.tags]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.88" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) for _graph_output_events in (graph_output_events, graph_output_batch_events): assert len(_graph_output_events) == 8 - assert 1 == len([e for e in _graph_output_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0"]) - assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888"]) - assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888"]) assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/"]) assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and "spider-danger" in e.tags]) - assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == False and e.scope_distance == 1]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888"]) - assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1]) + assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in _graph_output_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/"]) assert 0 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88"]) assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.88:8888/"]) @@ -483,78 +483,78 @@ def custom_setup(scan): ) assert len(events) == 8 - assert 1 == len([e for e in events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0"]) - assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888"]) - assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888"]) assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/"]) assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/"]) - assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == False and e.scope_distance == 1]) + assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888"]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1]) + assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/"]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88"]) assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/"]) assert len(all_events) == 22 - assert 1 == len([e for e in all_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal == True and e.scope_distance == 0]) - assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal == True and e.scope_distance == 0]) - assert 2 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1 and "spider-danger" in e.tags]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.88:8888/" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.88:8888" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.88:8888/" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.88:8888/" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.99:8888/" and e.internal == True and e.scope_distance == 3]) + assert 1 == len([e for e in all_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal is True and e.scope_distance == 0]) + assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal is True and e.scope_distance == 0]) + assert 2 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1 and "spider-danger" in e.tags]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.88:8888" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.99:8888/" and e.internal is True and e.scope_distance == 3]) assert len(all_events_nodups) == 20 - assert 1 == len([e for e in all_events_nodups if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1 and "spider-danger" in e.tags]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.88:8888/" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.88" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.88:8888" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.88:8888/" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.88:8888/" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.99:8888/" and e.internal == True and e.scope_distance == 3]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1 and "spider-danger" in e.tags]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.88" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.88:8888" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.88:8888/" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.99:8888/" and e.internal is True and e.scope_distance == 3]) for _graph_output_events in (graph_output_events, graph_output_batch_events): assert len(_graph_output_events) == 8 - assert 1 == len([e for e in _graph_output_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0"]) - assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:8888"]) - assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.1:8888/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.1:8888"]) assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.1:8888/"]) assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.77:8888/"]) - assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal == False and e.scope_distance == 1]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.77" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.77:8888"]) - assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal == False and e.scope_distance == 1]) + assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.77:8888/" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in _graph_output_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.77:8888/"]) assert 0 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.88"]) assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.88:8888/"]) @@ -576,25 +576,25 @@ def custom_setup(scan): ) assert len(events) == 12 - assert 1 == len([e for e in events if e.type == "IP_RANGE" and e.data == "127.0.0.110/31" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "IP_RANGE" and e.data == "127.0.0.110/31" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.110"]) - assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.111" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.111" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.110:8888"]) - assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.111:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.111:8888/" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.111:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.111:8888"]) assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.111:8888/"]) assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.222:8889/"]) - assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.222" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.222" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.33:8889/"]) - assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.33" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.33" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8888"]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8889"]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8888"]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8889"]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.222:8889/" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.222:8889"]) - assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.33:8889/" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.33:8889"]) assert 0 == len([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.44:8888/"]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.44"]) @@ -604,71 +604,71 @@ def custom_setup(scan): assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.55:8888"]) assert len(all_events) == 31 - assert 1 == len([e for e in all_events if e.type == "IP_RANGE" and e.data == "127.0.0.110/31" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.110" and e.internal == True and e.scope_distance == 0]) - assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.111" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.110:8888" and e.internal == True and e.scope_distance == 0]) - assert 2 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.111:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.111:8888/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.111:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.111:8888/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.222:8889/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.222" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.33:8889/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.33" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8888" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8889" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8889" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8888" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8889" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8889" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.222:8889/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.222:8889/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.33:8889/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.33:8889/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.44:8888/" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.44" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.55:8888/" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.55" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.44:8888" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.55:8888" and e.internal == True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "IP_RANGE" and e.data == "127.0.0.110/31" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.110" and e.internal is True and e.scope_distance == 0]) + assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.111" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.110:8888" and e.internal is True and e.scope_distance == 0]) + assert 2 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.111:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.111:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.222" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.33" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8888" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8889" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8889" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8888" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8889" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8889" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL" and e.data == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.44:8888/" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.44" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.55:8888/" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.55" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.44:8888" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.55:8888" and e.internal is True and e.scope_distance == 1]) assert len(all_events_nodups) == 27 - assert 1 == len([e for e in all_events_nodups if e.type == "IP_RANGE" and e.data == "127.0.0.110/31" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.110" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.111" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.110:8888" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.111:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.111:8888/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.111:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.111:8888/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.222:8889/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.222" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.33:8889/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.33" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8888" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8889" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8888" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8889" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.222:8889/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.222:8889/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.33:8889/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.33:8889/" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.44:8888/" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.44" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.55:8888/" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.55" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.44:8888" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.55:8888" and e.internal == True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_RANGE" and e.data == "127.0.0.110/31" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.110" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.111" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.110:8888" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.111:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.111:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.222" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.33" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8888" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8889" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8888" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8889" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL" and e.data == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "HTTP_RESPONSE" and e.data["url"] == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.44:8888/" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.44" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.55:8888/" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.55" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.44:8888" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.55:8888" and e.internal is True and e.scope_distance == 1]) for _graph_output_events in (graph_output_events, graph_output_batch_events): assert len(_graph_output_events) == 12 - assert 1 == len([e for e in _graph_output_events if e.type == "IP_RANGE" and e.data == "127.0.0.110/31" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_RANGE" and e.data == "127.0.0.110/31" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.110"]) - assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.111" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.111" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.110:8888"]) - assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.111:8888" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.111:8888/" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.111:8888" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.111:8888/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.111:8888"]) assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.111:8888/"]) assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.222:8889/"]) @@ -679,9 +679,9 @@ def custom_setup(scan): assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.222:8889"]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8888"]) assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.33:8889"]) - assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.222:8889/" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.222:8889/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.222:8889"]) - assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.33:8889/" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "URL" and e.data == "http://127.0.0.33:8889/" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "HTTP_RESPONSE" and e.data["input"] == "127.0.0.33:8889"]) assert 0 == len([e for e in _graph_output_events if e.type == "URL_UNVERIFIED" and e.data == "http://127.0.0.44:8888/"]) assert 0 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.44"]) @@ -699,49 +699,49 @@ def custom_setup(scan): ) assert len(events) == 7 - assert 1 == len([e for e in events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0"]) assert 1 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1"]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:9999"]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:9999"]) - assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0 and str(e.module) == "sslcert"]) - assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "www.bbottest.notreal" and e.internal == False and e.scope_distance == 1 and str(e.module) == "sslcert" and "affiliate" in e.tags]) + assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0 and str(e.module) == "sslcert"]) + assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "www.bbottest.notreal" and e.internal is False and e.scope_distance == 1 and str(e.module) == "sslcert" and "affiliate" in e.tags]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "test.notreal:9999"]) assert 0 == len([e for e in events if e.type == "DNS_NAME_UNRESOLVED" and e.data == "notreal"]) assert len(all_events) == 13 - assert 1 == len([e for e in all_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal == True and e.scope_distance == 0]) - assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:9999" and e.internal == True and e.scope_distance == 0]) - assert 2 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:9999" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0 and str(e.module) == "sslcert"]) - assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "www.bbottest.notreal" and e.internal == False and e.scope_distance == 1 and str(e.module) == "sslcert"]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "www.bbottest.notreal:9999" and e.internal == True and e.scope_distance == 1 and str(e.module) == "speculate"]) - assert 1 == len([e for e in all_events if e.type == "DNS_NAME_UNRESOLVED" and e.data == "bbottest.notreal" and e.internal == True and e.scope_distance == 2 and str(e.module) == "speculate"]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "test.notreal:9999" and e.internal == True and e.scope_distance == 0 and str(e.module) == "speculate"]) + assert 1 == len([e for e in all_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal is True and e.scope_distance == 0]) + assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:9999" and e.internal is True and e.scope_distance == 0]) + assert 2 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:9999" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0 and str(e.module) == "sslcert"]) + assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "www.bbottest.notreal" and e.internal is False and e.scope_distance == 1 and str(e.module) == "sslcert"]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "www.bbottest.notreal:9999" and e.internal is True and e.scope_distance == 1 and str(e.module) == "speculate"]) + assert 1 == len([e for e in all_events if e.type == "DNS_NAME_UNRESOLVED" and e.data == "bbottest.notreal" and e.internal is True and e.scope_distance == 2 and str(e.module) == "speculate"]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "test.notreal:9999" and e.internal is True and e.scope_distance == 0 and str(e.module) == "speculate"]) assert len(all_events_nodups) == 11 - assert 1 == len([e for e in all_events_nodups if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:9999" and e.internal == True and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:9999" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0 and str(e.module) == "sslcert"]) - assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "www.bbottest.notreal" and e.internal == False and e.scope_distance == 1 and str(e.module) == "sslcert"]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "www.bbottest.notreal:9999" and e.internal == True and e.scope_distance == 1 and str(e.module) == "speculate"]) - assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME_UNRESOLVED" and e.data == "bbottest.notreal" and e.internal == True and e.scope_distance == 2 and str(e.module) == "speculate"]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "test.notreal:9999" and e.internal == True and e.scope_distance == 0 and str(e.module) == "speculate"]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:9999" and e.internal is True and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:9999" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0 and str(e.module) == "sslcert"]) + assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "www.bbottest.notreal" and e.internal is False and e.scope_distance == 1 and str(e.module) == "sslcert"]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "www.bbottest.notreal:9999" and e.internal is True and e.scope_distance == 1 and str(e.module) == "speculate"]) + assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME_UNRESOLVED" and e.data == "bbottest.notreal" and e.internal is True and e.scope_distance == 2 and str(e.module) == "speculate"]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "test.notreal:9999" and e.internal is True and e.scope_distance == 0 and str(e.module) == "speculate"]) for _graph_output_events in (graph_output_events, graph_output_batch_events): assert len(_graph_output_events) == 7 - assert 1 == len([e for e in _graph_output_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0"]) - assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is False and e.scope_distance == 0]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:9999"]) - assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:9999" and e.internal == False and e.scope_distance == 0]) - assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0 and str(e.module) == "sslcert"]) - assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "www.bbottest.notreal" and e.internal == False and e.scope_distance == 1 and str(e.module) == "sslcert"]) + assert 1 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:9999" and e.internal is False and e.scope_distance == 0]) + assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0 and str(e.module) == "sslcert"]) + assert 1 == len([e for e in _graph_output_events if e.type == "DNS_NAME" and e.data == "www.bbottest.notreal" and e.internal is False and e.scope_distance == 1 and str(e.module) == "sslcert"]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "www.bbottest.notreal:9999"]) assert 0 == len([e for e in _graph_output_events if e.type == "DNS_NAME_UNRESOLVED" and e.data == "bbottest.notreal"]) assert 0 == len([e for e in _graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "test.notreal:9999"]) @@ -756,43 +756,43 @@ def custom_setup(scan): ) assert len(events) == 4 - assert 1 == len([e for e in events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 1]) + assert 1 == len([e for e in events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0"]) assert 0 == len([e for e in events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1"]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:9999"]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:9999"]) - assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0 and str(e.module) == "sslcert"]) + assert 1 == len([e for e in events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0 and str(e.module) == "sslcert"]) assert 0 == len([e for e in events if e.type == "DNS_NAME" and e.data == "www.bbottest.notreal"]) assert 0 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "test.notreal:9999"]) assert len(all_events) == 11 - assert 1 == len([e for e in all_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal == True and e.scope_distance == 2]) - assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:9999" and e.internal == True and e.scope_distance == 2]) - assert 2 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:9999" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0 and str(e.module) == "sslcert"]) - assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "www.bbottest.notreal" and e.internal == True and e.scope_distance == 3 and str(e.module) == "sslcert"]) - assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "test.notreal:9999" and e.internal == True and e.scope_distance == 0 and str(e.module) == "speculate"]) + assert 1 == len([e for e in all_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal is True and e.scope_distance == 2]) + assert 2 == len([e for e in all_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:9999" and e.internal is True and e.scope_distance == 2]) + assert 2 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:9999" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0 and str(e.module) == "sslcert"]) + assert 1 == len([e for e in all_events if e.type == "DNS_NAME" and e.data == "www.bbottest.notreal" and e.internal is True and e.scope_distance == 3 and str(e.module) == "sslcert"]) + assert 1 == len([e for e in all_events if e.type == "OPEN_TCP_PORT" and e.data == "test.notreal:9999" and e.internal is True and e.scope_distance == 0 and str(e.module) == "speculate"]) assert len(all_events_nodups) == 9 - assert 1 == len([e for e in all_events_nodups if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:9999" and e.internal == True and e.scope_distance == 2]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:9999" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0 and str(e.module) == "sslcert"]) - assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "www.bbottest.notreal" and e.internal == True and e.scope_distance == 3 and str(e.module) == "sslcert"]) - assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "test.notreal:9999" and e.internal == True and e.scope_distance == 0 and str(e.module) == "speculate"]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.0" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events_nodups if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:9999" and e.internal is True and e.scope_distance == 2]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:9999" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0 and str(e.module) == "sslcert"]) + assert 1 == len([e for e in all_events_nodups if e.type == "DNS_NAME" and e.data == "www.bbottest.notreal" and e.internal is True and e.scope_distance == 3 and str(e.module) == "sslcert"]) + assert 1 == len([e for e in all_events_nodups if e.type == "OPEN_TCP_PORT" and e.data == "test.notreal:9999" and e.internal is True and e.scope_distance == 0 and str(e.module) == "speculate"]) for _graph_output_events in (graph_output_events, graph_output_batch_events): assert len(_graph_output_events) == 6 - assert 1 == len([e for e in graph_output_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal == False and e.scope_distance == 1]) + assert 1 == len([e for e in graph_output_events if e.type == "IP_RANGE" and e.data == "127.0.0.0/31" and e.internal is False and e.scope_distance == 1]) assert 0 == len([e for e in graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.0"]) - assert 1 == len([e for e in graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal == True and e.scope_distance == 2]) + assert 1 == len([e for e in graph_output_events if e.type == "IP_ADDRESS" and e.data == "127.0.0.1" and e.internal is True and e.scope_distance == 2]) assert 0 == len([e for e in graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.0:9999"]) - assert 1 == len([e for e in graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:9999" and e.internal == True and e.scope_distance == 1]) - assert 1 == len([e for e in graph_output_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal == False and e.scope_distance == 0 and str(e.module) == "sslcert"]) + assert 1 == len([e for e in graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "127.0.0.1:9999" and e.internal is True and e.scope_distance == 1]) + assert 1 == len([e for e in graph_output_events if e.type == "DNS_NAME" and e.data == "test.notreal" and e.internal is False and e.scope_distance == 0 and str(e.module) == "sslcert"]) assert 0 == len([e for e in graph_output_events if e.type == "DNS_NAME" and e.data == "www.bbottest.notreal"]) assert 0 == len([e for e in graph_output_events if e.type == "OPEN_TCP_PORT" and e.data == "test.notreal:9999"]) @@ -817,9 +817,9 @@ async def test_manager_blacklist(bbot_scanner, bbot_httpserver, caplog): events = [e async for e in scan.async_start()] - assert any([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://www-dev.test.notreal:8888/"]) + assert any(e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://www-dev.test.notreal:8888/") # the hostname is in-scope, but its IP is blacklisted, therefore we shouldn't see it - assert not any([e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://www-prod.test.notreal:8888/"]) + assert not any(e for e in events if e.type == "URL_UNVERIFIED" and e.data == "http://www-prod.test.notreal:8888/") assert 'Not forwarding DNS_NAME("www-prod.test.notreal", module=excavate' in caplog.text and 'because it has a blacklisted DNS record' in caplog.text diff --git a/bbot/test/test_step_1/test_modules_basic.py b/bbot/test/test_step_1/test_modules_basic.py index 3051f8de1e..fbb8b35f15 100644 --- a/bbot/test/test_step_1/test_modules_basic.py +++ b/bbot/test/test_step_1/test_modules_basic.py @@ -10,9 +10,6 @@ @pytest.mark.asyncio async def test_modules_basic_checks(events, httpx_mock): - for http_method in ("GET", "CONNECT", "HEAD", "POST", "PUT", "TRACE", "DEBUG", "PATCH", "DELETE", "OPTIONS"): - httpx_mock.add_response(method=http_method, url=re.compile(r".*"), json={"test": "test"}) - from bbot.scanner import Scanner scan = Scanner(config={"omit_event_types": ["URL_UNVERIFIED"]}) @@ -26,27 +23,27 @@ async def test_modules_basic_checks(events, httpx_mock): localhost = scan.make_event("127.0.0.1", parent=scan.root_event) # ip addresses should be accepted result, reason = base_output_module_1._event_precheck(localhost) - assert result == True + assert result is True assert reason == "precheck succeeded" # internal events should be rejected localhost._internal = True result, reason = base_output_module_1._event_precheck(localhost) - assert result == False + assert result is False assert reason == "_internal is True" localhost._internal = False result, reason = base_output_module_1._event_precheck(localhost) - assert result == True + assert result is True assert reason == "precheck succeeded" # unwatched events should be rejected dns_name = scan.make_event("evilcorp.com", parent=scan.root_event) result, reason = base_output_module_1._event_precheck(dns_name) - assert result == False + assert result is False assert reason == "its type is not in watched_events" # omitted events matching watched types should be accepted url_unverified = scan.make_event("http://127.0.0.1", "URL_UNVERIFIED", parent=scan.root_event) url_unverified._omit = True result, reason = base_output_module_1._event_precheck(url_unverified) - assert result == True + assert result is True assert reason == "its type is explicitly in watched_events" base_output_module_2 = BaseOutputModule(scan) @@ -54,42 +51,42 @@ async def test_modules_basic_checks(events, httpx_mock): # normal events should be accepted localhost = scan.make_event("127.0.0.1", parent=scan.root_event) result, reason = base_output_module_2._event_precheck(localhost) - assert result == True + assert result is True assert reason == "precheck succeeded" # internal events should be rejected localhost._internal = True result, reason = base_output_module_2._event_precheck(localhost) - assert result == False + assert result is False assert reason == "_internal is True" localhost._internal = False result, reason = base_output_module_2._event_precheck(localhost) - assert result == True + assert result is True assert reason == "precheck succeeded" # omitted events should be rejected localhost._omit = True result, reason = base_output_module_2._event_precheck(localhost) - assert result == False + assert result is False assert reason == "_omit is True" # normal event should be accepted url_unverified = scan.make_event("http://127.0.0.1", "URL_UNVERIFIED", parent=scan.root_event) result, reason = base_output_module_2._event_precheck(url_unverified) - assert result == True + assert result is True assert reason == "precheck succeeded" # omitted event types should be marked during scan egress await scan.egress_module.handle_event(url_unverified) result, reason = base_output_module_2._event_precheck(url_unverified) - assert result == False + assert result is False assert reason == "_omit is True" # omitted events that are targets should be accepted dns_name = scan.make_event("evilcorp.com", "DNS_NAME", parent=scan.root_event) dns_name._omit = True result, reason = base_output_module_2._event_precheck(dns_name) - assert result == False + assert result is False assert reason == "_omit is True" # omitted results that are targets should be accepted dns_name.add_tag("target") result, reason = base_output_module_2._event_precheck(dns_name) - assert result == True + assert result is True assert reason == "it's a target" # common event filtering tests @@ -100,18 +97,18 @@ async def test_modules_basic_checks(events, httpx_mock): # base cases base_module._watched_events = None base_module.watched_events = ["*"] - assert base_module._event_precheck(events.emoji)[0] == True + assert base_module._event_precheck(events.emoji)[0] is True base_module._watched_events = None base_module.watched_events = ["IP_ADDRESS"] - assert base_module._event_precheck(events.ipv4)[0] == True - assert base_module._event_precheck(events.domain)[0] == False - assert base_module._event_precheck(events.localhost)[0] == True - assert base_module._event_precheck(localhost2)[0] == True + assert base_module._event_precheck(events.ipv4)[0] is True + assert base_module._event_precheck(events.domain)[0] is False + assert base_module._event_precheck(events.localhost)[0] is True + assert base_module._event_precheck(localhost2)[0] is True # target only base_module.target_only = True - assert base_module._event_precheck(localhost2)[0] == False + assert base_module._event_precheck(localhost2)[0] is False localhost2.add_tag("target") - assert base_module._event_precheck(localhost2)[0] == True + assert base_module._event_precheck(localhost2)[0] is True base_module.target_only = False # in scope only @@ -150,26 +147,24 @@ async def test_modules_basic_checks(events, httpx_mock): for flag in flags: all_flags.add(flag) if preloaded["type"] == "scan": - assert ("active" in flags and not "passive" in flags) or ( - not "active" in flags and "passive" in flags + assert ("active" in flags and "passive" not in flags) or ( + "active" not in flags and "passive" in flags ), f'module "{module_name}" must have either "active" or "passive" flag' - assert ("safe" in flags and not "aggressive" in flags) or ( - not "safe" in flags and "aggressive" in flags + assert ("safe" in flags and "aggressive" not in flags) or ( + "safe" not in flags and "aggressive" in flags ), f'module "{module_name}" must have either "safe" or "aggressive" flag' assert not ( "web-basic" in flags and "web-thorough" in flags ), f'module "{module_name}" should have either "web-basic" or "web-thorough" flags, not both' - meta = preloaded.get("meta", {}) - # make sure every module has a description - assert meta.get("description", ""), f"{module_name} must have a description" - # make sure every module has an author - assert meta.get("author", ""), f"{module_name} must have an author" - # make sure every module has a created date - created_date = meta.get("created_date", "") - assert created_date, f"{module_name} must have a created date" - assert created_date_regex.match( - created_date - ), f"{module_name}'s created_date must match the format YYYY-MM-DD" + meta = preloaded.get("meta", {}) + # make sure every module has a description + assert meta.get("description", ""), f"{module_name} must have a description" + # make sure every module has an author + assert meta.get("author", ""), f"{module_name} must have an author" + # make sure every module has a created date + created_date = meta.get("created_date", "") + assert created_date, f"{module_name} must have a created date" + assert created_date_regex.match(created_date), f"{module_name}'s created_date must match the format YYYY-MM-DD" # attribute checks watched_events = preloaded.get("watched_events") @@ -177,15 +172,15 @@ async def test_modules_basic_checks(events, httpx_mock): assert type(watched_events) == list assert type(produced_events) == list - if not preloaded.get("type", "") in ("internal",): + if preloaded.get("type", "") not in ("internal",): assert watched_events, f"{module_name}.watched_events must not be empty" assert type(watched_events) == list, f"{module_name}.watched_events must be of type list" assert type(produced_events) == list, f"{module_name}.produced_events must be of type list" assert all( - [type(t) == str for t in watched_events] + type(t) == str for t in watched_events ), f"{module_name}.watched_events entries must be of type string" assert all( - [type(t) == str for t in produced_events] + type(t) == str for t in produced_events ), f"{module_name}.produced_events entries must be of type string" assert type(preloaded.get("deps_pip", [])) == list, f"{module_name}.deps_pip must be of type list" @@ -271,35 +266,35 @@ class mod_domain_only(BaseModule): valid_5, reason_5 = await module._event_postcheck(url_5) if mod_name == "mod_normal": - assert valid_1 == True - assert valid_2 == True - assert valid_3 == True - assert valid_4 == True - assert valid_5 == True + assert valid_1 is True + assert valid_2 is True + assert valid_3 is True + assert valid_4 is True + assert valid_5 is True elif mod_name == "mod_host_only": - assert valid_1 == True - assert valid_2 == False + assert valid_1 is True + assert valid_2 is False assert "per_host_only=True" in reason_2 - assert valid_3 == False + assert valid_3 is False assert "per_host_only=True" in reason_3 - assert valid_4 == True - assert valid_5 == True + assert valid_4 is True + assert valid_5 is True elif mod_name == "mod_hostport_only": - assert valid_1 == True - assert valid_2 == False + assert valid_1 is True + assert valid_2 is False assert "per_hostport_only=True" in reason_2 - assert valid_3 == True - assert valid_4 == True - assert valid_5 == True + assert valid_3 is True + assert valid_4 is True + assert valid_5 is True elif mod_name == "mod_domain_only": - assert valid_1 == True - assert valid_2 == False + assert valid_1 is True + assert valid_2 is False assert "per_domain_only=True" in reason_2 - assert valid_3 == False + assert valid_3 is False assert "per_domain_only=True" in reason_3 - assert valid_4 == False + assert valid_4 is False assert "per_domain_only=True" in reason_4 - assert valid_5 == True + assert valid_5 is True await scan._cleanup() @@ -334,15 +329,15 @@ async def test_modules_basic_perdomainonly(bbot_scanner, monkeypatch): valid_1, reason_1 = await module._event_postcheck(url_1) valid_2, reason_2 = await module._event_postcheck(url_2) - if module.per_domain_only == True: - assert valid_1 == True - assert valid_2 == False + if module.per_domain_only is True: + assert valid_1 is True + assert valid_2 is False assert hash("evilcorp.com") in module._per_host_tracker assert reason_2 == "per_domain_only enabled and already seen domain" else: - assert valid_1 == True - assert valid_2 == True + assert valid_1 is True + assert valid_2 is True await per_domain_scan._cleanup() @@ -400,7 +395,6 @@ async def handle_event(self, event): "ORG_STUB": 1, "URL_UNVERIFIED": 1, "FINDING": 1, - "ORG_STUB": 1, } assert set(scan.stats.module_stats) == {"speculate", "host", "TARGET", "python", "dummy", "dnsresolve"} diff --git a/bbot/test/test_step_1/test_presets.py b/bbot/test/test_step_1/test_presets.py index ede53b632c..be31b38673 100644 --- a/bbot/test/test_step_1/test_presets.py +++ b/bbot/test/test_step_1/test_presets.py @@ -16,7 +16,7 @@ def test_preset_descriptions(): # ensure very preset has a description preset = Preset() - for yaml_file, (loaded_preset, category, preset_path, original_filename) in preset.all_presets.items(): + for loaded_preset, category, preset_path, original_filename in preset.all_presets.values(): assert ( loaded_preset.description ), f'Preset "{loaded_preset.name}" at {original_filename} does not have a description.' @@ -68,7 +68,6 @@ def test_core(): def test_preset_yaml(clean_default_config): - import yaml preset1 = Preset( @@ -86,12 +85,15 @@ def test_preset_yaml(clean_default_config): debug=False, silent=True, config={"preset_test_asdf": 1}, - strict_scope=False, ) preset1 = preset1.bake() - assert "evilcorp.com" in preset1.target + assert "evilcorp.com" in preset1.target.seeds + assert "evilcorp.ce" not in preset1.target.seeds + assert "asdf.www.evilcorp.ce" in preset1.target.seeds assert "evilcorp.ce" in preset1.whitelist + assert "asdf.evilcorp.ce" in preset1.whitelist assert "test.www.evilcorp.ce" in preset1.blacklist + assert "asdf.test.www.evilcorp.ce" in preset1.blacklist assert "sslcert" in preset1.scan_modules assert preset1.whitelisted("evilcorp.ce") assert preset1.whitelisted("www.evilcorp.ce") @@ -168,16 +170,17 @@ def test_preset_cache(): def test_preset_scope(): - # test target merging scan = Scanner("1.2.3.4", preset=Preset.from_dict({"target": ["evilcorp.com"]})) - assert set([str(h) for h in scan.preset.target.seeds.hosts]) == {"1.2.3.4", "evilcorp.com"} - assert set([e.data for e in scan.target]) == {"1.2.3.4", "evilcorp.com"} + assert {str(h) for h in scan.preset.target.seeds.hosts} == {"1.2.3.4/32", "evilcorp.com"} + assert {e.data for e in scan.target.seeds} == {"1.2.3.4", "evilcorp.com"} + assert {e.data for e in scan.target.whitelist} == {"1.2.3.4", "evilcorp.com"} blank_preset = Preset() blank_preset = blank_preset.bake() - assert not blank_preset.target - assert blank_preset.strict_scope == False + assert not blank_preset.target.seeds + assert not blank_preset.target.whitelist + assert blank_preset.strict_scope is False preset1 = Preset( "evilcorp.com", @@ -188,13 +191,14 @@ def test_preset_scope(): preset1_baked = preset1.bake() # make sure target logic works as expected - assert "evilcorp.com" in preset1_baked.target - assert "asdf.evilcorp.com" in preset1_baked.target - assert "asdf.www.evilcorp.ce" in preset1_baked.target - assert not "evilcorp.ce" in preset1_baked.target + assert "evilcorp.com" in preset1_baked.target.seeds + assert "evilcorp.com" not in preset1_baked.target.whitelist + assert "asdf.evilcorp.com" in preset1_baked.target.seeds + assert "asdf.evilcorp.com" not in preset1_baked.target.whitelist + assert "asdf.evilcorp.ce" in preset1_baked.whitelist assert "evilcorp.ce" in preset1_baked.whitelist assert "test.www.evilcorp.ce" in preset1_baked.blacklist - assert not "evilcorp.ce" in preset1_baked.blacklist + assert "evilcorp.ce" not in preset1_baked.blacklist assert preset1_baked.in_scope("www.evilcorp.ce") assert not preset1_baked.in_scope("evilcorp.com") assert not preset1_baked.in_scope("asdf.test.www.evilcorp.ce") @@ -210,7 +214,7 @@ def test_preset_scope(): "evilcorp.org", whitelist=["evilcorp.de"], blacklist=["test.www.evilcorp.de"], - strict_scope=True, + config={"scope": {"strict": True}}, ) preset1.merge(preset3) @@ -218,20 +222,24 @@ def test_preset_scope(): preset1_baked = preset1.bake() # targets should be merged - assert "evilcorp.com" in preset1_baked.target - assert "www.evilcorp.ce" in preset1_baked.target - assert "evilcorp.org" in preset1_baked.target + assert "evilcorp.com" in preset1_baked.target.seeds + assert "www.evilcorp.ce" in preset1_baked.target.seeds + assert "evilcorp.org" in preset1_baked.target.seeds # strict scope is enabled - assert not "asdf.evilcorp.com" in preset1_baked.target - assert not "asdf.www.evilcorp.ce" in preset1_baked.target + assert "asdf.www.evilcorp.ce" not in preset1_baked.target.seeds + assert "asdf.evilcorp.org" not in preset1_baked.target.seeds + assert "asdf.evilcorp.com" not in preset1_baked.target.seeds + assert "asdf.www.evilcorp.ce" not in preset1_baked.target.seeds assert "evilcorp.ce" in preset1_baked.whitelist assert "evilcorp.de" in preset1_baked.whitelist - assert not "asdf.evilcorp.de" in preset1_baked.whitelist - assert not "asdf.evilcorp.ce" in preset1_baked.whitelist + assert "asdf.evilcorp.de" not in preset1_baked.whitelist + assert "asdf.evilcorp.ce" not in preset1_baked.whitelist # blacklist should be merged, strict scope does not apply + assert "test.www.evilcorp.ce" in preset1_baked.blacklist + assert "test.www.evilcorp.de" in preset1_baked.blacklist assert "asdf.test.www.evilcorp.ce" in preset1_baked.blacklist assert "asdf.test.www.evilcorp.de" in preset1_baked.blacklist - assert not "asdf.test.www.evilcorp.org" in preset1_baked.blacklist + assert "asdf.test.www.evilcorp.org" not in preset1_baked.blacklist # only the base domain of evilcorp.de should be in scope assert not preset1_baked.in_scope("evilcorp.com") assert not preset1_baked.in_scope("evilcorp.org") @@ -264,14 +272,14 @@ def test_preset_scope(): } assert preset_whitelist_baked.to_dict(include_target=True) == { "target": ["evilcorp.org"], - "whitelist": ["1.2.3.0/24", "evilcorp.net"], - "blacklist": ["evilcorp.co.uk"], + "whitelist": ["1.2.3.4/24", "http://evilcorp.net"], + "blacklist": ["bob@evilcorp.co.uk", "evilcorp.co.uk:443"], "config": {"modules": {"secretsdb": {"api_key": "deadbeef", "otherthing": "asdf"}}}, } assert preset_whitelist_baked.to_dict(include_target=True, redact_secrets=True) == { "target": ["evilcorp.org"], - "whitelist": ["1.2.3.0/24", "evilcorp.net"], - "blacklist": ["evilcorp.co.uk"], + "whitelist": ["1.2.3.4/24", "http://evilcorp.net"], + "blacklist": ["bob@evilcorp.co.uk", "evilcorp.co.uk:443"], "config": {"modules": {"secretsdb": {"otherthing": "asdf"}}}, } @@ -279,7 +287,8 @@ def test_preset_scope(): assert not preset_nowhitelist_baked.in_scope("www.evilcorp.de") assert not preset_nowhitelist_baked.in_scope("1.2.3.4/24") - assert "www.evilcorp.org" in preset_whitelist_baked.target + assert "www.evilcorp.org" in preset_whitelist_baked.target.seeds + assert "www.evilcorp.org" not in preset_whitelist_baked.target.whitelist assert "1.2.3.4" in preset_whitelist_baked.whitelist assert not preset_whitelist_baked.in_scope("www.evilcorp.org") assert not preset_whitelist_baked.in_scope("www.evilcorp.de") @@ -292,17 +301,17 @@ def test_preset_scope(): assert preset_whitelist_baked.whitelisted("1.2.3.4/28") assert preset_whitelist_baked.whitelisted("1.2.3.4/24") - assert set([e.data for e in preset_nowhitelist_baked.target]) == {"evilcorp.com"} - assert set([e.data for e in preset_whitelist_baked.target]) == {"evilcorp.org"} - assert set([e.data for e in preset_nowhitelist_baked.whitelist]) == {"evilcorp.com"} - assert set([e.data for e in preset_whitelist_baked.whitelist]) == {"1.2.3.0/24", "evilcorp.net"} + assert {e.data for e in preset_nowhitelist_baked.seeds} == {"evilcorp.com"} + assert {e.data for e in preset_nowhitelist_baked.whitelist} == {"evilcorp.com"} + assert {e.data for e in preset_whitelist_baked.seeds} == {"evilcorp.org"} + assert {e.data for e in preset_whitelist_baked.whitelist} == {"1.2.3.0/24", "http://evilcorp.net/"} preset_nowhitelist.merge(preset_whitelist) preset_nowhitelist_baked = preset_nowhitelist.bake() - assert set([e.data for e in preset_nowhitelist_baked.target]) == {"evilcorp.com", "evilcorp.org"} - assert set([e.data for e in preset_nowhitelist_baked.whitelist]) == {"1.2.3.0/24", "evilcorp.net"} - assert "www.evilcorp.org" in preset_nowhitelist_baked.target - assert "www.evilcorp.com" in preset_nowhitelist_baked.target + assert {e.data for e in preset_nowhitelist_baked.seeds} == {"evilcorp.com", "evilcorp.org"} + assert {e.data for e in preset_nowhitelist_baked.whitelist} == {"1.2.3.0/24", "http://evilcorp.net/"} + assert "www.evilcorp.org" in preset_nowhitelist_baked.seeds + assert "www.evilcorp.com" in preset_nowhitelist_baked.seeds assert "1.2.3.4" in preset_nowhitelist_baked.whitelist assert not preset_nowhitelist_baked.in_scope("www.evilcorp.org") assert not preset_nowhitelist_baked.in_scope("www.evilcorp.com") @@ -314,10 +323,12 @@ def test_preset_scope(): preset_whitelist = Preset("evilcorp.org", whitelist=["1.2.3.4/24"]) preset_whitelist.merge(preset_nowhitelist) preset_whitelist_baked = preset_whitelist.bake() - assert set([e.data for e in preset_whitelist_baked.target]) == {"evilcorp.com", "evilcorp.org"} - assert set([e.data for e in preset_whitelist_baked.whitelist]) == {"1.2.3.0/24"} - assert "www.evilcorp.org" in preset_whitelist_baked.target - assert "www.evilcorp.com" in preset_whitelist_baked.target + assert {e.data for e in preset_whitelist_baked.seeds} == {"evilcorp.com", "evilcorp.org"} + assert {e.data for e in preset_whitelist_baked.whitelist} == {"1.2.3.0/24"} + assert "www.evilcorp.org" in preset_whitelist_baked.seeds + assert "www.evilcorp.com" in preset_whitelist_baked.seeds + assert "www.evilcorp.org" not in preset_whitelist_baked.target.whitelist + assert "www.evilcorp.com" not in preset_whitelist_baked.target.whitelist assert "1.2.3.4" in preset_whitelist_baked.whitelist assert not preset_whitelist_baked.in_scope("www.evilcorp.org") assert not preset_whitelist_baked.in_scope("www.evilcorp.com") @@ -329,18 +340,18 @@ def test_preset_scope(): preset_nowhitelist2 = Preset("evilcorp.de") preset_nowhitelist1_baked = preset_nowhitelist1.bake() preset_nowhitelist2_baked = preset_nowhitelist2.bake() - assert set([e.data for e in preset_nowhitelist1_baked.target]) == {"evilcorp.com"} - assert set([e.data for e in preset_nowhitelist2_baked.target]) == {"evilcorp.de"} - assert set([e.data for e in preset_nowhitelist1_baked.whitelist]) == {"evilcorp.com"} - assert set([e.data for e in preset_nowhitelist2_baked.whitelist]) == {"evilcorp.de"} + assert {e.data for e in preset_nowhitelist1_baked.seeds} == {"evilcorp.com"} + assert {e.data for e in preset_nowhitelist2_baked.seeds} == {"evilcorp.de"} + assert {e.data for e in preset_nowhitelist1_baked.whitelist} == {"evilcorp.com"} + assert {e.data for e in preset_nowhitelist2_baked.whitelist} == {"evilcorp.de"} preset_nowhitelist1.merge(preset_nowhitelist2) preset_nowhitelist1_baked = preset_nowhitelist1.bake() - assert set([e.data for e in preset_nowhitelist1_baked.target]) == {"evilcorp.com", "evilcorp.de"} - assert set([e.data for e in preset_nowhitelist2_baked.target]) == {"evilcorp.de"} - assert set([e.data for e in preset_nowhitelist1_baked.whitelist]) == {"evilcorp.com", "evilcorp.de"} - assert set([e.data for e in preset_nowhitelist2_baked.whitelist]) == {"evilcorp.de"} - assert "www.evilcorp.com" in preset_nowhitelist1_baked.target - assert "www.evilcorp.de" in preset_nowhitelist1_baked.target + assert {e.data for e in preset_nowhitelist1_baked.seeds} == {"evilcorp.com", "evilcorp.de"} + assert {e.data for e in preset_nowhitelist2_baked.seeds} == {"evilcorp.de"} + assert {e.data for e in preset_nowhitelist1_baked.whitelist} == {"evilcorp.com", "evilcorp.de"} + assert {e.data for e in preset_nowhitelist2_baked.whitelist} == {"evilcorp.de"} + assert "www.evilcorp.com" in preset_nowhitelist1_baked.seeds + assert "www.evilcorp.de" in preset_nowhitelist1_baked.seeds assert "www.evilcorp.com" in preset_nowhitelist1_baked.target.seeds assert "www.evilcorp.de" in preset_nowhitelist1_baked.target.seeds assert "www.evilcorp.com" in preset_nowhitelist1_baked.whitelist @@ -357,15 +368,14 @@ def test_preset_scope(): preset_nowhitelist2.merge(preset_nowhitelist1) preset_nowhitelist1_baked = preset_nowhitelist1.bake() preset_nowhitelist2_baked = preset_nowhitelist2.bake() - assert set([e.data for e in preset_nowhitelist1_baked.target]) == {"evilcorp.com"} - assert set([e.data for e in preset_nowhitelist2_baked.target]) == {"evilcorp.com", "evilcorp.de"} - assert set([e.data for e in preset_nowhitelist1_baked.whitelist]) == {"evilcorp.com"} - assert set([e.data for e in preset_nowhitelist2_baked.whitelist]) == {"evilcorp.com", "evilcorp.de"} + assert {e.data for e in preset_nowhitelist1_baked.seeds} == {"evilcorp.com"} + assert {e.data for e in preset_nowhitelist2_baked.seeds} == {"evilcorp.com", "evilcorp.de"} + assert {e.data for e in preset_nowhitelist1_baked.whitelist} == {"evilcorp.com"} + assert {e.data for e in preset_nowhitelist2_baked.whitelist} == {"evilcorp.com", "evilcorp.de"} @pytest.mark.asyncio async def test_preset_logging(): - scan = Scanner() # test individual verbosity levels @@ -374,30 +384,30 @@ async def test_preset_logging(): try: silent_preset = Preset(silent=True) - assert silent_preset.silent == True - assert silent_preset.debug == False - assert silent_preset.verbose == False + assert silent_preset.silent is True + assert silent_preset.debug is False + assert silent_preset.verbose is False assert original_log_level == CORE.logger.log_level debug_preset = Preset(debug=True) - assert debug_preset.silent == False - assert debug_preset.debug == True - assert debug_preset.verbose == False + assert debug_preset.silent is False + assert debug_preset.debug is True + assert debug_preset.verbose is False assert original_log_level == CORE.logger.log_level verbose_preset = Preset(verbose=True) - assert verbose_preset.silent == False - assert verbose_preset.debug == False - assert verbose_preset.verbose == True + assert verbose_preset.silent is False + assert verbose_preset.debug is False + assert verbose_preset.verbose is True assert original_log_level == CORE.logger.log_level # test conflicting verbosity levels silent_and_verbose = Preset(silent=True, verbose=True) - assert silent_and_verbose.silent == True - assert silent_and_verbose.debug == False - assert silent_and_verbose.verbose == True + assert silent_and_verbose.silent is True + assert silent_and_verbose.debug is False + assert silent_and_verbose.verbose is True baked = silent_and_verbose.bake() - assert baked.silent == True - assert baked.debug == False - assert baked.verbose == False + assert baked.silent is True + assert baked.debug is False + assert baked.verbose is False assert baked.core.logger.log_level == original_log_level baked = silent_and_verbose.bake(scan=scan) assert baked.core.logger.log_level == logging.CRITICAL @@ -407,13 +417,13 @@ async def test_preset_logging(): assert CORE.logger.log_level == original_log_level silent_and_debug = Preset(silent=True, debug=True) - assert silent_and_debug.silent == True - assert silent_and_debug.debug == True - assert silent_and_debug.verbose == False + assert silent_and_debug.silent is True + assert silent_and_debug.debug is True + assert silent_and_debug.verbose is False baked = silent_and_debug.bake() - assert baked.silent == True - assert baked.debug == False - assert baked.verbose == False + assert baked.silent is True + assert baked.debug is False + assert baked.verbose is False assert baked.core.logger.log_level == original_log_level baked = silent_and_debug.bake(scan=scan) assert baked.core.logger.log_level == logging.CRITICAL @@ -423,13 +433,13 @@ async def test_preset_logging(): assert CORE.logger.log_level == original_log_level debug_and_verbose = Preset(verbose=True, debug=True) - assert debug_and_verbose.silent == False - assert debug_and_verbose.debug == True - assert debug_and_verbose.verbose == True + assert debug_and_verbose.silent is False + assert debug_and_verbose.debug is True + assert debug_and_verbose.verbose is True baked = debug_and_verbose.bake() - assert baked.silent == False - assert baked.debug == True - assert baked.verbose == False + assert baked.silent is False + assert baked.debug is True + assert baked.verbose is False assert baked.core.logger.log_level == original_log_level baked = debug_and_verbose.bake(scan=scan) assert baked.core.logger.log_level == logging.DEBUG @@ -439,13 +449,13 @@ async def test_preset_logging(): assert CORE.logger.log_level == original_log_level all_preset = Preset(verbose=True, debug=True, silent=True) - assert all_preset.silent == True - assert all_preset.debug == True - assert all_preset.verbose == True + assert all_preset.silent is True + assert all_preset.debug is True + assert all_preset.verbose is True baked = all_preset.bake() - assert baked.silent == True - assert baked.debug == False - assert baked.verbose == False + assert baked.silent is True + assert baked.debug is False + assert baked.verbose is False assert baked.core.logger.log_level == original_log_level baked = all_preset.bake(scan=scan) assert baked.core.logger.log_level == logging.CRITICAL @@ -675,7 +685,7 @@ class TestModule5(BaseModule): ) preset = Preset.from_yaml_string( - f""" + """ modules: - testmodule5 """ @@ -698,7 +708,6 @@ class TestModule5(BaseModule): def test_preset_include(): - # test recursive preset inclusion custom_preset_dir_1 = bbot_test_dir / "custom_preset_dir" @@ -870,7 +879,6 @@ def test_preset_module_disablement(clean_default_config): def test_preset_require_exclude(): - def get_module_flags(p): for m in p.scan_modules: preloaded = p.preloaded_module(m) @@ -883,9 +891,9 @@ def get_module_flags(p): dnsbrute_flags = preset.preloaded_module("dnsbrute").get("flags", []) assert "subdomain-enum" in dnsbrute_flags assert "active" in dnsbrute_flags - assert not "passive" in dnsbrute_flags + assert "passive" not in dnsbrute_flags assert "aggressive" in dnsbrute_flags - assert not "safe" in dnsbrute_flags + assert "safe" not in dnsbrute_flags assert "dnsbrute" in [x[0] for x in module_flags] assert "certspotter" in [x[0] for x in module_flags] assert "c99" in [x[0] for x in module_flags] @@ -899,7 +907,7 @@ def get_module_flags(p): assert len(preset.modules) > 25 module_flags = list(get_module_flags(preset)) assert "chaos" in [x[0] for x in module_flags] - assert not "httpx" in [x[0] for x in module_flags] + assert "httpx" not in [x[0] for x in module_flags] assert all("passive" in flags for module, flags in module_flags) assert not any("active" in flags for module, flags in module_flags) assert any("safe" in flags for module, flags in module_flags) @@ -910,7 +918,7 @@ def get_module_flags(p): assert len(preset.modules) > 25 module_flags = list(get_module_flags(preset)) assert "chaos" in [x[0] for x in module_flags] - assert not "httpx" in [x[0] for x in module_flags] + assert "httpx" not in [x[0] for x in module_flags] assert all("passive" in flags for module, flags in module_flags) assert not any("active" in flags for module, flags in module_flags) assert any("safe" in flags for module, flags in module_flags) @@ -920,7 +928,7 @@ def get_module_flags(p): preset = Preset(flags=["subdomain-enum"], exclude_modules=["dnsbrute"]).bake() assert len(preset.modules) > 25 module_flags = list(get_module_flags(preset)) - assert not "dnsbrute" in [x[0] for x in module_flags] + assert "dnsbrute" not in [x[0] for x in module_flags] assert "httpx" in [x[0] for x in module_flags] assert any("passive" in flags for module, flags in module_flags) assert any("active" in flags for module, flags in module_flags) @@ -931,7 +939,7 @@ def get_module_flags(p): preset = Preset(flags=["subdomain-enum"], require_flags=["safe", "passive"]).bake() assert len(preset.modules) > 25 module_flags = list(get_module_flags(preset)) - assert not "dnsbrute" in [x[0] for x in module_flags] + assert "dnsbrute" not in [x[0] for x in module_flags] assert all("passive" in flags and "safe" in flags for module, flags in module_flags) assert all("active" not in flags and "aggressive" not in flags for module, flags in module_flags) assert not any("active" in flags for module, flags in module_flags) @@ -941,7 +949,7 @@ def get_module_flags(p): preset = Preset(flags=["subdomain-enum"], exclude_flags=["aggressive", "active"]).bake() assert len(preset.modules) > 25 module_flags = list(get_module_flags(preset)) - assert not "dnsbrute" in [x[0] for x in module_flags] + assert "dnsbrute" not in [x[0] for x in module_flags] assert all("passive" in flags and "safe" in flags for module, flags in module_flags) assert all("active" not in flags and "aggressive" not in flags for module, flags in module_flags) assert not any("active" in flags for module, flags in module_flags) @@ -951,9 +959,9 @@ def get_module_flags(p): preset = Preset(flags=["subdomain-enum"], exclude_modules=["dnsbrute", "c99"]).bake() assert len(preset.modules) > 25 module_flags = list(get_module_flags(preset)) - assert not "dnsbrute" in [x[0] for x in module_flags] + assert "dnsbrute" not in [x[0] for x in module_flags] assert "certspotter" in [x[0] for x in module_flags] - assert not "c99" in [x[0] for x in module_flags] + assert "c99" not in [x[0] for x in module_flags] assert any("passive" in flags for module, flags in module_flags) assert any("active" in flags for module, flags in module_flags) assert any("safe" in flags for module, flags in module_flags) diff --git a/bbot/test/test_step_1/test_python_api.py b/bbot/test/test_step_1/test_python_api.py index 60ab892861..eaa9636b1c 100644 --- a/bbot/test/test_step_1/test_python_api.py +++ b/bbot/test/test_step_1/test_python_api.py @@ -84,6 +84,10 @@ def test_python_api_sync(): def test_python_api_validation(): from bbot.scanner import Scanner, Preset + # invalid target + with pytest.raises(ValidationError) as error: + Scanner("asdf:::asdf") + assert str(error.value) == 'Unable to autodetect event type from "asdf:::asdf"' # invalid module with pytest.raises(ValidationError) as error: Scanner(modules=["asdf"]) diff --git a/bbot/test/test_step_1/test_regexes.py b/bbot/test/test_step_1/test_regexes.py index 77ccc987e2..dbd8dce2b0 100644 --- a/bbot/test/test_step_1/test_regexes.py +++ b/bbot/test/test_step_1/test_regexes.py @@ -91,7 +91,7 @@ def test_ip_regexes(): ip == "2001:db8::1/128" and event_type == "IP_RANGE" ), f"Event type for IP_ADDRESS {ip} was not properly detected" else: - matches = list(r.match(ip) for r in ip_address_regexes) + matches = [r.match(ip) for r in ip_address_regexes] assert any(matches), f"Good IP ADDRESS {ip} did not match regexes" @@ -138,7 +138,7 @@ def test_ip_range_regexes(): pytest.fail(f"BAD IP_RANGE: {bad_ip_range} raised unknown error: {e}: {traceback.format_exc()}") for good_ip_range in good_ip_ranges: - matches = list(r.match(good_ip_range) for r in ip_range_regexes) + matches = [r.match(good_ip_range) for r in ip_range_regexes] assert any(matches), f"Good IP_RANGE {good_ip_range} did not match regexes" @@ -191,7 +191,7 @@ def test_dns_name_regexes(): pytest.fail(f"BAD DNS NAME: {dns} raised unknown error: {e}") for dns in good_dns: - matches = list(r.match(dns) for r in dns_name_regexes) + matches = [r.match(dns) for r in dns_name_regexes] assert any(matches), f"Good DNS_NAME {dns} did not match regexes" event_type, _ = get_event_type(dns) if not event_type == "DNS_NAME": @@ -253,7 +253,7 @@ def test_open_port_regexes(): pytest.fail(f"BAD OPEN_TCP_PORT: {open_port} raised unknown error: {e}") for open_port in good_ports: - matches = list(r.match(open_port) for r in open_port_regexes) + matches = [r.match(open_port) for r in open_port_regexes] assert any(matches), f"Good OPEN_TCP_PORT {open_port} did not match regexes" event_type, _ = get_event_type(open_port) assert event_type == "OPEN_TCP_PORT" @@ -267,7 +267,6 @@ def test_url_regexes(): "http:///evilcorp.com", "http:// evilcorp.com", "http://evilcorp com", - "http://evilcorp.", "http://.com", "evilcorp.com", "http://ex..ample.com", @@ -288,6 +287,7 @@ def test_url_regexes(): good_urls = [ "https://evilcorp.com", + "http://evilcorp.", "https://asdf.www.evilcorp.com", "https://asdf.www-test.evilcorp.com", "https://a.www-test.evilcorp.c", @@ -318,7 +318,7 @@ def test_url_regexes(): pytest.fail(f"BAD URL: {bad_url} raised unknown error: {e}: {traceback.format_exc()}") for good_url in good_urls: - matches = list(r.match(good_url) for r in url_regexes) + matches = [r.match(good_url) for r in url_regexes] assert any(matches), f"Good URL {good_url} did not match regexes" assert ( get_event_type(good_url)[0] == "URL_UNVERIFIED" diff --git a/bbot/test/test_step_1/test_scan.py b/bbot/test/test_step_1/test_scan.py index 3f80807afa..0102590461 100644 --- a/bbot/test/test_step_1/test_scan.py +++ b/bbot/test/test_step_1/test_scan.py @@ -1,3 +1,5 @@ +from ipaddress import ip_network + from ..bbot_fixtures import * @@ -12,6 +14,7 @@ async def test_scan( "1.1.1.0", "1.1.1.1/31", "evilcorp.com", + "test.evilcorp.com", blacklist=["1.1.1.1/28", "www.evilcorp.com"], modules=["ipneighbor"], ) @@ -31,8 +34,11 @@ async def test_scan( assert not scan0.in_scope("test.www.evilcorp.com") assert not scan0.in_scope("www.evilcorp.co.uk") j = scan0.json - assert set(j["target"]["seeds"]) == {"1.1.1.0", "1.1.1.0/31", "evilcorp.com"} - assert set(j["target"]["whitelist"]) == {"1.1.1.0/31", "evilcorp.com"} + assert set(j["target"]["seeds"]) == {"1.1.1.0", "1.1.1.0/31", "evilcorp.com", "test.evilcorp.com"} + # we preserve the original whitelist inputs + assert set(j["target"]["whitelist"]) == {"1.1.1.0", "1.1.1.0/31", "evilcorp.com", "test.evilcorp.com"} + # but in the background they are collapsed + assert scan0.target.whitelist.hosts == {ip_network("1.1.1.0/31"), "evilcorp.com"} assert set(j["target"]["blacklist"]) == {"1.1.1.0/28", "www.evilcorp.com"} assert "ipneighbor" in j["preset"]["modules"] @@ -94,13 +100,13 @@ async def test_url_extension_handling(bbot_scanner): assert "blacklisted" not in bad_event.tags assert "httpx-only" not in httpx_event.tags result = await scan.ingress_module.handle_event(good_event) - assert result == None + assert result is None result, reason = await scan.ingress_module.handle_event(bad_event) - assert result == False + assert result is False assert reason == "event is blacklisted" assert "blacklisted" in bad_event.tags result = await scan.ingress_module.handle_event(httpx_event) - assert result == None + assert result is None assert "httpx-only" in httpx_event.tags await scan._cleanup() @@ -138,7 +144,7 @@ async def test_python_output_matches_json(bbot_scanner): assert len(events) == 5 scan_events = [e for e in events if e["type"] == "SCAN"] assert len(scan_events) == 2 - assert all([isinstance(e["data"]["status"], str) for e in scan_events]) + assert all(isinstance(e["data"]["status"], str) for e in scan_events) assert len([e for e in events if e["type"] == "DNS_NAME"]) == 1 assert len([e for e in events if e["type"] == "ORG_STUB"]) == 1 assert len([e for e in events if e["type"] == "IP_ADDRESS"]) == 1 diff --git a/bbot/test/test_step_1/test_target.py b/bbot/test/test_step_1/test_target.py index 5b974bd452..4bc269595a 100644 --- a/bbot/test/test_step_1/test_target.py +++ b/bbot/test/test_step_1/test_target.py @@ -2,40 +2,32 @@ @pytest.mark.asyncio -async def test_target(bbot_scanner): - import random +async def test_target_basic(bbot_scanner): + from radixtarget import RadixTarget from ipaddress import ip_address, ip_network - from bbot.scanner.target import Target, BBOTTarget + from bbot.scanner.target import BBOTTarget, ScanSeeds scan1 = bbot_scanner("api.publicapis.org", "8.8.8.8/30", "2001:4860:4860::8888/126") scan2 = bbot_scanner("8.8.8.8/29", "publicapis.org", "2001:4860:4860::8888/125") scan3 = bbot_scanner("8.8.8.8/29", "publicapis.org", "2001:4860:4860::8888/125") scan4 = bbot_scanner("8.8.8.8/29") scan5 = bbot_scanner() - assert not scan5.target - assert len(scan1.target) == 9 - assert len(scan4.target) == 8 - assert "8.8.8.9" in scan1.target - assert "8.8.8.12" not in scan1.target - assert "8.8.8.8/31" in scan1.target - assert "8.8.8.8/30" in scan1.target - assert "8.8.8.8/29" not in scan1.target - assert "2001:4860:4860::8889" in scan1.target - assert "2001:4860:4860::888c" not in scan1.target - assert "www.api.publicapis.org" in scan1.target - assert "api.publicapis.org" in scan1.target - assert "publicapis.org" not in scan1.target - assert "bob@www.api.publicapis.org" in scan1.target - assert "https://www.api.publicapis.org" in scan1.target - assert "www.api.publicapis.org:80" in scan1.target - assert scan1.make_event("https://[2001:4860:4860::8888]:80", dummy=True) in scan1.target - assert scan1.make_event("[2001:4860:4860::8888]:80", "OPEN_TCP_PORT", dummy=True) in scan1.target - assert scan1.make_event("[2001:4860:4860::888c]:80", "OPEN_TCP_PORT", dummy=True) not in scan1.target - assert scan1.target in scan2.target - assert scan2.target not in scan1.target - assert scan3.target in scan2.target - assert scan2.target == scan3.target - assert scan4.target != scan1.target + + # test different types of inputs + target = BBOTTarget("evilcorp.com", "1.2.3.4/8") + assert "www.evilcorp.com" in target.seeds + assert "www.evilcorp.com:80" in target.seeds + assert "http://www.evilcorp.com:80" in target.seeds + assert "1.2.3.4" in target.seeds + assert "1.2.3.4/24" in target.seeds + assert ip_address("1.2.3.4") in target.seeds + assert ip_network("1.2.3.4/24", strict=False) in target.seeds + event = scan1.make_event("https://www.evilcorp.com:80", dummy=True) + assert event in target.seeds + with pytest.raises(ValueError): + ["asdf"] in target.seeds + with pytest.raises(ValueError): + target.seeds.get(["asdf"]) assert not scan5.target.seeds assert len(scan1.target.seeds) == 9 @@ -56,6 +48,36 @@ async def test_target(bbot_scanner): assert scan1.make_event("https://[2001:4860:4860::8888]:80", dummy=True) in scan1.target.seeds assert scan1.make_event("[2001:4860:4860::8888]:80", "OPEN_TCP_PORT", dummy=True) in scan1.target.seeds assert scan1.make_event("[2001:4860:4860::888c]:80", "OPEN_TCP_PORT", dummy=True) not in scan1.target.seeds + assert scan1.target.seeds in scan2.target.seeds + assert scan2.target.seeds not in scan1.target.seeds + assert scan3.target.seeds in scan2.target.seeds + assert scan2.target.seeds == scan3.target.seeds + assert scan4.target.seeds != scan1.target.seeds + + assert not scan5.target.whitelist + assert len(scan1.target.whitelist) == 9 + assert len(scan4.target.whitelist) == 8 + assert "8.8.8.9" in scan1.target.whitelist + assert "8.8.8.12" not in scan1.target.whitelist + assert "8.8.8.8/31" in scan1.target.whitelist + assert "8.8.8.8/30" in scan1.target.whitelist + assert "8.8.8.8/29" not in scan1.target.whitelist + assert "2001:4860:4860::8889" in scan1.target.whitelist + assert "2001:4860:4860::888c" not in scan1.target.whitelist + assert "www.api.publicapis.org" in scan1.target.whitelist + assert "api.publicapis.org" in scan1.target.whitelist + assert "publicapis.org" not in scan1.target.whitelist + assert "bob@www.api.publicapis.org" in scan1.target.whitelist + assert "https://www.api.publicapis.org" in scan1.target.whitelist + assert "www.api.publicapis.org:80" in scan1.target.whitelist + assert scan1.make_event("https://[2001:4860:4860::8888]:80", dummy=True) in scan1.target.whitelist + assert scan1.make_event("[2001:4860:4860::8888]:80", "OPEN_TCP_PORT", dummy=True) in scan1.target.whitelist + assert scan1.make_event("[2001:4860:4860::888c]:80", "OPEN_TCP_PORT", dummy=True) not in scan1.target.whitelist + assert scan1.target.whitelist in scan2.target.whitelist + assert scan2.target.whitelist not in scan1.target.whitelist + assert scan3.target.whitelist in scan2.target.whitelist + assert scan2.target.whitelist == scan3.target.whitelist + assert scan4.target.whitelist != scan1.target.whitelist assert scan1.whitelisted("https://[2001:4860:4860::8888]:80") assert scan1.whitelisted("[2001:4860:4860::8888]:80") @@ -70,45 +92,58 @@ async def test_target(bbot_scanner): assert scan2.target.seeds == scan3.target.seeds assert scan4.target.seeds != scan1.target.seeds - assert str(scan1.target.get("8.8.8.9").host) == "8.8.8.8/30" - assert scan1.target.get("8.8.8.12") is None - assert str(scan1.target.get("2001:4860:4860::8889").host) == "2001:4860:4860::8888/126" - assert scan1.target.get("2001:4860:4860::888c") is None - assert str(scan1.target.get("www.api.publicapis.org").host) == "api.publicapis.org" - assert scan1.target.get("publicapis.org") is None - - target = Target("evilcorp.com") - assert not "com" in target + assert str(scan1.target.seeds.get("8.8.8.9").host) == "8.8.8.8/30" + assert str(scan1.target.whitelist.get("8.8.8.9").host) == "8.8.8.8/30" + assert scan1.target.seeds.get("8.8.8.12") is None + assert scan1.target.whitelist.get("8.8.8.12") is None + assert str(scan1.target.seeds.get("2001:4860:4860::8889").host) == "2001:4860:4860::8888/126" + assert str(scan1.target.whitelist.get("2001:4860:4860::8889").host) == "2001:4860:4860::8888/126" + assert scan1.target.seeds.get("2001:4860:4860::888c") is None + assert scan1.target.whitelist.get("2001:4860:4860::888c") is None + assert str(scan1.target.seeds.get("www.api.publicapis.org").host) == "api.publicapis.org" + assert str(scan1.target.whitelist.get("www.api.publicapis.org").host) == "api.publicapis.org" + assert scan1.target.seeds.get("publicapis.org") is None + assert scan1.target.whitelist.get("publicapis.org") is None + + target = RadixTarget("evilcorp.com") + assert "com" not in target assert "evilcorp.com" in target assert "www.evilcorp.com" in target - strict_target = Target("evilcorp.com", strict_scope=True) - assert not "com" in strict_target + strict_target = RadixTarget("evilcorp.com", strict_dns_scope=True) + assert "com" not in strict_target assert "evilcorp.com" in strict_target - assert not "www.evilcorp.com" in strict_target + assert "www.evilcorp.com" not in strict_target - target = Target() + target = RadixTarget() target.add("evilcorp.com") - assert not "com" in target + assert "com" not in target assert "evilcorp.com" in target assert "www.evilcorp.com" in target - strict_target = Target(strict_scope=True) + strict_target = RadixTarget(strict_dns_scope=True) strict_target.add("evilcorp.com") - assert not "com" in strict_target + assert "com" not in strict_target assert "evilcorp.com" in strict_target - assert not "www.evilcorp.com" in strict_target + assert "www.evilcorp.com" not in strict_target # test target hashing - target1 = Target() - target1.add("evilcorp.com") - target1.add("1.2.3.4/24") - target1.add("https://evilcorp.net:8080") - - target2 = Target() - target2.add("bob@evilcorp.org") - target2.add("evilcorp.com") - target2.add("1.2.3.4/24") - target2.add("https://evilcorp.net:8080") + target1 = BBOTTarget() + target1.whitelist.add("evilcorp.com") + target1.whitelist.add("1.2.3.4/24") + target1.whitelist.add("https://evilcorp.net:8080") + target1.seeds.add("evilcorp.com") + target1.seeds.add("1.2.3.4/24") + target1.seeds.add("https://evilcorp.net:8080") + + target2 = BBOTTarget() + target2.whitelist.add("bob@evilcorp.org") + target2.whitelist.add("evilcorp.com") + target2.whitelist.add("1.2.3.4/24") + target2.whitelist.add("https://evilcorp.net:8080") + target2.seeds.add("bob@evilcorp.org") + target2.seeds.add("evilcorp.com") + target2.seeds.add("1.2.3.4/24") + target2.seeds.add("https://evilcorp.net:8080") # make sure it's a sha1 hash assert isinstance(target1.hash, bytes) @@ -116,11 +151,22 @@ async def test_target(bbot_scanner): # hashes shouldn't match yet assert target1.hash != target2.hash + assert target1.scope_hash != target2.scope_hash # add missing email - target1.add("bob@evilcorp.org") + target1.whitelist.add("bob@evilcorp.org") + assert target1.hash != target2.hash + assert target1.scope_hash == target2.scope_hash + target1.seeds.add("bob@evilcorp.org") # now they should match assert target1.hash == target2.hash + # test default whitelist + bbottarget = BBOTTarget("http://1.2.3.4:8443", "bob@evilcorp.com") + assert bbottarget.seeds.hosts == {ip_network("1.2.3.4"), "evilcorp.com"} + assert bbottarget.whitelist.hosts == {ip_network("1.2.3.4"), "evilcorp.com"} + assert {e.data for e in bbottarget.seeds.events} == {"http://1.2.3.4:8443/", "bob@evilcorp.com"} + assert {e.data for e in bbottarget.whitelist.events} == {"1.2.3.4", "evilcorp.com"} + bbottarget1 = BBOTTarget("evilcorp.com", "evilcorp.net", whitelist=["1.2.3.4/24"], blacklist=["1.2.3.4"]) bbottarget2 = BBOTTarget("evilcorp.com", "evilcorp.net", whitelist=["1.2.3.0/24"], blacklist=["1.2.3.4"]) bbottarget3 = BBOTTarget("evilcorp.com", whitelist=["1.2.3.4/24"], blacklist=["1.2.3.4"]) @@ -137,14 +183,23 @@ async def test_target(bbot_scanner): assert bbottarget1 == bbottarget2 assert bbottarget2 == bbottarget1 + # 1 and 3 have different seeds assert bbottarget1 != bbottarget3 assert bbottarget3 != bbottarget1 - bbottarget3.add("evilcorp.net") + # until we make them the same + bbottarget3.seeds.add("evilcorp.net") assert bbottarget1 == bbottarget3 assert bbottarget3 == bbottarget1 - bbottarget1.add("http://evilcorp.co.nz") - bbottarget2.add("evilcorp.co.nz") + # adding different events (but with same host) to whitelist should not change hash (since only hosts matter) + bbottarget1.whitelist.add("http://evilcorp.co.nz") + bbottarget2.whitelist.add("evilcorp.co.nz") + assert bbottarget1 == bbottarget2 + assert bbottarget2 == bbottarget1 + + # but seeds should change hash + bbottarget1.seeds.add("http://evilcorp.co.nz") + bbottarget2.seeds.add("evilcorp.co.nz") assert bbottarget1 != bbottarget2 assert bbottarget2 != bbottarget1 @@ -156,15 +211,11 @@ async def test_target(bbot_scanner): assert bbottarget8 != bbottarget9 assert bbottarget9 != bbottarget8 - bbottarget10 = bbottarget9.copy() - assert bbottarget10 == bbottarget9 - assert bbottarget9 == bbottarget10 - # make sure duplicate events don't change hash - target1 = Target("https://evilcorp.com") - target2 = Target("https://evilcorp.com") + target1 = BBOTTarget("https://evilcorp.com") + target2 = BBOTTarget("https://evilcorp.com") assert target1 == target2 - target1.add("https://evilcorp.com:443") + target1.seeds.add("https://evilcorp.com:443") assert target1 == target2 # make sure hosts are collapsed in whitelist and blacklist @@ -173,24 +224,37 @@ async def test_target(bbot_scanner): whitelist=["evilcorp.net:443", "http://evilcorp.net:8080"], blacklist=["http://evilcorp.org:8080", "evilcorp.org:443"], ) - assert list(bbottarget) == ["http://evilcorp.com:8080"] + # base class is not iterable + with pytest.raises(TypeError): + assert list(bbottarget) == ["http://evilcorp.com:8080"] assert list(bbottarget.seeds) == ["http://evilcorp.com:8080"] - assert list(bbottarget.whitelist) == ["evilcorp.net"] - assert list(bbottarget.blacklist) == ["evilcorp.org"] + assert {e.data for e in bbottarget.whitelist} == {"evilcorp.net:443", "http://evilcorp.net:8080/"} + assert {e.data for e in bbottarget.blacklist} == {"http://evilcorp.org:8080/", "evilcorp.org:443"} # test org stub as target for org_target in ("ORG:evilcorp", "ORG_STUB:evilcorp"): scan = bbot_scanner(org_target) events = [e async for e in scan.async_start()] assert len(events) == 3 - assert set([e.type for e in events]) == {"SCAN", "ORG_STUB"} + assert {e.type for e in events} == {"SCAN", "ORG_STUB"} # test username as target for user_target in ("USER:vancerefrigeration", "USERNAME:vancerefrigeration"): scan = bbot_scanner(user_target) events = [e async for e in scan.async_start()] assert len(events) == 3 - assert set([e.type for e in events]) == {"SCAN", "USERNAME"} + assert {e.type for e in events} == {"SCAN", "USERNAME"} + + # users + orgs + domains + scan = bbot_scanner("USER:evilcorp", "ORG:evilcorp", "evilcorp.com") + await scan.helpers.dns._mock_dns( + { + "evilcorp.com": {"A": ["1.2.3.4"]}, + }, + ) + events = [e async for e in scan.async_start()] + assert len(events) == 5 + assert {e.type for e in events} == {"SCAN", "USERNAME", "ORG_STUB", "DNS_NAME"} # verify hash values bbottarget = BBOTTarget( @@ -200,21 +264,30 @@ async def test_target(bbot_scanner): whitelist=["evilcorp.com", "bob@www.evilcorp.com", "evilcorp.net"], blacklist=["1.2.3.4", "4.3.2.1/24", "http://1.2.3.4", "bob@asdf.evilcorp.net"], ) - assert set([e.data for e in bbottarget.seeds.events]) == { + assert {e.data for e in bbottarget.seeds.events} == { "1.2.3.0/24", "http://www.evilcorp.net/", "bob@fdsa.evilcorp.net", } - assert set([e.data for e in bbottarget.whitelist.events]) == {"evilcorp.com", "evilcorp.net"} - assert set([e.data for e in bbottarget.blacklist.events]) == {"1.2.3.4", "4.3.2.0/24", "asdf.evilcorp.net"} + assert {e.data for e in bbottarget.whitelist.events} == { + "evilcorp.com", + "evilcorp.net", + "bob@www.evilcorp.com", + } + assert {e.data for e in bbottarget.blacklist.events} == { + "1.2.3.4", + "4.3.2.0/24", + "http://1.2.3.4/", + "bob@asdf.evilcorp.net", + } assert set(bbottarget.seeds.hosts) == {ip_network("1.2.3.0/24"), "www.evilcorp.net", "fdsa.evilcorp.net"} assert set(bbottarget.whitelist.hosts) == {"evilcorp.com", "evilcorp.net"} - assert set(bbottarget.blacklist.hosts) == {ip_address("1.2.3.4"), ip_network("4.3.2.0/24"), "asdf.evilcorp.net"} - assert bbottarget.hash == b"\x0b\x908\xe3\xef\n=\x13d\xdf\x00;\xack\x0c\xbc\xd2\xcc'\xba" - assert bbottarget.scope_hash == b"\x00\xf5V\xfb.\xeb#\xcb\xf0q\xf9\xe9e\xb7\x1f\xe2T+\xdbw" - assert bbottarget.seeds.hash == b"\xaf.\x86\x83\xa1C\xad\xb4\xe7`X\x94\xe2\xa0\x01\xc2\xe3:J\xc5" - assert bbottarget.whitelist.hash == b"\xa0Af\x07n\x10\xd9\xb6\n\xa7TO\xb07\xcdW\xc4vLC" - assert bbottarget.blacklist.hash == b"\xaf\x0e\x8a\xe9JZ\x86\xbe\xee\xa9\xa9\xdb0\xaf'#\x84 U/" + assert set(bbottarget.blacklist.hosts) == {ip_network("1.2.3.4/32"), ip_network("4.3.2.0/24"), "asdf.evilcorp.net"} + assert bbottarget.hash == b"\xb3iU\xa8#\x8aq\x84/\xc5\xf2;\x11\x11\x0c&\xea\x07\xd4Q" + assert bbottarget.scope_hash == b"f\xe1\x01c^3\xf5\xd24B\x87P\xa0Glq0p3J" + assert bbottarget.seeds.hash == b"V\n\xf5\x1d\x1f=i\xbc\\\x15o\xc2p\xb2\x84\x97\xfeR\xde\xc1" + assert bbottarget.whitelist.hash == b"\x8e\xd0\xa76\x8em4c\x0e\x1c\xfdA\x9d*sv}\xeb\xc4\xc4" + assert bbottarget.blacklist.hash == b'\xf7\xaf\xa1\xda4"C:\x13\xf42\xc3,\xc3\xa9\x9f\x15\x15n\\' scan = bbot_scanner( "http://www.evilcorp.net", @@ -227,83 +300,119 @@ async def test_target(bbot_scanner): scan_events = [e for e in events if e.type == "SCAN"] assert len(scan_events) == 2 target_dict = scan_events[0].data["target"] - assert target_dict["strict_scope"] == False - assert target_dict["hash"] == b"\x0b\x908\xe3\xef\n=\x13d\xdf\x00;\xack\x0c\xbc\xd2\xcc'\xba".hex() - assert target_dict["scope_hash"] == b"\x00\xf5V\xfb.\xeb#\xcb\xf0q\xf9\xe9e\xb7\x1f\xe2T+\xdbw".hex() - assert target_dict["seed_hash"] == b"\xaf.\x86\x83\xa1C\xad\xb4\xe7`X\x94\xe2\xa0\x01\xc2\xe3:J\xc5".hex() - assert target_dict["whitelist_hash"] == b"\xa0Af\x07n\x10\xd9\xb6\n\xa7TO\xb07\xcdW\xc4vLC".hex() - assert target_dict["blacklist_hash"] == b"\xaf\x0e\x8a\xe9JZ\x86\xbe\xee\xa9\xa9\xdb0\xaf'#\x84 U/".hex() - assert target_dict["hash"] == "0b9038e3ef0a3d1364df003bac6b0cbcd2cc27ba" - assert target_dict["scope_hash"] == "00f556fb2eeb23cbf071f9e965b71fe2542bdb77" - assert target_dict["seed_hash"] == "af2e8683a143adb4e7605894e2a001c2e33a4ac5" - assert target_dict["whitelist_hash"] == "a04166076e10d9b60aa7544fb037cd57c4764c43" - assert target_dict["blacklist_hash"] == "af0e8ae94a5a86beeea9a9db30af27238420552f" - - # test target sorting - big_subnet = scan.make_event("1.2.3.4/24", dummy=True) - medium_subnet = scan.make_event("1.2.3.4/28", dummy=True) - small_subnet = scan.make_event("1.2.3.4/30", dummy=True) - ip_event = scan.make_event("1.2.3.4", dummy=True) - parent_domain = scan.make_event("evilcorp.com", dummy=True) - grandparent_domain = scan.make_event("www.evilcorp.com", dummy=True) - greatgrandparent_domain = scan.make_event("api.www.evilcorp.com", dummy=True) - target = Target() - assert big_subnet._host_size == -256 - assert medium_subnet._host_size == -16 - assert small_subnet._host_size == -4 - assert ip_event._host_size == 1 - assert parent_domain._host_size == 12 - assert grandparent_domain._host_size == 16 - assert greatgrandparent_domain._host_size == 20 - events = [ - big_subnet, - medium_subnet, - small_subnet, - ip_event, - parent_domain, - grandparent_domain, - greatgrandparent_domain, - ] - random.shuffle(events) - assert target._sort_events(events) == [ - big_subnet, - medium_subnet, - small_subnet, - ip_event, - parent_domain, - grandparent_domain, - greatgrandparent_domain, - ] + + assert target_dict["seeds"] == ["1.2.3.0/24", "bob@fdsa.evilcorp.net", "http://www.evilcorp.net/"] + assert target_dict["whitelist"] == ["bob@www.evilcorp.com", "evilcorp.com", "evilcorp.net"] + assert target_dict["blacklist"] == ["1.2.3.4", "4.3.2.0/24", "bob@asdf.evilcorp.net", "http://1.2.3.4/"] + assert target_dict["strict_scope"] is False + assert target_dict["hash"] == "b36955a8238a71842fc5f23b11110c26ea07d451" + assert target_dict["seed_hash"] == "560af51d1f3d69bc5c156fc270b28497fe52dec1" + assert target_dict["whitelist_hash"] == "8ed0a7368e6d34630e1cfd419d2a73767debc4c4" + assert target_dict["blacklist_hash"] == "f7afa1da3422433a13f432c32cc3a99f15156e5c" + assert target_dict["scope_hash"] == "66e101635e33f5d234428750a0476c713070334a" # make sure child subnets/IPs don't get added to whitelist/blacklist - target = Target("1.2.3.4/24", "1.2.3.4/28", acl_mode=True) - assert set(e.data for e in target) == {"1.2.3.0/24"} - target = Target("1.2.3.4/28", "1.2.3.4/24", acl_mode=True) - assert set(e.data for e in target) == {"1.2.3.0/24"} - target = Target("1.2.3.4/28", "1.2.3.4", acl_mode=True) - assert set(e.data for e in target) == {"1.2.3.0/28"} - target = Target("1.2.3.4", "1.2.3.4/28", acl_mode=True) - assert set(e.data for e in target) == {"1.2.3.0/28"} + target = RadixTarget("1.2.3.4/24", "1.2.3.4/28", acl_mode=True) + assert set(target) == {ip_network("1.2.3.0/24")} + target = RadixTarget("1.2.3.4/28", "1.2.3.4/24", acl_mode=True) + assert set(target) == {ip_network("1.2.3.0/24")} + target = RadixTarget("1.2.3.4/28", "1.2.3.4", acl_mode=True) + assert set(target) == {ip_network("1.2.3.0/28")} + target = RadixTarget("1.2.3.4", "1.2.3.4/28", acl_mode=True) + assert set(target) == {ip_network("1.2.3.0/28")} # same but for domains - target = Target("evilcorp.com", "www.evilcorp.com", acl_mode=True) - assert set(e.data for e in target) == {"evilcorp.com"} - target = Target("www.evilcorp.com", "evilcorp.com", acl_mode=True) - assert set(e.data for e in target) == {"evilcorp.com"} + target = RadixTarget("evilcorp.com", "www.evilcorp.com", acl_mode=True) + assert set(target) == {"evilcorp.com"} + target = RadixTarget("www.evilcorp.com", "evilcorp.com", acl_mode=True) + assert set(target) == {"evilcorp.com"} # make sure strict_scope doesn't mess us up - target = Target("evilcorp.co.uk", "www.evilcorp.co.uk", acl_mode=True, strict_scope=True) + target = RadixTarget("evilcorp.co.uk", "www.evilcorp.co.uk", acl_mode=True, strict_dns_scope=True) assert set(target.hosts) == {"evilcorp.co.uk", "www.evilcorp.co.uk"} assert "evilcorp.co.uk" in target assert "www.evilcorp.co.uk" in target - assert not "api.evilcorp.co.uk" in target - assert not "api.www.evilcorp.co.uk" in target + assert "api.evilcorp.co.uk" not in target + assert "api.www.evilcorp.co.uk" not in target # test 'single' boolean argument - target = Target("http://evilcorp.com", "evilcorp.com:443") + target = ScanSeeds("http://evilcorp.com", "evilcorp.com:443") assert "www.evilcorp.com" in target + assert "bob@evilcorp.com" in target event = target.get("www.evilcorp.com") assert event.host == "evilcorp.com" events = target.get("www.evilcorp.com", single=False) assert len(events) == 2 - assert set([e.data for e in events]) == {"http://evilcorp.com/", "evilcorp.com:443"} + assert {e.data for e in events} == {"http://evilcorp.com/", "evilcorp.com:443"} + + +@pytest.mark.asyncio +async def test_blacklist_regex(bbot_scanner, bbot_httpserver): + from bbot.scanner.target import ScanBlacklist + + blacklist = ScanBlacklist("evilcorp.com") + assert blacklist.inputs == {"evilcorp.com"} + assert "www.evilcorp.com" in blacklist + assert "http://www.evilcorp.com" in blacklist + blacklist.add("RE:test") + assert "RE:test" in blacklist.inputs + assert set(blacklist.inputs) == {"evilcorp.com", "RE:test"} + assert blacklist.blacklist_regexes + assert next(iter(blacklist.blacklist_regexes)).pattern == "test" + result1 = blacklist.get("test.com") + assert result1.type == "DNS_NAME" + assert result1.data == "test.com" + result2 = blacklist.get("www.evilcorp.com") + assert result2.type == "DNS_NAME" + assert result2.data == "evilcorp.com" + result2 = blacklist.get("www.evil.com") + assert result2 is None + with pytest.raises(KeyError): + blacklist.get("www.evil.com", raise_error=True) + assert "test.com" in blacklist + assert "http://evilcorp.com/test.aspx" in blacklist + assert "http://tes.com" not in blacklist + + blacklist = ScanBlacklist("evilcorp.com", r"RE:[0-9]{6}\.aspx$") + assert "http://evilcorp.com" in blacklist + assert "http://test.com/123456" not in blacklist + assert "http://test.com/12345.aspx?a=asdf" not in blacklist + assert "http://test.com/asdf/123456.aspx/asdf" not in blacklist + assert "http://test.com/asdf/123456.aspx?a=asdf" in blacklist + assert "http://test.com/asdf/123456.aspx" in blacklist + + bbot_httpserver.expect_request(uri="/").respond_with_data( + """ + + + """ + ) + bbot_httpserver.expect_request(uri="/asdfevilasdf").respond_with_data("") + bbot_httpserver.expect_request(uri="/logout.aspx").respond_with_data("") + + # make sure URL is detected normally + scan = bbot_scanner("http://127.0.0.1:8888/", presets=["spider"], config={"excavate": True}, debug=True) + assert {r.pattern for r in scan.target.blacklist.blacklist_regexes} == {r"/.*(sign|log)[_-]?out"} + events = [e async for e in scan.async_start()] + urls = [e.data for e in events if e.type == "URL"] + assert len(urls) == 2 + assert set(urls) == {"http://127.0.0.1:8888/", "http://127.0.0.1:8888/asdfevil333asdf"} + + # same scan again but with blacklist regex + scan = bbot_scanner( + "http://127.0.0.1:8888/", + blacklist=[r"RE:evil[0-9]{3}"], + presets=["spider"], + config={"excavate": True}, + debug=True, + ) + assert len(scan.target.blacklist) == 2 + assert scan.target.blacklist.blacklist_regexes + assert {r.pattern for r in scan.target.blacklist.blacklist_regexes} == { + r"evil[0-9]{3}", + r"/.*(sign|log)[_-]?out", + } + events = [e async for e in scan.async_start()] + urls = [e.data for e in events if e.type == "URL"] + assert len(urls) == 1 + assert set(urls) == {"http://127.0.0.1:8888/"} diff --git a/bbot/test/test_step_1/test_web.py b/bbot/test/test_step_1/test_web.py index 4d5654c86b..e07ed3d7d4 100644 --- a/bbot/test/test_step_1/test_web.py +++ b/bbot/test/test_step_1/test_web.py @@ -6,7 +6,6 @@ @pytest.mark.asyncio async def test_web_engine(bbot_scanner, bbot_httpserver, httpx_mock): - from werkzeug.wrappers import Response def server_handler(request): @@ -29,7 +28,7 @@ def server_handler(request): urls = [f"{base_url}{i}" for i in range(num_urls)] responses = [r async for r in scan.helpers.request_batch(urls)] assert len(responses) == 100 - assert all([r[1].status_code == 200 and r[1].text.startswith(f"{r[0]}: ") for r in responses]) + assert all(r[1].status_code == 200 and r[1].text.startswith(f"{r[0]}: ") for r in responses) # request_batch w/ cancellation agen = scan.helpers.request_batch(urls) @@ -134,7 +133,6 @@ def server_handler(request): @pytest.mark.asyncio async def test_web_helpers(bbot_scanner, bbot_httpserver, httpx_mock): - # json conversion scan = bbot_scanner("evilcorp.com") url = "http://www.evilcorp.com/json_test?a=b" @@ -211,14 +209,14 @@ async def test_web_helpers(bbot_scanner, bbot_httpserver, httpx_mock): url = bbot_httpserver.url_for(path) bbot_httpserver.expect_request(uri=path).respond_with_data(download_content, status=200) webpage = await scan1.helpers.request(url) - assert webpage, f"Webpage is False" + assert webpage, "Webpage is False" soup = scan1.helpers.beautifulsoup(webpage, "html.parser") - assert soup, f"Soup is False" + assert soup, "Soup is False" # pretty_print = soup.prettify() # assert pretty_print, f"PrettyPrint is False" # scan1.helpers.log.info(f"{pretty_print}") html_text = soup.find(text="Example Domain") - assert html_text, f"Find HTML Text is False" + assert html_text, "Find HTML Text is False" # 404 path = "/test_http_helpers_download_404" @@ -389,7 +387,7 @@ async def test_web_http_compare(httpx_mock, bbot_scanner): await compare_helper.compare("http://www.example.com", check_reflection=True) compare_helper.compare_body({"asdf": "fdsa"}, {"fdsa": "asdf"}) for mode in ("getparam", "header", "cookie"): - assert await compare_helper.canary_check("http://www.example.com", mode=mode) == True + assert await compare_helper.canary_check("http://www.example.com", mode=mode) is True await scan._cleanup() @@ -471,6 +469,9 @@ async def test_web_cookies(bbot_scanner, httpx_mock): # but that they're not sent in the response with pytest.raises(httpx.TimeoutException): r = await client2.get(url="http://www2.evilcorp.com/cookies/test") + # make sure cookies are sent + r = await client2.get(url="http://www2.evilcorp.com/cookies/test", cookies={"wats": "fdsa"}) + assert r.status_code == 200 # make sure we can manually send cookies httpx_mock.add_response(url="http://www2.evilcorp.com/cookies/test2", match_headers={"Cookie": "fdsa=wats"}) r = await client2.get(url="http://www2.evilcorp.com/cookies/test2", cookies={"fdsa": "wats"}) diff --git a/bbot/test/test_step_2/module_tests/base.py b/bbot/test/test_step_2/module_tests/base.py index bb63b57e5b..3f6b5dd768 100644 --- a/bbot/test/test_step_2/module_tests/base.py +++ b/bbot/test/test_step_2/module_tests/base.py @@ -20,6 +20,8 @@ class ModuleTestBase: config_overrides = {} modules_overrides = None log = logging.getLogger("bbot") + # if True, the test will be skipped (useful for tests that require docker) + skip_distro_tests = False class ModuleTest: def __init__( @@ -90,36 +92,38 @@ async def module_test( self, httpx_mock, bbot_httpserver, bbot_httpserver_ssl, monkeypatch, request, caplog, capsys ): # Skip dastardly test if we're in the distro tests (because dastardly uses docker) - if os.getenv("BBOT_DISTRO_TESTS") and self.name == "dastardly": + if os.getenv("BBOT_DISTRO_TESTS") and self.skip_distro_tests: pytest.skip("Skipping module_test for dastardly module due to BBOT_DISTRO_TESTS environment variable") self.log.info(f"Starting {self.name} module test") module_test = self.ModuleTest( self, httpx_mock, bbot_httpserver, bbot_httpserver_ssl, monkeypatch, request, caplog, capsys ) - self.log.debug(f"Mocking DNS") + self.log.debug("Mocking DNS") await module_test.mock_dns({"blacklanternsecurity.com": {"A": ["127.0.0.88"]}}) - self.log.debug(f"Executing setup_before_prep()") + self.log.debug("Executing setup_before_prep()") await self.setup_before_prep(module_test) - self.log.debug(f"Executing scan._prep()") + self.log.debug("Executing scan._prep()") await module_test.scan._prep() - self.log.debug(f"Executing setup_after_prep()") + self.log.debug("Executing setup_after_prep()") await self.setup_after_prep(module_test) - self.log.debug(f"Starting scan") + self.log.debug("Starting scan") module_test.events = [e async for e in module_test.scan.async_start()] self.log.debug(f"Finished {module_test.name} module test") yield module_test @pytest.mark.asyncio async def test_module_run(self, module_test): - self.check(module_test, module_test.events) + from bbot.core.helpers.misc import execute_sync_or_async + + await execute_sync_or_async(self.check, module_test, module_test.events) module_test.log.info(f"Finished {self.name} module test") current_task = asyncio.current_task() tasks = [t for t in asyncio.all_tasks() if t != current_task] if len(tasks): module_test.log.info(f"Unfinished tasks detected: {tasks}") else: - module_test.log.info(f"No unfinished tasks detected") + module_test.log.info("No unfinished tasks detected") def check(self, module_test, events): assert False, f"Must override {self.name}.check()" diff --git a/bbot/test/test_step_2/module_tests/test_module_anubisdb.py b/bbot/test/test_step_2/module_tests/test_module_anubisdb.py index dbebf86212..7b1bc6659d 100644 --- a/bbot/test/test_step_2/module_tests/test_module_anubisdb.py +++ b/bbot/test/test_step_2/module_tests/test_module_anubisdb.py @@ -5,7 +5,7 @@ class TestAnubisdb(ModuleTestBase): async def setup_after_prep(self, module_test): module_test.module.abort_if = lambda e: False module_test.httpx_mock.add_response( - url=f"https://jldc.me/anubis/subdomains/blacklanternsecurity.com", + url="https://jldc.me/anubis/subdomains/blacklanternsecurity.com", json=["asdf.blacklanternsecurity.com", "zzzz.blacklanternsecurity.com"], ) diff --git a/bbot/test/test_step_2/module_tests/test_module_asset_inventory.py b/bbot/test/test_step_2/module_tests/test_module_asset_inventory.py index 99af7d556b..5cb2f36033 100644 --- a/bbot/test/test_step_2/module_tests/test_module_asset_inventory.py +++ b/bbot/test/test_step_2/module_tests/test_module_asset_inventory.py @@ -10,7 +10,6 @@ class TestAsset_Inventory(ModuleTestBase): masscan_output = """{ "ip": "127.0.0.1", "timestamp": "1680197558", "ports": [ {"port": 9999, "proto": "tcp", "status": "open", "reason": "syn-ack", "ttl": 54} ] }""" async def setup_before_prep(self, module_test): - async def run_masscan(command, *args, **kwargs): if "masscan" in command[:2]: targets = open(command[11]).read().splitlines() diff --git a/bbot/test/test_step_2/module_tests/test_module_azure_realm.py b/bbot/test/test_step_2/module_tests/test_module_azure_realm.py index 557c362ee8..fa06726158 100644 --- a/bbot/test/test_step_2/module_tests/test_module_azure_realm.py +++ b/bbot/test/test_step_2/module_tests/test_module_azure_realm.py @@ -22,7 +22,7 @@ class TestAzure_Realm(ModuleTestBase): async def setup_after_prep(self, module_test): await module_test.mock_dns({"evilcorp.com": {"A": ["127.0.0.5"]}}) module_test.httpx_mock.add_response( - url=f"https://login.microsoftonline.com/getuserrealm.srf?login=test@evilcorp.com", + url="https://login.microsoftonline.com/getuserrealm.srf?login=test@evilcorp.com", json=self.response_json, ) diff --git a/bbot/test/test_step_2/module_tests/test_module_baddns.py b/bbot/test/test_step_2/module_tests/test_module_baddns.py index b21e0d1146..288af7ae79 100644 --- a/bbot/test/test_step_2/module_tests/test_module_baddns.py +++ b/bbot/test/test_step_2/module_tests/test_module_baddns.py @@ -31,9 +31,9 @@ async def setup_after_prep(self, module_test): module_test.monkeypatch.setattr(WhoisManager, "dispatchWHOIS", self.dispatchWHOIS) def check(self, module_test, events): - assert any([e.data == "baddns.azurewebsites.net" for e in events]), "CNAME detection failed" - assert any([e.type == "VULNERABILITY" for e in events]), "Failed to emit VULNERABILITY" - assert any(["baddns-cname" in e.tags for e in events]), "Failed to add baddns tag" + assert any(e.data == "baddns.azurewebsites.net" for e in events), "CNAME detection failed" + assert any(e.type == "VULNERABILITY" for e in events), "Failed to emit VULNERABILITY" + assert any("baddns-cname" in e.tags for e in events), "Failed to add baddns tag" class TestBaddns_cname_signature(BaseTestBaddns): @@ -60,8 +60,8 @@ def set_target(self, target): module_test.monkeypatch.setattr(WhoisManager, "dispatchWHOIS", self.dispatchWHOIS) def check(self, module_test, events): - assert any([e for e in events]) + assert any(e for e in events) assert any( - [e.type == "VULNERABILITY" and "bigcartel.com" in e.data["description"] for e in events] + e.type == "VULNERABILITY" and "bigcartel.com" in e.data["description"] for e in events ), "Failed to emit VULNERABILITY" - assert any(["baddns-cname" in e.tags for e in events]), "Failed to add baddns tag" + assert any("baddns-cname" in e.tags for e in events), "Failed to add baddns tag" diff --git a/bbot/test/test_step_2/module_tests/test_module_baddns_direct.py b/bbot/test/test_step_2/module_tests/test_module_baddns_direct.py index 596d3c89e7..b2b49717c8 100644 --- a/bbot/test/test_step_2/module_tests/test_module_baddns_direct.py +++ b/bbot/test/test_step_2/module_tests/test_module_baddns_direct.py @@ -54,11 +54,9 @@ def set_target(self, target): def check(self, module_test, events): assert any( - [ - e.type == "FINDING" - and "Possible [AWS Bucket Takeover Detection] via direct BadDNS analysis. Indicator: [[Words: The specified bucket does not exist | Condition: and | Part: body] Matchers-Condition: and] Trigger: [self] baddns Module: [CNAME]" - in e.data["description"] - for e in events - ] + e.type == "FINDING" + and "Possible [AWS Bucket Takeover Detection] via direct BadDNS analysis. Indicator: [[Words: The specified bucket does not exist | Condition: and | Part: body] Matchers-Condition: and] Trigger: [self] baddns Module: [CNAME]" + in e.data["description"] + for e in events ), "Failed to emit FINDING" - assert any(["baddns-cname" in e.tags for e in events]), "Failed to add baddns tag" + assert any("baddns-cname" in e.tags for e in events), "Failed to add baddns tag" diff --git a/bbot/test/test_step_2/module_tests/test_module_bevigil.py b/bbot/test/test_step_2/module_tests/test_module_bevigil.py index 328e213d2c..7e616752fa 100644 --- a/bbot/test/test_step_2/module_tests/test_module_bevigil.py +++ b/bbot/test/test_step_2/module_tests/test_module_bevigil.py @@ -9,7 +9,7 @@ class TestBeVigil(ModuleTestBase): async def setup_after_prep(self, module_test): module_test.httpx_mock.add_response( - url=f"https://osint.bevigil.com/api/blacklanternsecurity.com/subdomains/", + url="https://osint.bevigil.com/api/blacklanternsecurity.com/subdomains/", match_headers={"X-Access-Token": "asdf"}, json={ "domain": "blacklanternsecurity.com", @@ -19,7 +19,7 @@ async def setup_after_prep(self, module_test): }, ) module_test.httpx_mock.add_response( - url=f"https://osint.bevigil.com/api/blacklanternsecurity.com/urls/", + url="https://osint.bevigil.com/api/blacklanternsecurity.com/urls/", json={"domain": "blacklanternsecurity.com", "urls": ["https://asdf.blacklanternsecurity.com"]}, ) @@ -35,7 +35,7 @@ class TestBeVigilMultiKey(TestBeVigil): async def setup_after_prep(self, module_test): module_test.httpx_mock.add_response( - url=f"https://osint.bevigil.com/api/blacklanternsecurity.com/subdomains/", + url="https://osint.bevigil.com/api/blacklanternsecurity.com/subdomains/", match_headers={"X-Access-Token": "fdsa"}, json={ "domain": "blacklanternsecurity.com", @@ -46,6 +46,6 @@ async def setup_after_prep(self, module_test): ) module_test.httpx_mock.add_response( match_headers={"X-Access-Token": "asdf"}, - url=f"https://osint.bevigil.com/api/blacklanternsecurity.com/urls/", + url="https://osint.bevigil.com/api/blacklanternsecurity.com/urls/", json={"domain": "blacklanternsecurity.com", "urls": ["https://asdf.blacklanternsecurity.com"]}, ) diff --git a/bbot/test/test_step_2/module_tests/test_module_binaryedge.py b/bbot/test/test_step_2/module_tests/test_module_binaryedge.py index 95b4ae7a71..348e2efb24 100644 --- a/bbot/test/test_step_2/module_tests/test_module_binaryedge.py +++ b/bbot/test/test_step_2/module_tests/test_module_binaryedge.py @@ -6,7 +6,7 @@ class TestBinaryEdge(ModuleTestBase): async def setup_before_prep(self, module_test): module_test.httpx_mock.add_response( - url=f"https://api.binaryedge.io/v2/query/domains/subdomain/blacklanternsecurity.com", + url="https://api.binaryedge.io/v2/query/domains/subdomain/blacklanternsecurity.com", match_headers={"X-Key": "asdf"}, json={ "query": "blacklanternsecurity.com", @@ -19,7 +19,7 @@ async def setup_before_prep(self, module_test): }, ) module_test.httpx_mock.add_response( - url=f"https://api.binaryedge.io/v2/user/subscription", + url="https://api.binaryedge.io/v2/user/subscription", match_headers={"X-Key": "asdf"}, json={ "subscription": {"name": "Free"}, diff --git a/bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py b/bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py index b566e9a826..7a5499b2e0 100644 --- a/bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py +++ b/bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py @@ -82,8 +82,8 @@ def check(self, module_test, events): if e.type == "FINDING" and str(e.module) == self.module_name: url = e.data.get("url", "") assert self.random_bucket_2 in url - assert not self.random_bucket_1 in url - assert not self.random_bucket_3 in url + assert self.random_bucket_1 not in url + assert self.random_bucket_3 not in url # make sure bucket mutations were found assert any( e.type == "STORAGE_BUCKET" diff --git a/bbot/test/test_step_2/module_tests/test_module_bucket_azure.py b/bbot/test/test_step_2/module_tests/test_module_bucket_azure.py index a3c866c08e..3b172eaaba 100644 --- a/bbot/test/test_step_2/module_tests/test_module_bucket_azure.py +++ b/bbot/test/test_step_2/module_tests/test_module_bucket_azure.py @@ -21,7 +21,7 @@ class TestBucket_Azure_NoDup(ModuleTestBase): async def setup_before_prep(self, module_test): module_test.httpx_mock.add_response( - url=f"https://tesla.blob.core.windows.net/tesla?restype=container", + url="https://tesla.blob.core.windows.net/tesla?restype=container", text="", ) await module_test.mock_dns( diff --git a/bbot/test/test_step_2/module_tests/test_module_builtwith.py b/bbot/test/test_step_2/module_tests/test_module_builtwith.py index 0fc4de9d56..d11c8940d2 100644 --- a/bbot/test/test_step_2/module_tests/test_module_builtwith.py +++ b/bbot/test/test_step_2/module_tests/test_module_builtwith.py @@ -6,7 +6,7 @@ class TestBuiltWith(ModuleTestBase): async def setup_after_prep(self, module_test): module_test.httpx_mock.add_response( - url=f"https://api.builtwith.com/v20/api.json?KEY=asdf&LOOKUP=blacklanternsecurity.com&NOMETA=yes&NOATTR=yes&HIDETEXT=yes&HIDEDL=yes", + url="https://api.builtwith.com/v20/api.json?KEY=asdf&LOOKUP=blacklanternsecurity.com&NOMETA=yes&NOATTR=yes&HIDETEXT=yes&HIDEDL=yes", json={ "Results": [ { @@ -91,7 +91,7 @@ async def setup_after_prep(self, module_test): }, ) module_test.httpx_mock.add_response( - url=f"https://api.builtwith.com/redirect1/api.json?KEY=asdf&LOOKUP=blacklanternsecurity.com", + url="https://api.builtwith.com/redirect1/api.json?KEY=asdf&LOOKUP=blacklanternsecurity.com", json={ "Lookup": "blacklanternsecurity.com", "Inbound": [ diff --git a/bbot/test/test_step_2/module_tests/test_module_bypass403.py b/bbot/test/test_step_2/module_tests/test_module_bypass403.py index 8990f0d5ec..57c1a8bedf 100644 --- a/bbot/test/test_step_2/module_tests/test_module_bypass403.py +++ b/bbot/test/test_step_2/module_tests/test_module_bypass403.py @@ -26,7 +26,7 @@ class TestBypass403_collapsethreshold(ModuleTestBase): async def setup_after_prep(self, module_test): respond_args = {"response_data": "alive"} - # some of these wont work outside of the module because of the complex logic. This doesn't matter, we just need to get more alerts than the threshold. + # some of these won't work outside of the module because of the complex logic. This doesn't matter, we just need to get more alerts than the threshold. query_payloads = [ "%09", diff --git a/bbot/test/test_step_2/module_tests/test_module_c99.py b/bbot/test/test_step_2/module_tests/test_module_c99.py index 284b76e1e2..ce9c7c8878 100644 --- a/bbot/test/test_step_2/module_tests/test_module_c99.py +++ b/bbot/test/test_step_2/module_tests/test_module_c99.py @@ -51,10 +51,10 @@ async def custom_callback(request): def check(self, module_test, events): assert module_test.module.api_failure_abort_threshold == 13 - assert module_test.module.errored == False + assert module_test.module.errored is False # assert module_test.module._api_request_failures == 4 assert module_test.module.api_retries == 4 - assert set([e.data for e in events if e.type == "DNS_NAME"]) == {"blacklanternsecurity.com"} + assert {e.data for e in events if e.type == "DNS_NAME"} == {"blacklanternsecurity.com"} assert self.url_count == { "https://api.c99.nl/randomnumber?key=6789&between=1,100&json": 1, "https://api.c99.nl/randomnumber?key=4321&between=1,100&json": 1, @@ -82,10 +82,10 @@ async def setup_before_prep(self, module_test): def check(self, module_test, events): assert module_test.module.api_failure_abort_threshold == 13 - assert module_test.module.errored == False + assert module_test.module.errored is False assert module_test.module._api_request_failures == 8 assert module_test.module.api_retries == 4 - assert set([e.data for e in events if e.type == "DNS_NAME"]) == {"blacklanternsecurity.com", "evilcorp.com"} + assert {e.data for e in events if e.type == "DNS_NAME"} == {"blacklanternsecurity.com", "evilcorp.com"} assert self.url_count == { "https://api.c99.nl/randomnumber?key=6789&between=1,100&json": 1, "https://api.c99.nl/randomnumber?key=4321&between=1,100&json": 1, @@ -106,10 +106,10 @@ class TestC99AbortThreshold3(TestC99AbortThreshold2): def check(self, module_test, events): assert module_test.module.api_failure_abort_threshold == 13 - assert module_test.module.errored == False + assert module_test.module.errored is False assert module_test.module._api_request_failures == 12 assert module_test.module.api_retries == 4 - assert set([e.data for e in events if e.type == "DNS_NAME"]) == { + assert {e.data for e in events if e.type == "DNS_NAME"} == { "blacklanternsecurity.com", "evilcorp.com", "evilcorp.net", @@ -138,14 +138,14 @@ class TestC99AbortThreshold4(TestC99AbortThreshold3): def check(self, module_test, events): assert module_test.module.api_failure_abort_threshold == 13 - assert module_test.module.errored == True + assert module_test.module.errored is True assert module_test.module._api_request_failures == 13 assert module_test.module.api_retries == 4 - assert set([e.data for e in events if e.type == "DNS_NAME"]) == { + assert {e.data for e in events if e.type == "DNS_NAME"} == { "blacklanternsecurity.com", "evilcorp.com", "evilcorp.net", "evilcorp.co.uk", } assert len(self.url_count) == 16 - assert all([v == 1 for v in self.url_count.values()]) + assert all(v == 1 for v in self.url_count.values()) diff --git a/bbot/test/test_step_2/module_tests/test_module_censys.py b/bbot/test/test_step_2/module_tests/test_module_censys.py index 51a2d054bc..14e72921e1 100644 --- a/bbot/test/test_step_2/module_tests/test_module_censys.py +++ b/bbot/test/test_step_2/module_tests/test_module_censys.py @@ -2,11 +2,12 @@ class TestCensys(ModuleTestBase): - config_overrides = {"modules": {"censys": {"api_id": "api_id", "api_secret": "api_secret"}}} + config_overrides = {"modules": {"censys": {"api_key": "api_id:api_secret"}}} async def setup_before_prep(self, module_test): module_test.httpx_mock.add_response( url="https://search.censys.io/api/v1/account", + match_headers={"Authorization": "Basic YXBpX2lkOmFwaV9zZWNyZXQ="}, json={ "email": "info@blacklanternsecurity.com", "login": "nope", @@ -17,6 +18,7 @@ async def setup_before_prep(self, module_test): ) module_test.httpx_mock.add_response( url="https://search.censys.io/api/v2/certificates/search", + match_headers={"Authorization": "Basic YXBpX2lkOmFwaV9zZWNyZXQ="}, match_content=b'{"q": "names: blacklanternsecurity.com", "per_page": 100}', json={ "code": 200, @@ -45,6 +47,7 @@ async def setup_before_prep(self, module_test): ) module_test.httpx_mock.add_response( url="https://search.censys.io/api/v2/certificates/search", + match_headers={"Authorization": "Basic YXBpX2lkOmFwaV9zZWNyZXQ="}, match_content=b'{"q": "names: blacklanternsecurity.com", "per_page": 100, "cursor": "NextToken"}', json={ "code": 200, diff --git a/bbot/test/test_step_2/module_tests/test_module_cloudcheck.py b/bbot/test/test_step_2/module_tests/test_module_cloudcheck.py index b95e7455d2..0ce93ec00d 100644 --- a/bbot/test/test_step_2/module_tests/test_module_cloudcheck.py +++ b/bbot/test/test_step_2/module_tests/test_module_cloudcheck.py @@ -8,7 +8,7 @@ class TestCloudCheck(ModuleTestBase): modules_overrides = ["httpx", "excavate", "cloudcheck"] async def setup_after_prep(self, module_test): - module_test.set_expect_requests({"uri": "/"}, {"response_data": ""}) + module_test.set_expect_requests({"uri": "/"}, {"response_data": ""}) scan = Scanner(config={"cloudcheck": True}) await scan._prep() @@ -51,6 +51,10 @@ async def setup_after_prep(self, module_test): await module.handle_event(event) assert "cloud-amazon" in event.tags, f"{event} was not properly cloud-tagged" + assert "cloud-domain" in aws_event1.tags + assert "cloud-ip" in other_event2.tags + assert "cloud-cname" in other_event3.tags + for event in (aws_event3, other_event1): await module.handle_event(event) assert "cloud-amazon" not in event.tags, f"{event} was improperly cloud-tagged" diff --git a/bbot/test/test_step_2/module_tests/test_module_columbus.py b/bbot/test/test_step_2/module_tests/test_module_columbus.py index 55d456ce31..b91b532d71 100644 --- a/bbot/test/test_step_2/module_tests/test_module_columbus.py +++ b/bbot/test/test_step_2/module_tests/test_module_columbus.py @@ -4,7 +4,7 @@ class TestColumbus(ModuleTestBase): async def setup_after_prep(self, module_test): module_test.httpx_mock.add_response( - url=f"https://columbus.elmasy.com/api/lookup/blacklanternsecurity.com?days=365", + url="https://columbus.elmasy.com/api/lookup/blacklanternsecurity.com?days=365", json=["asdf", "zzzz"], ) diff --git a/bbot/test/test_step_2/module_tests/test_module_credshed.py b/bbot/test/test_step_2/module_tests/test_module_credshed.py index 44b9133c97..a6b1e65c51 100644 --- a/bbot/test/test_step_2/module_tests/test_module_credshed.py +++ b/bbot/test/test_step_2/module_tests/test_module_credshed.py @@ -58,12 +58,12 @@ class TestCredshed(ModuleTestBase): async def setup_before_prep(self, module_test): module_test.httpx_mock.add_response( - url=f"https://credshed.com/api/auth", + url="https://credshed.com/api/auth", json=credshed_auth_response, method="POST", ) module_test.httpx_mock.add_response( - url=f"https://credshed.com/api/search", + url="https://credshed.com/api/search", json=credshed_response, method="POST", ) diff --git a/bbot/test/test_step_2/module_tests/test_module_dastardly.py b/bbot/test/test_step_2/module_tests/test_module_dastardly.py index cb4a501b8b..a3b59ef13e 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dastardly.py +++ b/bbot/test/test_step_2/module_tests/test_module_dastardly.py @@ -7,6 +7,7 @@ class TestDastardly(ModuleTestBase): targets = ["http://127.0.0.1:5556/"] modules_overrides = ["httpx", "dastardly"] + skip_distro_tests = True web_response = """ @@ -44,7 +45,7 @@ async def setup_after_prep(self, module_test): # get docker IP docker_ip = await self.get_docker_ip(module_test) - module_test.scan.target.add(docker_ip) + module_test.scan.target.seeds.add(docker_ip) # replace 127.0.0.1 with docker host IP to allow dastardly access to local http server old_filter_event = module_test.module.filter_event diff --git a/bbot/test/test_step_2/module_tests/test_module_dehashed.py b/bbot/test/test_step_2/module_tests/test_module_dehashed.py index 0ac91c3b85..f642a444b6 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dehashed.py +++ b/bbot/test/test_step_2/module_tests/test_module_dehashed.py @@ -45,7 +45,7 @@ class TestDehashed(ModuleTestBase): async def setup_before_prep(self, module_test): module_test.httpx_mock.add_response( - url=f"https://api.dehashed.com/search?query=domain:blacklanternsecurity.com&size=10000&page=1", + url="https://api.dehashed.com/search?query=domain:blacklanternsecurity.com&size=10000&page=1", json=dehashed_domain_response, ) await module_test.mock_dns( diff --git a/bbot/test/test_step_2/module_tests/test_module_digitorus.py b/bbot/test/test_step_2/module_tests/test_module_digitorus.py index fc95a82c76..a683a17d8f 100644 --- a/bbot/test/test_step_2/module_tests/test_module_digitorus.py +++ b/bbot/test/test_step_2/module_tests/test_module_digitorus.py @@ -11,7 +11,7 @@ class TestDigitorus(ModuleTestBase): async def setup_after_prep(self, module_test): module_test.httpx_mock.add_response( - url=f"https://certificatedetails.com/blacklanternsecurity.com", + url="https://certificatedetails.com/blacklanternsecurity.com", text=self.web_response, ) diff --git a/bbot/test/test_step_2/module_tests/test_module_dnsbrute.py b/bbot/test/test_step_2/module_tests/test_module_dnsbrute.py index 12427b0506..0cbee8440e 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dnsbrute.py +++ b/bbot/test/test_step_2/module_tests/test_module_dnsbrute.py @@ -7,7 +7,6 @@ class TestDnsbrute(ModuleTestBase): config_overrides = {"modules": {"dnsbrute": {"wordlist": str(subdomain_wordlist), "max_depth": 3}}} async def setup_after_prep(self, module_test): - old_run_live = module_test.scan.helpers.run_live async def new_run_live(*command, check=False, text=True, **kwargs): @@ -57,39 +56,39 @@ async def new_run_live(*command, check=False, text=True, **kwargs): event = module_test.scan.make_event("blacklanternsecurity.com", "DNS_NAME", parent=module_test.scan.root_event) event.scope_distance = 0 result, reason = await module_test.module.filter_event(event) - assert result == True + assert result is True event = module_test.scan.make_event( "www.blacklanternsecurity.com", "DNS_NAME", parent=module_test.scan.root_event ) event.scope_distance = 0 result, reason = await module_test.module.filter_event(event) - assert result == True + assert result is True event = module_test.scan.make_event( "test.www.blacklanternsecurity.com", "DNS_NAME", parent=module_test.scan.root_event ) event.scope_distance = 0 result, reason = await module_test.module.filter_event(event) - assert result == True + assert result is True event = module_test.scan.make_event( "asdf.test.www.blacklanternsecurity.com", "DNS_NAME", parent=module_test.scan.root_event ) event.scope_distance = 0 result, reason = await module_test.module.filter_event(event) - assert result == True + assert result is True event = module_test.scan.make_event( "wat.asdf.test.www.blacklanternsecurity.com", "DNS_NAME", parent=module_test.scan.root_event ) event.scope_distance = 0 result, reason = await module_test.module.filter_event(event) - assert result == False - assert reason == f"subdomain depth of *.asdf.test.www.blacklanternsecurity.com (4) > max_depth (3)" + assert result is False + assert reason == "subdomain depth of *.asdf.test.www.blacklanternsecurity.com (4) > max_depth (3)" event = module_test.scan.make_event( "hmmm.ptr1234.blacklanternsecurity.com", "DNS_NAME", parent=module_test.scan.root_event ) event.scope_distance = 0 result, reason = await module_test.module.filter_event(event) - assert result == False - assert reason == f'"ptr1234.blacklanternsecurity.com" looks like an autogenerated PTR' + assert result is False + assert reason == '"ptr1234.blacklanternsecurity.com" looks like an autogenerated PTR' def check(self, module_test, events): assert len(events) == 4 diff --git a/bbot/test/test_step_2/module_tests/test_module_dnsbrute_mutations.py b/bbot/test/test_step_2/module_tests/test_module_dnsbrute_mutations.py index 0c9b6baaa9..4f4009825f 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dnsbrute_mutations.py +++ b/bbot/test/test_step_2/module_tests/test_module_dnsbrute_mutations.py @@ -11,7 +11,6 @@ class TestDnsbrute_mutations(ModuleTestBase): ] async def setup_after_prep(self, module_test): - old_run_live = module_test.scan.helpers.run_live async def new_run_live(*command, check=False, text=True, **kwargs): diff --git a/bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py b/bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py index 6c5023db17..848491511e 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py +++ b/bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py @@ -8,7 +8,6 @@ class TestDNSCommonSRV(ModuleTestBase): config_overrides = {"dns": {"minimal": False}} async def setup_after_prep(self, module_test): - old_run_live = module_test.scan.helpers.run_live async def new_run_live(*command, check=False, text=True, **kwargs): diff --git a/bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py b/bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py index 6bf045d5c4..714a610c0c 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py +++ b/bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py @@ -4,14 +4,14 @@ class TestDNSDumpster(ModuleTestBase): async def setup_after_prep(self, module_test): module_test.httpx_mock.add_response( - url=f"https://dnsdumpster.com", + url="https://dnsdumpster.com", headers={"Set-Cookie": "csrftoken=asdf"}, - content=b'\n\n \n\n \n \n\n \n \n \n \n DNSdumpster.com - dns recon and research, find and lookup dns records\n\n\n \n \n \n\n \n \n\n \n\n \n\n
\n
\n\n
\n
\n\n
\n
\n \n
\n
\n\n\n\n\n
\n
\n

dns recon & research, find & lookup dns records

\n

\n

\n
\n
\n
\n\n\n\n\n
\n
\n
\n
\n
Loading...
\n
\n
\n
\n

\n\n
\n\n
\n\n

DNSdumpster.com is a FREE domain research tool that can discover hosts related to a domain. Finding visible hosts from the attackers perspective is an important part of the security assessment process.

\n\n
\n\n

this is a project

\n\n\n
\n
\n
\n

\n

Open Source Intelligence for Networks

\n
\n
\n
\n
\n \n \n \n

Attack

\n

The ability to quickly identify the attack surface is essential. Whether you are penetration testing or chasing bug bounties.

\n
\n
\n \n \n \n

Defend

\n

Network defenders benefit from passive reconnaissance in a number of ways. With analysis informing information security strategy.

\n
\n
\n \n \n \n

Learn

\n

Understanding network based OSINT helps information technologists to better operate, assess and manage the network.

\n
\n
\n
\n\n\n\n\n
\n\n \n

Map an organizations attack surface with a virtual dumpster dive* of the DNS records associated with the target organization.

\n

*DUMPSTER DIVING: The practice of sifting refuse from an office or technical installation to extract confidential data, especially security-compromising information.

\n
\n\n\n
\n\n

Frequently Asked Questions

\n\n

How can I take my security assessments to the next level?

\n\n

The company behind DNSDumpster is hackertarget.com where we provide online hosted access to trusted open source security vulnerability scanners and network intelligence tools.

Save time and headaches by incorporating our attack surface discovery into your vulnerability assessment process.

HackerTarget.com | Online Security Testing and Open Source Intelligence

\n\n

What data does DNSDumpster use?

\n\n

No brute force subdomain enumeration is used as is common in dns recon tools that enumerate subdomains. We use open source intelligence resources to query for related domain data. It is then compiled into an actionable resource for both attackers and defenders of Internet facing systems.

\n

More than a simple DNS lookup this tool will discover those hard to find sub-domains and web hosts. The search relies on data from our crawls of the Alexa Top 1 Million sites, Search Engines, Common Crawl, Certificate Transparency, Max Mind, Team Cymru, Shodan and scans.io.

\n\n

I have hit the host limit, do you have a PRO option?

\n\n

Over at hackertarget.com there\'s a tool we call domain profiler. This compiles data similiar to DNSDumpster; with additional data discovery. Queries available are based on the membership plan with the number of results (subdomains) being unlimited. With a STARTER membership you have access to the domain profiler tool for 12 months. Once the years membership expires you will revert to BASIC member status, however access to Domain Profiler and Basic Nmap scans continue. The BASIC access does not expire.

\n\n

What are some other resources and tools for learning more?

\n\n

There are some great open source recon frameworks that have been developed over the past couple of years. In addition tools such as Metasploit and Nmap include various modules for enumerating DNS. Check our Getting Started with Footprinting for more information.

\n\n
\n\n\n
\n\n\n
\n
\n
\n\n
\n
\n
\n\n\n
\n

dnsdumpster@gmail.com

\n
\n\n\n\n\n
\n
\n
\n\n \n \n
\n
Low volume Updates and News
\n
\n
\n
\n\n \n\n
\n
\n
\n
\n\n
\n\n\n
\n \n \n \n \n\n\n\n\n\n\n \n \n \n \n\n\n\n\n\n\n\n\n\n \n\n', + content=b'\n\n \n\n \n \n\n \n \n \n \n DNSdumpster.com - dns recon and research, find and lookup dns records\n\n\n \n \n \n\n \n \n\n \n\n \n\n
\n
\n\n
\n
\n\n
\n
\n \n
\n
\n\n\n\n\n
\n
\n

dns recon & research, find & lookup dns records

\n

\n

\n
\n
\n
\n\n\n\n\n
\n
\n
\n
\n
Loading...
\n
\n
\n
\n

\n\n
\n\n
\n\n

DNSdumpster.com is a FREE domain research tool that can discover hosts related to a domain. Finding visible hosts from the attackers perspective is an important part of the security assessment process.

\n\n
\n\n

this is a project

\n\n\n
\n
\n
\n

\n

Open Source Intelligence for Networks

\n
\n
\n
\n
\n \n \n \n

Attack

\n

The ability to quickly identify the attack surface is essential. Whether you are penetration testing or chasing bug bounties.

\n
\n
\n \n \n \n

Defend

\n

Network defenders benefit from passive reconnaissance in a number of ways. With analysis informing information security strategy.

\n
\n
\n \n \n \n

Learn

\n

Understanding network based OSINT helps information technologists to better operate, assess and manage the network.

\n
\n
\n
\n\n\n\n\n
\n\n \n

Map an organizations attack surface with a virtual dumpster dive* of the DNS records associated with the target organization.

\n

*DUMPSTER DIVING: The practice of sifting refuse from an office or technical installation to extract confidential data, especially security-compromising information.

\n
\n\n\n
\n\n

Frequently Asked Questions

\n\n

How can I take my security assessments to the next level?

\n\n

The company behind DNSDumpster is hackertarget.com where we provide online hosted access to trusted open source security vulnerability scanners and network intelligence tools.

Save time and headaches by incorporating our attack surface discovery into your vulnerability assessment process.

HackerTarget.com | Online Security Testing and Open Source Intelligence

\n\n

What data does DNSDumpster use?

\n\n

No brute force subdomain enumeration is used as is common in dns recon tools that enumerate subdomains. We use open source intelligence resources to query for related domain data. It is then compiled into an actionable resource for both attackers and defenders of Internet facing systems.

\n

More than a simple DNS lookup this tool will discover those hard to find sub-domains and web hosts. The search relies on data from our crawls of the Alexa Top 1 Million sites, Search Engines, Common Crawl, Certificate Transparency, Max Mind, Team Cymru, Shodan and scans.io.

\n\n

I have hit the host limit, do you have a PRO option?

\n\n

Over at hackertarget.com there\'s a tool we call domain profiler. This compiles data similar to DNSDumpster; with additional data discovery. Queries available are based on the membership plan with the number of results (subdomains) being unlimited. With a STARTER membership you have access to the domain profiler tool for 12 months. Once the years membership expires you will revert to BASIC member status, however access to Domain Profiler and Basic Nmap scans continue. The BASIC access does not expire.

\n\n

What are some other resources and tools for learning more?

\n\n

There are some great open source recon frameworks that have been developed over the past couple of years. In addition tools such as Metasploit and Nmap include various modules for enumerating DNS. Check our Getting Started with Footprinting for more information.

\n\n
\n\n\n
\n\n\n
\n
\n
\n\n
\n
\n
\n\n\n
\n

dnsdumpster@gmail.com

\n
\n\n\n\n\n
\n
\n
\n\n \n \n
\n
Low volume Updates and News
\n
\n
\n
\n\n \n\n
\n
\n
\n
\n\n
\n\n\n
\n \n \n \n \n\n\n\n\n\n\n \n \n \n \n\n\n\n\n\n\n\n\n\n \n\n', ) module_test.httpx_mock.add_response( - url=f"https://dnsdumpster.com/", + url="https://dnsdumpster.com/", method="POST", - content=b'\n\n \n\n \n \n\n \n \n \n \n DNSdumpster.com - dns recon and research, find and lookup dns records\n\n\n \n \n \n\n \n \n\n \n\n \n\n
\n
\n\n
\n
\n\n
\n
\n \n
\n
\n\n\n\n\n
\n
\n

dns recon & research, find & lookup dns records

\n

\n

\n
\n
\n
\n\n\n\n\n
\n
\n
\n
\n
Loading...
\n
\n
\n
\n

\n\n
\n\n

Showing results for blacklanternsecurity.com

\n
\n
\n
\n
\n

Hosting (IP block owners)

\n
\n
\n

GeoIP of Host Locations

\n
\n
\n
\n\n

DNS Servers

\n
\n \n \n \n \n \n \n
ns01.domaincontrol.com.
\n\n\n \n
\n
\n
97.74.100.1
ns01.domaincontrol.com
GODADDY-DNS
United States
ns02.domaincontrol.com.
\n\n\n \n
\n
\n
173.201.68.1
ns02.domaincontrol.com
GODADDY-DNS
United States
\n
\n\n

MX Records ** This is where email for the domain goes...

\n
\n \n \n \n \n
asdf.blacklanternsecurity.com.mail.protection.outlook.com.
\n\n\n
\n
\n
104.47.55.138
mail-bn8nam120138.inbound.protection.outlook.com
MICROSOFT-CORP-MSN-AS-BLOCK
United States
\n
\n\n

TXT Records ** Find more hosts in Sender Policy Framework (SPF) configurations

\n
\n \n\n\n\n\n\n\n\n\n\n
"MS=ms26206678"
"v=spf1 ip4:50.240.76.25 include:spf.protection.outlook.com -all"
"google-site-verification=O_PoQFTGJ_hZ9LqfNT9OEc0KPFERKHQ_1t1m0YTx_1E"
"google-site-verification=7XKUMxJSTHBSzdvT7gH47jLRjNAS76nrEfXmzhR_DO4"
\n
\n\n\n

Host Records (A) ** this data may not be current as it uses a static database (updated monthly)

\n
\n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
blacklanternsecurity.com
\n\n\n\n
\n
\n\n\n
HTTP: \n GitHub.com\n\n\n\n\n\n\n\n\n
HTTP TECH: \n varnish\n\n\n\n
185.199.108.153
cdn-185-199-108-153.github.com
FASTLY
United States
asdf.blacklanternsecurity.com
\n\n\n\n
\n
\n\n\n\n\n\n
SSH: \n SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.3\n\n\n\n\n\n\n\n
143.244.156.80
asdf.blacklanternsecurity.com
DIGITALOCEAN-ASN
United States
asdf.blacklanternsecurity.com
\n\n\n\n
\n
\n\n\n
HTTP: \n Apache/2.4.29 (Ubuntu)\n\n\n\n\n\n\n\n\n
HTTP TECH: \n Ubuntu
Apache,2.4.29
\n\n\n\n
64.227.8.231
asdf.blacklanternsecurity.com
DIGITALOCEAN-ASN
United States
asdf.blacklanternsecurity.com
\n\n\n\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n
192.34.56.157
asdf.blacklanternsecurity.com
DIGITALOCEAN-ASN
United States
asdf.blacklanternsecurity.com
\n\n\n\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n
192.241.216.208
asdf.blacklanternsecurity.com
DIGITALOCEAN-ASN
United States
asdf.blacklanternsecurity.com
\n\n\n\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n
167.71.95.71
asdf.blacklanternsecurity.com
DIGITALOCEAN-ASN
United States
asdf.blacklanternsecurity.com
\n\n\n\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n
157.245.247.197
asdf.blacklanternsecurity.com
DIGITALOCEAN-ASN
United States
\n
\n\n\n\n\n\n
\n

Mapping the domain ** click for full size image

\n

\n\n

\n
\n\n
\n\n

DNSdumpster.com is a FREE domain research tool that can discover hosts related to a domain. Finding visible hosts from the attackers perspective is an important part of the security assessment process.

\n\n
\n\n

this is a project

\n\n\n
\n
\n
\n

\n

Open Source Intelligence for Networks

\n
\n
\n
\n
\n \n \n \n

Attack

\n

The ability to quickly identify the attack surface is essential. Whether you are penetration testing or chasing bug bounties.

\n
\n
\n \n \n \n

Defend

\n

Network defenders benefit from passive reconnaissance in a number of ways. With analysis informing information security strategy.

\n
\n
\n \n \n \n

Learn

\n

Understanding network based OSINT helps information technologists to better operate, assess and manage the network.

\n
\n
\n
\n\n\n\n\n
\n\n \n

Map an organizations attack surface with a virtual dumpster dive* of the DNS records associated with the target organization.

\n

*DUMPSTER DIVING: The practice of sifting refuse from an office or technical installation to extract confidential data, especially security-compromising information.

\n
\n\n\n
\n\n

Frequently Asked Questions

\n\n

How can I take my security assessments to the next level?

\n\n

The company behind DNSDumpster is hackertarget.com where we provide online hosted access to trusted open source security vulnerability scanners and network intelligence tools.

Save time and headaches by incorporating our attack surface discovery into your vulnerability assessment process.

HackerTarget.com | Online Security Testing and Open Source Intelligence

\n\n

What data does DNSDumpster use?

\n\n

No brute force subdomain enumeration is used as is common in dns recon tools that enumerate subdomains. We use open source intelligence resources to query for related domain data. It is then compiled into an actionable resource for both attackers and defenders of Internet facing systems.

\n

More than a simple DNS lookup this tool will discover those hard to find sub-domains and web hosts. The search relies on data from our crawls of the Alexa Top 1 Million sites, Search Engines, Common Crawl, Certificate Transparency, Max Mind, Team Cymru, Shodan and scans.io.

\n\n

I have hit the host limit, do you have a PRO option?

\n\n

Over at hackertarget.com there\'s a tool we call domain profiler. This compiles data similiar to DNSDumpster; with additional data discovery. Queries available are based on the membership plan with the number of results (subdomains) being unlimited. With a STARTER membership you have access to the domain profiler tool for 12 months. Once the years membership expires you will revert to BASIC member status, however access to Domain Profiler and Basic Nmap scans continue. The BASIC access does not expire.

\n\n

What are some other resources and tools for learning more?

\n\n

There are some great open source recon frameworks that have been developed over the past couple of years. In addition tools such as Metasploit and Nmap include various modules for enumerating DNS. Check our Getting Started with Footprinting for more information.

\n\n
\n\n\n\n\n\n\n
\n\n\n
\n
\n
\n\n
\n
\n
\n\n\n
\n

dnsdumpster@gmail.com

\n
\n\n\n\n\n
\n
\n
\n\n \n \n
\n
Low volume Updates and News
\n
\n
\n
\n\n \n\n
\n
\n
\n
\n\n
\n\n\n
\n \n \n \n \n\n\n\n\n\n\n \n \n \n \n\n\n\n \n \n \n\n \n\n\n\n\n\n\n\n\n\n\n\n\n \n\n', + content=b'\n\n \n\n \n \n\n \n \n \n \n DNSdumpster.com - dns recon and research, find and lookup dns records\n\n\n \n \n \n\n \n \n\n \n\n \n\n
\n
\n\n
\n
\n\n
\n
\n \n
\n
\n\n\n\n\n
\n
\n

dns recon & research, find & lookup dns records

\n

\n

\n
\n
\n
\n\n\n\n\n
\n
\n
\n
\n
Loading...
\n
\n
\n
\n

\n\n
\n\n

Showing results for blacklanternsecurity.com

\n
\n
\n
\n
\n

Hosting (IP block owners)

\n
\n
\n

GeoIP of Host Locations

\n
\n
\n
\n\n

DNS Servers

\n
\n \n \n \n \n \n \n
ns01.domaincontrol.com.
\n\n\n \n
\n
\n
97.74.100.1
ns01.domaincontrol.com
GODADDY-DNS
United States
ns02.domaincontrol.com.
\n\n\n \n
\n
\n
173.201.68.1
ns02.domaincontrol.com
GODADDY-DNS
United States
\n
\n\n

MX Records ** This is where email for the domain goes...

\n
\n \n \n \n \n
asdf.blacklanternsecurity.com.mail.protection.outlook.com.
\n\n\n
\n
\n
104.47.55.138
mail-bn8nam120138.inbound.protection.outlook.com
MICROSOFT-CORP-MSN-AS-BLOCK
United States
\n
\n\n

TXT Records ** Find more hosts in Sender Policy Framework (SPF) configurations

\n
\n \n\n\n\n\n\n\n\n\n\n
"MS=ms26206678"
"v=spf1 ip4:50.240.76.25 include:spf.protection.outlook.com -all"
"google-site-verification=O_PoQFTGJ_hZ9LqfNT9OEc0KPFERKHQ_1t1m0YTx_1E"
"google-site-verification=7XKUMxJSTHBSzdvT7gH47jLRjNAS76nrEfXmzhR_DO4"
\n
\n\n\n

Host Records (A) ** this data may not be current as it uses a static database (updated monthly)

\n
\n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
blacklanternsecurity.com
\n\n\n\n
\n
\n\n\n
HTTP: \n GitHub.com\n\n\n\n\n\n\n\n\n
HTTP TECH: \n varnish\n\n\n\n
185.199.108.153
cdn-185-199-108-153.github.com
FASTLY
United States
asdf.blacklanternsecurity.com
\n\n\n\n
\n
\n\n\n\n\n\n
SSH: \n SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.3\n\n\n\n\n\n\n\n
143.244.156.80
asdf.blacklanternsecurity.com
DIGITALOCEAN-ASN
United States
asdf.blacklanternsecurity.com
\n\n\n\n
\n
\n\n\n
HTTP: \n Apache/2.4.29 (Ubuntu)\n\n\n\n\n\n\n\n\n
HTTP TECH: \n Ubuntu
Apache,2.4.29
\n\n\n\n
64.227.8.231
asdf.blacklanternsecurity.com
DIGITALOCEAN-ASN
United States
asdf.blacklanternsecurity.com
\n\n\n\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n
192.34.56.157
asdf.blacklanternsecurity.com
DIGITALOCEAN-ASN
United States
asdf.blacklanternsecurity.com
\n\n\n\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n
192.241.216.208
asdf.blacklanternsecurity.com
DIGITALOCEAN-ASN
United States
asdf.blacklanternsecurity.com
\n\n\n\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n
167.71.95.71
asdf.blacklanternsecurity.com
DIGITALOCEAN-ASN
United States
asdf.blacklanternsecurity.com
\n\n\n\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n
157.245.247.197
asdf.blacklanternsecurity.com
DIGITALOCEAN-ASN
United States
\n
\n\n\n\n\n\n
\n

Mapping the domain ** click for full size image

\n

\n\n

\n
\n\n
\n\n

DNSdumpster.com is a FREE domain research tool that can discover hosts related to a domain. Finding visible hosts from the attackers perspective is an important part of the security assessment process.

\n\n
\n\n

this is a project

\n\n\n
\n
\n
\n

\n

Open Source Intelligence for Networks

\n
\n
\n
\n
\n \n \n \n

Attack

\n

The ability to quickly identify the attack surface is essential. Whether you are penetration testing or chasing bug bounties.

\n
\n
\n \n \n \n

Defend

\n

Network defenders benefit from passive reconnaissance in a number of ways. With analysis informing information security strategy.

\n
\n
\n \n \n \n

Learn

\n

Understanding network based OSINT helps information technologists to better operate, assess and manage the network.

\n
\n
\n
\n\n\n\n\n
\n\n \n

Map an organizations attack surface with a virtual dumpster dive* of the DNS records associated with the target organization.

\n

*DUMPSTER DIVING: The practice of sifting refuse from an office or technical installation to extract confidential data, especially security-compromising information.

\n
\n\n\n
\n\n

Frequently Asked Questions

\n\n

How can I take my security assessments to the next level?

\n\n

The company behind DNSDumpster is hackertarget.com where we provide online hosted access to trusted open source security vulnerability scanners and network intelligence tools.

Save time and headaches by incorporating our attack surface discovery into your vulnerability assessment process.

HackerTarget.com | Online Security Testing and Open Source Intelligence

\n\n

What data does DNSDumpster use?

\n\n

No brute force subdomain enumeration is used as is common in dns recon tools that enumerate subdomains. We use open source intelligence resources to query for related domain data. It is then compiled into an actionable resource for both attackers and defenders of Internet facing systems.

\n

More than a simple DNS lookup this tool will discover those hard to find sub-domains and web hosts. The search relies on data from our crawls of the Alexa Top 1 Million sites, Search Engines, Common Crawl, Certificate Transparency, Max Mind, Team Cymru, Shodan and scans.io.

\n\n

I have hit the host limit, do you have a PRO option?

\n\n

Over at hackertarget.com there\'s a tool we call domain profiler. This compiles data similar to DNSDumpster; with additional data discovery. Queries available are based on the membership plan with the number of results (subdomains) being unlimited. With a STARTER membership you have access to the domain profiler tool for 12 months. Once the years membership expires you will revert to BASIC member status, however access to Domain Profiler and Basic Nmap scans continue. The BASIC access does not expire.

\n\n

What are some other resources and tools for learning more?

\n\n

There are some great open source recon frameworks that have been developed over the past couple of years. In addition tools such as Metasploit and Nmap include various modules for enumerating DNS. Check our Getting Started with Footprinting for more information.

\n\n
\n\n\n\n\n\n\n
\n\n\n
\n
\n
\n\n
\n
\n
\n\n\n
\n

dnsdumpster@gmail.com

\n
\n\n\n\n\n
\n
\n
\n\n \n \n
\n
Low volume Updates and News
\n
\n
\n
\n\n \n\n
\n
\n
\n
\n\n
\n\n\n
\n \n \n \n \n\n\n\n\n\n\n \n \n \n \n\n\n\n \n \n \n\n \n\n\n\n\n\n\n\n\n\n\n\n\n \n\n', ) def check(self, module_test, events): diff --git a/bbot/test/test_step_2/module_tests/test_module_dnstlsrpt.py b/bbot/test/test_step_2/module_tests/test_module_dnstlsrpt.py new file mode 100644 index 0000000000..a14a882fda --- /dev/null +++ b/bbot/test/test_step_2/module_tests/test_module_dnstlsrpt.py @@ -0,0 +1,64 @@ +from .base import ModuleTestBase + +raw_smtp_tls_txt = '"v=TLSRPTv1; rua=mailto:tlsrpt@sub.blacklanternsecurity.notreal,mailto:test@on.thirdparty.com, https://tlspost.example.com;"' + + +class TestDNSTLSRPT(ModuleTestBase): + targets = ["blacklanternsecurity.notreal"] + modules_overrides = ["dnstlsrpt", "speculate"] + config_overrides = {"modules": {"dnstlsrpt": {"emit_raw_dns_records": True}}, "scope": {"report_distance": 1}} + + async def setup_after_prep(self, module_test): + await module_test.mock_dns( + { + "blacklanternsecurity.notreal": { + "A": ["127.0.0.11"], + }, + "_tls.blacklanternsecurity.notreal": { + "A": ["127.0.0.22"], + }, + "_smtp._tls.blacklanternsecurity.notreal": { + "A": ["127.0.0.33"], + "TXT": [raw_smtp_tls_txt], + }, + "_tls._smtp._tls.blacklanternsecurity.notreal": { + "A": ["127.0.0.44"], + }, + "_smtp._tls._smtp._tls.blacklanternsecurity.notreal": { + "TXT": [raw_smtp_tls_txt], + }, + "sub.blacklanternsecurity.notreal": { + "A": ["127.0.0.55"], + }, + } + ) + + def check(self, module_test, events): + assert any( + e.type == "RAW_DNS_RECORD" and e.data["answer"] == raw_smtp_tls_txt for e in events + ), "Failed to emit RAW_DNS_RECORD" + assert any( + e.type == "DNS_NAME" and e.data == "sub.blacklanternsecurity.notreal" for e in events + ), "Failed to detect sub-domain" + assert any( + e.type == "EMAIL_ADDRESS" and e.data == "tlsrpt@sub.blacklanternsecurity.notreal" for e in events + ), "Failed to detect email address" + assert any( + e.type == "EMAIL_ADDRESS" and e.data == "test@on.thirdparty.com" for e in events + ), "Failed to detect third party email address" + assert any( + e.type == "URL_UNVERIFIED" and e.data == "https://tlspost.example.com/" for e in events + ), "Failed to detect third party URL" + + +class TestDNSTLSRPTRecursiveRecursion(TestDNSTLSRPT): + config_overrides = { + "scope": {"report_distance": 1}, + "modules": {"dnstlsrpt": {"emit_raw_dns_records": True}}, + } + + def check(self, module_test, events): + assert not any( + e.type == "RAW_DNS_RECORD" and e.data["host"] == "_mta-sts._mta-sts.blacklanternsecurity.notreal" + for e in events + ), "Unwanted recursion occurring" diff --git a/bbot/test/test_step_2/module_tests/test_module_dotnetnuke.py b/bbot/test/test_step_2/module_tests/test_module_dotnetnuke.py index de78ad50b9..fc666b64eb 100644 --- a/bbot/test/test_step_2/module_tests/test_module_dotnetnuke.py +++ b/bbot/test/test_step_2/module_tests/test_module_dotnetnuke.py @@ -92,9 +92,6 @@ def check(self, module_test, events): dnn_installwizard_privesc_detection = False for e in events: - print(e) - print(e.type) - if e.type == "TECHNOLOGY" and "DotNetNuke" in e.data["technology"]: dnn_technology_detection = True @@ -149,14 +146,12 @@ def request_handler(self, request): return Response("alive", status=200) async def setup_before_prep(self, module_test): - self.interactsh_mock_instance = module_test.mock_interactsh("dotnetnuke_blindssrf") module_test.monkeypatch.setattr( module_test.scan.helpers, "interactsh", lambda *args, **kwargs: self.interactsh_mock_instance ) async def setup_after_prep(self, module_test): - # Simulate DotNetNuke Instance expect_args = {"method": "GET", "uri": "/"} respond_args = {"response_data": dotnetnuke_http_response} @@ -170,9 +165,6 @@ def check(self, module_test, events): dnn_dnnimagehandler_blindssrf = False for e in events: - - print(e) - print(e.type) if e.type == "TECHNOLOGY" and "DotNetNuke" in e.data["technology"]: dnn_technology_detection = True diff --git a/bbot/test/test_step_2/module_tests/test_module_excavate.py b/bbot/test/test_step_2/module_tests/test_module_excavate.py index 5b266a7814..e4e9c907c9 100644 --- a/bbot/test/test_step_2/module_tests/test_module_excavate.py +++ b/bbot/test/test_step_2/module_tests/test_module_excavate.py @@ -13,7 +13,6 @@ class TestExcavate(ModuleTestBase): config_overrides = {"web": {"spider_distance": 1, "spider_depth": 1}} async def setup_before_prep(self, module_test): - response_data = """ ftp://ftp.test.notreal \\nhttps://www1.test.notreal @@ -30,6 +29,7 @@ async def setup_before_prep(self, module_test): # these ones should + Help """ expect_args = {"method": "GET", "uri": "/"} respond_args = {"response_data": response_data} @@ -61,8 +61,8 @@ def check(self, module_test, events): assert "www6.test.notreal" in event_data assert "www7.test.notreal" in event_data assert "www8.test.notreal" in event_data - assert not "http://127.0.0.1:8888/a_relative.js" in event_data - assert not "http://127.0.0.1:8888/link_relative.js" in event_data + assert "http://127.0.0.1:8888/a_relative.js" not in event_data + assert "http://127.0.0.1:8888/link_relative.js" not in event_data assert "http://127.0.0.1:8888/a_relative.txt" in event_data assert "http://127.0.0.1:8888/link_relative.txt" in event_data @@ -181,7 +181,6 @@ async def setup_before_prep(self, module_test): module_test.httpserver.no_handler_status_code = 404 def check(self, module_test, events): - assert 1 == len( [ e @@ -222,7 +221,7 @@ def check(self, module_test, events): [e for e in events if e.type == "FINDING" and e.data["description"] == "Non-HTTP URI: smb://127.0.0.1"] ) assert 1 == len( - [e for e in events if e.type == "PROTOCOL" and e.data["protocol"] == "SMB" and not "port" in e.data] + [e for e in events if e.type == "PROTOCOL" and e.data["protocol"] == "SMB" and "port" not in e.data] ) assert 0 == len([e for e in events if e.type == "FINDING" and "ssh://127.0.0.1" in e.data["description"]]) assert 0 == len([e for e in events if e.type == "PROTOCOL" and e.data["protocol"] == "SSH"]) @@ -332,7 +331,6 @@ def check(self, module_test, events): class TestExcavateCSP(TestExcavate): - csp_test_header = "default-src 'self'; script-src asdf.test.notreal; object-src 'none';" async def setup_before_prep(self, module_test): @@ -356,7 +354,6 @@ def check(self, module_test, events): class TestExcavateURL_IP(TestExcavate): - targets = ["http://127.0.0.1:8888/", "127.0.0.2"] async def setup_before_prep(self, module_test): @@ -405,7 +402,6 @@ def check(self, module_test, events): class TestExcavateNonHttpScheme(TestExcavate): - targets = ["http://127.0.0.1:8888/", "test.notreal"] non_http_scheme_html = """ @@ -425,7 +421,6 @@ async def setup_before_prep(self, module_test): module_test.httpserver.expect_request("/").respond_with_data(self.non_http_scheme_html) def check(self, module_test, events): - found_hxxp_url = False found_ftp_url = False found_nonsense_url = False @@ -540,7 +535,6 @@ def check(self, module_test, events): class TestExcavateParameterExtraction_getparam(ModuleTestBase): - targets = ["http://127.0.0.1:8888/"] # hunt is added as parameter extraction is only activated by one or more modules that consume WEB_PARAMETER @@ -554,11 +548,9 @@ async def setup_after_prep(self, module_test): module_test.set_expect_requests(respond_args=respond_args) def check(self, module_test, events): - excavate_getparam_extraction = False for e in events: if e.type == "WEB_PARAMETER": - if "HTTP Extracted Parameter [hack] (HTML Tags Submodule)" in e.data["description"]: excavate_getparam_extraction = True assert excavate_getparam_extraction, "Excavate failed to extract web parameter" @@ -626,7 +618,6 @@ class excavateTestRule(ExcavateRule): class TestExcavateYara(TestExcavate): - targets = ["http://127.0.0.1:8888/"] yara_test_html = """ @@ -641,12 +632,10 @@ class TestExcavateYara(TestExcavate): """ async def setup_before_prep(self, module_test): - self.modules_overrides = ["excavate", "httpx"] module_test.httpserver.expect_request("/").respond_with_data(self.yara_test_html) async def setup_after_prep(self, module_test): - excavate_module = module_test.scan.modules["excavate"] excavateruleinstance = excavateTestRule(excavate_module) excavate_module.add_yara_rule( @@ -665,7 +654,6 @@ def check(self, module_test, events): found_yara_string_1 = False found_yara_string_2 = False for e in events: - if e.type == "FINDING": if e.data["description"] == "HTTP response (body) Contains the text AAAABBBBCCCC": found_yara_string_1 = True @@ -677,7 +665,6 @@ def check(self, module_test, events): class TestExcavateYaraCustom(TestExcavateYara): - rule_file = [ 'rule SearchForText { meta: description = "Contains the text AAAABBBBCCCC" strings: $text = "AAAABBBBCCCC" condition: $text }', 'rule SearchForText2 { meta: description = "Contains the text DDDDEEEEFFFF" strings: $text2 = "DDDDEEEEFFFF" condition: $text2 }', @@ -711,7 +698,6 @@ async def setup_after_prep(self, module_test): module_test.httpserver.expect_request("/spider").respond_with_data("hi") def check(self, module_test, events): - found_url_unverified_spider_max = False found_url_unverified_dummy = False found_url_event = False @@ -726,7 +712,7 @@ def check(self, module_test, events): if ( str(e.module) == "dummy_module" and "spider-danger" not in e.tags - and not "spider-max" in e.tags + and "spider-max" not in e.tags ): found_url_unverified_dummy = True if e.type == "URL" and e.data == "http://127.0.0.1:8888/spider": @@ -803,7 +789,6 @@ def check(self, module_test, events): class TestExcavate_retain_querystring_not(TestExcavate_retain_querystring): - config_overrides = { "url_querystring_remove": False, "url_querystring_collapse": False, @@ -827,7 +812,6 @@ def check(self, module_test, events): class TestExcavate_webparameter_outofscope(ModuleTestBase): - html_body = "" targets = ["http://127.0.0.1:8888", "socialmediasite.com"] @@ -858,13 +842,11 @@ def check(self, module_test, events): class TestExcavateHeaders(ModuleTestBase): - targets = ["http://127.0.0.1:8888/"] modules_overrides = ["excavate", "httpx", "hunt"] config_overrides = {"web": {"spider_distance": 1, "spider_depth": 1}} async def setup_before_prep(self, module_test): - module_test.httpserver.expect_request("/").respond_with_data( "

test

", status=200, @@ -877,7 +859,6 @@ async def setup_before_prep(self, module_test): ) def check(self, module_test, events): - found_first_cookie = False found_second_cookie = False @@ -888,8 +869,8 @@ def check(self, module_test, events): if e.data["name"] == "COOKIE2": found_second_cookie = True - assert found_first_cookie == True - assert found_second_cookie == True + assert found_first_cookie is True + assert found_second_cookie is True class TestExcavateRAWTEXT(ModuleTestBase): @@ -915,7 +896,7 @@ class TestExcavateRAWTEXT(ModuleTestBase): /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] >> /Rotate 0 /Trans << ->> +>> /Type /Page >> endobj @@ -926,7 +907,7 @@ class TestExcavateRAWTEXT(ModuleTestBase): endobj 5 0 obj << -/Author (anonymous) /CreationDate (D:20240807182842+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240807182842+00'00') /Producer (ReportLab PDF Library - www.reportlab.com) +/Author (anonymous) /CreationDate (D:20240807182842+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240807182842+00'00') /Producer (ReportLab PDF Library - www.reportlab.com) /Subject (unspecified) /Title (untitled) /Trapped /False >> endobj @@ -944,17 +925,17 @@ class TestExcavateRAWTEXT(ModuleTestBase): endobj xref 0 8 -0000000000 65535 f -0000000073 00000 n -0000000104 00000 n -0000000211 00000 n -0000000414 00000 n -0000000482 00000 n -0000000778 00000 n -0000000837 00000 n +0000000000 65535 f +0000000073 00000 n +0000000104 00000 n +0000000211 00000 n +0000000414 00000 n +0000000482 00000 n +0000000778 00000 n +0000000837 00000 n trailer << -/ID +/ID [<3c7340500fa2fe72523c5e6f07511599><3c7340500fa2fe72523c5e6f07511599>] % ReportLab generated PDF document -- digest (http://www.reportlab.com) @@ -977,12 +958,12 @@ class TestExcavateRAWTEXT(ModuleTestBase): async def setup_after_prep(self, module_test): module_test.set_expect_requests( - dict(uri="/"), - dict(response_data='
'), + {"uri": "/"}, + {"response_data": ''}, ) module_test.set_expect_requests( - dict(uri="/Test_PDF"), - dict(response_data=self.pdf_data, headers={"Content-Type": "application/pdf"}), + {"uri": "/Test_PDF"}, + {"response_data": self.pdf_data, "headers": {"Content-Type": "application/pdf"}}, ) def check(self, module_test, events): @@ -1030,3 +1011,29 @@ def check(self, module_test, events): assert ( "/donot_detect.js" not in url_events ), f"URL extracted from extractous text is incorrect, got {url_events}" + + +class TestExcavateBadURLs(ModuleTestBase): + targets = ["http://127.0.0.1:8888/"] + modules_overrides = ["excavate", "httpx", "hunt"] + config_overrides = {"interactsh_disable": True, "scope": {"report_distance": 10}} + + bad_url_data = """ +Help +Help +""" + + async def setup_after_prep(self, module_test): + module_test.set_expect_requests({"uri": "/"}, {"response_data": self.bad_url_data}) + + def check(self, module_test, events): + log_file = module_test.scan.home / "debug.log" + log_text = log_file.read_text() + # make sure our logging is working + assert "Setting scan status to STARTING" in log_text + # make sure we don't have any URL validation errors + assert "Error Parsing reconstructed URL" not in log_text + assert "Error sanitizing event data" not in log_text + + url_events = [e for e in events if e.type == "URL_UNVERIFIED"] + assert sorted([e.data for e in url_events]) == sorted(["https://ssl/", "http://127.0.0.1:8888/"]) diff --git a/bbot/test/test_step_2/module_tests/test_module_extractous.py b/bbot/test/test_step_2/module_tests/test_module_extractous.py index 9c47945e4a..27f3c95bf7 100644 --- a/bbot/test/test_step_2/module_tests/test_module_extractous.py +++ b/bbot/test/test_step_2/module_tests/test_module_extractous.py @@ -21,19 +21,19 @@ class TestExtractous(ModuleTestBase): async def setup_after_prep(self, module_test): module_test.set_expect_requests( - dict(uri="/"), - dict(response_data=''), + {"uri": "/"}, + {"response_data": ''}, ) module_test.set_expect_requests( - dict(uri="/Test_PDF"), - dict(response_data=self.pdf_data, headers={"Content-Type": "application/pdf"}), + {"uri": "/Test_PDF"}, + {"response_data": self.pdf_data, "headers": {"Content-Type": "application/pdf"}}, ) module_test.set_expect_requests( - dict(uri="/Test_DOCX"), - dict( - response_data=self.docx_data, - headers={"Content-Type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, - ), + {"uri": "/Test_DOCX"}, + { + "response_data": self.docx_data, + "headers": {"Content-Type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + }, ) def check(self, module_test, events): diff --git a/bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py b/bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py index 00c1f9b1e4..85327e7439 100644 --- a/bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py +++ b/bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py @@ -142,7 +142,7 @@ async def setup_after_prep(self, module_test): tags=["shortname-file"], ) ) - module_test.scan.target.seeds._events = set(seed_events) + module_test.scan.target.seeds.events = set(seed_events) expect_args = {"method": "GET", "uri": "/administrator.aspx"} respond_args = {"response_data": "alive"} diff --git a/bbot/test/test_step_2/module_tests/test_module_filedownload.py b/bbot/test/test_step_2/module_tests/test_module_filedownload.py index 0b949e9616..6e046aa473 100644 --- a/bbot/test/test_step_2/module_tests/test_module_filedownload.py +++ b/bbot/test/test_step_2/module_tests/test_module_filedownload.py @@ -15,28 +15,28 @@ class TestFileDownload(ModuleTestBase): async def setup_after_prep(self, module_test): module_test.set_expect_requests( - dict(uri="/"), - dict( - response_data='' - ), + {"uri": "/"}, + { + "response_data": '' + }, ) module_test.set_expect_requests( - dict(uri="/Test_File.txt"), - dict( - response_data="juicy stuff", - ), + {"uri": "/Test_File.txt"}, + { + "response_data": "juicy stuff", + }, ) module_test.set_expect_requests( - dict(uri="/Test_PDF"), - dict(response_data=self.pdf_data, headers={"Content-Type": "application/pdf"}), + {"uri": "/Test_PDF"}, + {"response_data": self.pdf_data, "headers": {"Content-Type": "application/pdf"}}, ) module_test.set_expect_requests( - dict(uri="/test.html"), - dict(response_data="", headers={"Content-Type": "text/html"}), + {"uri": "/test.html"}, + {"response_data": "", "headers": {"Content-Type": "text/html"}}, ) module_test.set_expect_requests( - dict(uri="/test2"), - dict(response_data="", headers={"Content-Type": "text/html"}), + {"uri": "/test2"}, + {"response_data": "", "headers": {"Content-Type": "text/html"}}, ) def check(self, module_test, events): @@ -60,3 +60,28 @@ def check(self, module_test, events): # we don't want html files html_files = list(download_dir.glob("*.html")) assert len(html_files) == 0, "HTML files were erroneously downloaded" + + +class TestFileDownloadLongFilename(TestFileDownload): + async def setup_after_prep(self, module_test): + module_test.set_expect_requests( + {"uri": "/"}, + { + "response_data": '' + }, + ) + module_test.set_expect_requests( + { + "uri": "/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity/blacklanternsecurity.txt" + }, + { + "response_data": "juicy stuff", + }, + ) + + def check(self, module_test, events): + filesystem_events = [e for e in events if e.type == "FILESYSTEM"] + assert len(filesystem_events) == 1 + file_path = Path(filesystem_events[0].data["path"]) + assert file_path.is_file(), f"File not found at {file_path}" + assert file_path.read_text() == "juicy stuff", f"File at {file_path} does not contain the correct content" diff --git a/bbot/test/test_step_2/module_tests/test_module_git_clone.py b/bbot/test/test_step_2/module_tests/test_module_git_clone.py index 15bc54fb37..d6a994402a 100644 --- a/bbot/test/test_step_2/module_tests/test_module_git_clone.py +++ b/bbot/test/test_step_2/module_tests/test_module_git_clone.py @@ -202,7 +202,7 @@ def check(self, module_test, events): ] assert 1 == len(filesystem_events), "Failed to git clone CODE_REPOSITORY" # make sure the binary blob isn't here - assert not any(["blob" in e.data for e in [e for e in events if e.type == "FILESYSTEM"]]) + assert not any("blob" in e.data for e in [e for e in events if e.type == "FILESYSTEM"]) filesystem_event = filesystem_events[0] folder = Path(filesystem_event.data["path"]) assert folder.is_dir(), "Destination folder doesn't exist" @@ -217,7 +217,7 @@ class TestGit_CloneWithBlob(TestGit_Clone): def check(self, module_test, events): filesystem_events = [e for e in events if e.type == "FILESYSTEM"] assert len(filesystem_events) == 1 - assert all(["blob" in e.data for e in filesystem_events]) + assert all("blob" in e.data for e in filesystem_events) filesystem_event = filesystem_events[0] blob = filesystem_event.data["blob"] tar_bytes = base64.b64decode(blob) diff --git a/bbot/test/test_step_2/module_tests/test_module_github_codesearch.py b/bbot/test/test_step_2/module_tests/test_module_github_codesearch.py index 03c519a8cf..7ede43b7e9 100644 --- a/bbot/test/test_step_2/module_tests/test_module_github_codesearch.py +++ b/bbot/test/test_step_2/module_tests/test_module_github_codesearch.py @@ -3,17 +3,35 @@ class TestGithub_Codesearch(ModuleTestBase): config_overrides = { - "modules": {"github_codesearch": {"api_key": "asdf", "limit": 1}}, + "modules": { + "github_codesearch": {"api_key": "asdf", "limit": 1}, + "trufflehog": {"only_verified": False}, + }, "omit_event_types": [], "scope": {"report_distance": 2}, } - modules_overrides = ["github_codesearch", "httpx", "secretsdb"] + modules_overrides = ["github_codesearch", "httpx", "trufflehog"] github_file_endpoint = ( "/projectdiscovery/nuclei/06f242e5fce3439b7418877676810cbf57934875/v2/cmd/cve-annotate/main.go" ) github_file_url = f"http://127.0.0.1:8888{github_file_endpoint}" - github_file_content = "-----BEGIN PGP PRIVATE KEY BLOCK-----" + github_file_content = """-----BEGIN PRIVATE KEY----- +MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOBY2pd9PSQvuxqu +WXFNVgILTWuUc721Wc2sFNvp4beowhUe1lfxaq5ZfCJcz7z4QsqFhOeks69O9UIb +oiOTDocPDog9PHO8yZXopHm0StFZvSjjKSNuFvy/WopPTGpxUZ5boCaF1CXumY7W +FL+jIap5faimLL9prIwaQKBwv80lAgMBAAECgYEAxvpHtgCgD849tqZYMgOTevCn +U/kwxltoMOClB39icNA+gxj8prc6FTTMwnVq0oGmS5UskX8k1yHCqUV1AvRU9o+q +I8L8a3F3TQKQieI/YjiUNK8A87bKkaiN65ooOnhT+I3ZjZMPR5YEyycimMp22jsv +LyX/35J/wf1rNiBs/YECQQDvtxgmMhE+PeajXqw1w2C3Jds27hI3RPDnamEyWr/L +KkSplbKTF6FuFDYOFdJNPrfxm1tx2MZ2cBfs+h/GnCJVAkEA75Z9w7q8obbqGBHW +9bpuFvLjW7bbqO7HBuXYX9zQcZL6GSArFP0ba5lhgH1qsVQfxVWVyiV9/chme7xc +ljfvkQJBAJ7MpSPQcRnRefNp6R0ok+5gFqt55PlWI1y6XS81bO7Szm+laooE0n0Q +yIpmLE3dqY9VgquVlkupkD/9poU0s40CQD118ZVAVht1/N9n1Cj9RjiE3mYspnTT +rCLM25Db6Gz6M0Y2xlaAB4S2uBhqE/Chj/TjW6WbsJJl0kRzsZynhMECQFYKiM1C +T4LB26ynW00VE8z4tEWSoYt4/Vn/5wFhalVjzoSJ8Hm2qZiObRYLQ1m0X4KnkShk +Gnl54dJHT+EhlfY= +-----END PRIVATE KEY-----""" async def setup_before_prep(self, module_test): expect_args = {"method": "GET", "uri": self.github_file_endpoint} @@ -82,5 +100,5 @@ def check(self, module_test, events): ] ), "Failed to visit URL" assert [ - e for e in events if e.type == "FINDING" and str(e.module) == "secretsdb" + e for e in events if e.type == "FINDING" and str(e.module) == "trufflehog" ], "Failed to find secret in repo file" diff --git a/bbot/test/test_step_2/module_tests/test_module_gowitness.py b/bbot/test/test_step_2/module_tests/test_module_gowitness.py index b6439dbbb1..b4fead9221 100644 --- a/bbot/test/test_step_2/module_tests/test_module_gowitness.py +++ b/bbot/test/test_step_2/module_tests/test_module_gowitness.py @@ -1,3 +1,5 @@ +from pathlib import Path + from .base import ModuleTestBase @@ -27,8 +29,8 @@ async def setup_after_prep(self, module_test): "headers": {"Server": "Apache/2.4.41 (Ubuntu)"}, } module_test.set_expect_requests(respond_args=respond_args) - request_args = dict(uri="/blacklanternsecurity") - respond_args = dict(response_data="""blacklanternsecurity github BBOT is lifeBBOT is life", + "headers": {"Server": "Apache/2.4.41 (Ubuntu)"}, + } + module_test.set_expect_requests(request_args, respond_args) + + def check(self, module_test, events): + webscreenshots = [e for e in events if e.type == "WEBSCREENSHOT"] + assert webscreenshots, "failed to raise WEBSCREENSHOT events" + assert len(webscreenshots) == 1 + webscreenshot = webscreenshots[0] + filename = Path(webscreenshot.data["path"]) + # sadly this file doesn't exist because gowitness doesn't truncate properly + assert not filename.exists() diff --git a/bbot/test/test_step_2/module_tests/test_module_host_header.py b/bbot/test/test_step_2/module_tests/test_module_host_header.py index b71a31b1d4..a2d69e9b57 100644 --- a/bbot/test/test_step_2/module_tests/test_module_host_header.py +++ b/bbot/test/test_step_2/module_tests/test_module_host_header.py @@ -31,7 +31,7 @@ def request_handler(self, request): if subdomain_tag_overrides: return Response(f"Alive, host is: {subdomain_tag}.{self.fake_host}", status=200) - return Response(f"Alive, host is: defaulthost.com", status=200) + return Response("Alive, host is: defaulthost.com", status=200) async def setup_before_prep(self, module_test): self.interactsh_mock_instance = module_test.mock_interactsh("host_header") diff --git a/bbot/test/test_step_2/module_tests/test_module_http.py b/bbot/test/test_step_2/module_tests/test_module_http.py index 43b7189adf..2bc99f5ddf 100644 --- a/bbot/test/test_step_2/module_tests/test_module_http.py +++ b/bbot/test/test_step_2/module_tests/test_module_http.py @@ -48,10 +48,10 @@ async def custom_callback(request): ) def check(self, module_test, events): - assert self.got_event == True - assert self.headers_correct == True - assert self.method_correct == True - assert self.url_correct == True + assert self.got_event is True + assert self.headers_correct is True + assert self.method_correct is True + assert self.url_correct is True class TestHTTPSIEMFriendly(TestHTTP): diff --git a/bbot/test/test_step_2/module_tests/test_module_httpx.py b/bbot/test/test_step_2/module_tests/test_module_httpx.py index c05b6842d6..450de75050 100644 --- a/bbot/test/test_step_2/module_tests/test_module_httpx.py +++ b/bbot/test/test_step_2/module_tests/test_module_httpx.py @@ -31,11 +31,11 @@ class TestHTTPXBase(ModuleTestBase): """ async def setup_after_prep(self, module_test): - request_args = dict(uri="/", headers={"test": "header"}) - respond_args = dict(response_data=self.html_without_login) + request_args = {"uri": "/", "headers": {"test": "header"}} + respond_args = {"response_data": self.html_without_login} module_test.set_expect_requests(request_args, respond_args) - request_args = dict(uri="/url", headers={"test": "header"}) - respond_args = dict(response_data=self.html_with_login) + request_args = {"uri": "/url", "headers": {"test": "header"}} + respond_args = {"response_data": self.html_with_login} module_test.set_expect_requests(request_args, respond_args) def check(self, module_test, events): @@ -44,7 +44,7 @@ def check(self, module_test, events): for e in events: if e.type == "HTTP_RESPONSE": if e.data["path"] == "/": - assert not "login-page" in e.tags + assert "login-page" not in e.tags open_port = True elif e.data["path"] == "/url": assert "login-page" in e.tags @@ -124,8 +124,8 @@ def check(self, module_test, events): assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/"]) assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/test.aspx"]) assert 1 == len([e for e in events if e.type == "URL" and e.data == "http://127.0.0.1:8888/test.txt"]) - assert not any([e for e in events if "URL" in e.type and ".svg" in e.data]) - assert not any([e for e in events if "URL" in e.type and ".woff" in e.data]) + assert not any(e for e in events if "URL" in e.type and ".svg" in e.data) + assert not any(e for e in events if "URL" in e.type and ".woff" in e.data) class TestHTTPX_querystring_removed(ModuleTestBase): diff --git a/bbot/test/test_step_2/module_tests/test_module_hunterio.py b/bbot/test/test_step_2/module_tests/test_module_hunterio.py index 263f304a38..ecd71957ab 100644 --- a/bbot/test/test_step_2/module_tests/test_module_hunterio.py +++ b/bbot/test/test_step_2/module_tests/test_module_hunterio.py @@ -17,7 +17,7 @@ async def setup_before_prep(self, module_test): "reset_date": "1917-05-23", "team_id": 1234, "calls": { - "_deprecation_notice": "Sums the searches and the verifications, giving an unprecise look of the available requests", + "_deprecation_notice": "Sums the searches and the verifications, giving an imprecise look of the available requests", "used": 999, "available": 2000, }, diff --git a/bbot/test/test_step_2/module_tests/test_module_leakix.py b/bbot/test/test_step_2/module_tests/test_module_leakix.py index aad4a095c4..f87dba6b50 100644 --- a/bbot/test/test_step_2/module_tests/test_module_leakix.py +++ b/bbot/test/test_step_2/module_tests/test_module_leakix.py @@ -6,12 +6,12 @@ class TestLeakIX(ModuleTestBase): async def setup_before_prep(self, module_test): module_test.httpx_mock.add_response( - url="https://leakix.net/host/1.2.3.4.5", + url="https://leakix.net/host/1.1.1.1", match_headers={"api-key": "asdf"}, json={"title": "Not Found", "description": "Host not found"}, ) module_test.httpx_mock.add_response( - url=f"https://leakix.net/api/subdomains/blacklanternsecurity.com", + url="https://leakix.net/api/subdomains/blacklanternsecurity.com", match_headers={"api-key": "asdf"}, json=[ { @@ -31,7 +31,11 @@ class TestLeakIX_NoAPIKey(ModuleTestBase): async def setup_before_prep(self, module_test): module_test.httpx_mock.add_response( - url=f"https://leakix.net/api/subdomains/blacklanternsecurity.com", + url="https://leakix.net/host/1.1.1.1", + json={"title": "Not Found", "description": "Host not found"}, + ) + module_test.httpx_mock.add_response( + url="https://leakix.net/api/subdomains/blacklanternsecurity.com", json=[ { "subdomain": "asdf.blacklanternsecurity.com", diff --git a/bbot/test/test_step_2/module_tests/test_module_mysql.py b/bbot/test/test_step_2/module_tests/test_module_mysql.py new file mode 100644 index 0000000000..4867c568d5 --- /dev/null +++ b/bbot/test/test_step_2/module_tests/test_module_mysql.py @@ -0,0 +1,76 @@ +import asyncio +import time + +from .base import ModuleTestBase + + +class TestMySQL(ModuleTestBase): + targets = ["evilcorp.com"] + skip_distro_tests = True + + async def setup_before_prep(self, module_test): + process = await asyncio.create_subprocess_exec( + "docker", + "run", + "--name", + "bbot-test-mysql", + "--rm", + "-e", + "MYSQL_ROOT_PASSWORD=bbotislife", + "-e", + "MYSQL_DATABASE=bbot", + "-p", + "3306:3306", + "-d", + "mysql", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await process.communicate() + + import aiomysql + + # wait for the container to start + start_time = time.time() + while True: + try: + conn = await aiomysql.connect(user="root", password="bbotislife", db="bbot", host="localhost") + conn.close() + break + except Exception as e: + if time.time() - start_time > 60: # timeout after 60 seconds + self.log.error("MySQL server did not start in time.") + raise e + await asyncio.sleep(1) + + if process.returncode != 0: + self.log.error(f"Failed to start MySQL server: {stderr.decode()}") + + async def check(self, module_test, events): + import aiomysql + + # Connect to the MySQL database + conn = await aiomysql.connect(user="root", password="bbotislife", db="bbot", host="localhost") + + try: + async with conn.cursor() as cur: + await cur.execute("SELECT * FROM event") + events = await cur.fetchall() + assert len(events) == 3, "No events found in MySQL database" + + await cur.execute("SELECT * FROM scan") + scans = await cur.fetchall() + assert len(scans) == 1, "No scans found in MySQL database" + + await cur.execute("SELECT * FROM target") + targets = await cur.fetchall() + assert len(targets) == 1, "No targets found in MySQL database" + finally: + conn.close() + process = await asyncio.create_subprocess_exec( + "docker", "stop", "bbot-test-mysql", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await process.communicate() + + if process.returncode != 0: + raise Exception(f"Failed to stop MySQL server: {stderr.decode()}") diff --git a/bbot/test/test_step_2/module_tests/test_module_myssl.py b/bbot/test/test_step_2/module_tests/test_module_myssl.py index 34b9b9972e..b39f2711d5 100644 --- a/bbot/test/test_step_2/module_tests/test_module_myssl.py +++ b/bbot/test/test_step_2/module_tests/test_module_myssl.py @@ -5,7 +5,7 @@ class TestMySSL(ModuleTestBase): async def setup_after_prep(self, module_test): module_test.module.abort_if = lambda e: False module_test.httpx_mock.add_response( - url=f"https://myssl.com/api/v1/discover_sub_domain?domain=blacklanternsecurity.com", + url="https://myssl.com/api/v1/discover_sub_domain?domain=blacklanternsecurity.com", json={ "code": 0, "data": [ diff --git a/bbot/test/test_step_2/module_tests/test_module_neo4j.py b/bbot/test/test_step_2/module_tests/test_module_neo4j.py index 98107481ad..c5df1e4748 100644 --- a/bbot/test/test_step_2/module_tests/test_module_neo4j.py +++ b/bbot/test/test_step_2/module_tests/test_module_neo4j.py @@ -41,4 +41,4 @@ async def close(self): module_test.monkeypatch.setattr("neo4j.AsyncGraphDatabase.driver", MockDriver) def check(self, module_test, events): - assert self.neo4j_used == True + assert self.neo4j_used is True diff --git a/bbot/test/test_step_2/module_tests/test_module_newsletters.py b/bbot/test/test_step_2/module_tests/test_module_newsletters.py index d3712be5c0..c5edd25141 100644 --- a/bbot/test/test_step_2/module_tests/test_module_newsletters.py +++ b/bbot/test/test_step_2/module_tests/test_module_newsletters.py @@ -10,16 +10,16 @@ class TestNewsletters(ModuleTestBase): modules_overrides = ["speculate", "httpx", "newsletters"] html_with_newsletter = """ - """ @@ -33,11 +33,11 @@ class TestNewsletters(ModuleTestBase): """ async def setup_after_prep(self, module_test): - request_args = dict(uri="/found", headers={"test": "header"}) - respond_args = dict(response_data=self.html_with_newsletter) + request_args = {"uri": "/found", "headers": {"test": "header"}} + respond_args = {"response_data": self.html_with_newsletter} module_test.set_expect_requests(request_args, respond_args) - request_args = dict(uri="/missing", headers={"test": "header"}) - respond_args = dict(response_data=self.html_without_newsletter) + request_args = {"uri": "/missing", "headers": {"test": "header"}} + respond_args = {"response_data": self.html_without_newsletter} module_test.set_expect_requests(request_args, respond_args) def check(self, module_test, events): @@ -53,5 +53,5 @@ def check(self, module_test, events): # Verify Negative Result (should skip this statement if correct) elif event.data["url"] == self.missing_tgt: missing = False - assert found, f"NEWSLETTER 'Found' Error - Expect status of True but got False" - assert missing, f"NEWSLETTER 'Missing' Error - Expect status of True but got False" + assert found, "NEWSLETTER 'Found' Error - Expect status of True but got False" + assert missing, "NEWSLETTER 'Missing' Error - Expect status of True but got False" diff --git a/bbot/test/test_step_2/module_tests/test_module_nmap_xml.py b/bbot/test/test_step_2/module_tests/test_module_nmap_xml.py new file mode 100644 index 0000000000..b88595be01 --- /dev/null +++ b/bbot/test/test_step_2/module_tests/test_module_nmap_xml.py @@ -0,0 +1,85 @@ +import xml.etree.ElementTree as ET + +from bbot.modules.base import BaseModule +from .base import ModuleTestBase + + +class TestNmap_XML(ModuleTestBase): + modules_overrides = ["nmap_xml", "speculate"] + targets = ["blacklanternsecurity.com", "127.0.0.3"] + config_overrides = {"dns": {"minimal": False}} + + class DummyModule(BaseModule): + watched_events = ["OPEN_TCP_PORT"] + _name = "dummy_module" + + async def handle_event(self, event): + if event.port == 80: + await self.emit_event( + {"host": str(event.host), "port": event.port, "protocol": "http", "banner": "Apache"}, + "PROTOCOL", + parent=event, + ) + elif event.port == 443: + await self.emit_event( + {"host": str(event.host), "port": event.port, "protocol": "https"}, "PROTOCOL", parent=event + ) + + async def setup_before_prep(self, module_test): + self.dummy_module = self.DummyModule(module_test.scan) + module_test.scan.modules["dummy_module"] = self.dummy_module + await module_test.mock_dns( + { + "blacklanternsecurity.com": {"A": ["127.0.0.1", "127.0.0.2"]}, + "3.0.0.127.in-addr.arpa": {"PTR": ["www.blacklanternsecurity.com"]}, + "www.blacklanternsecurity.com": {"A": ["127.0.0.1"]}, + } + ) + + def check(self, module_test, events): + nmap_xml_file = module_test.scan.modules["nmap_xml"].output_file + nmap_xml = open(nmap_xml_file).read() + + # Parse the XML + root = ET.fromstring(nmap_xml) + + # Expected IP addresses + expected_ips = {"127.0.0.1", "127.0.0.2", "127.0.0.3"} + found_ips = set() + + # Iterate over each host in the XML + for host in root.findall("host"): + # Get the IP address + address = host.find("address").get("addr") + found_ips.add(address) + + # Get hostnames if available + hostnames = sorted([hostname.get("name") for hostname in host.findall(".//hostname")]) + + # Get open ports and services + ports = [] + for port in host.findall(".//port"): + port_id = port.get("portid") + state = port.find("state").get("state") + if state == "open": + service_name = port.find("service").get("name") + service_product = port.find("service").get("product", "") + service_extrainfo = port.find("service").get("extrainfo", "") + ports.append((port_id, service_name, service_product, service_extrainfo)) + + # Sort ports for consistency + ports.sort() + + # Assertions + if address == "127.0.0.1": + assert hostnames == ["blacklanternsecurity.com", "www.blacklanternsecurity.com"] + assert ports == sorted([("80", "http", "Apache", "Apache"), ("443", "https", "", "")]) + elif address == "127.0.0.2": + assert hostnames == sorted(["blacklanternsecurity.com"]) + assert ports == sorted([("80", "http", "Apache", "Apache"), ("443", "https", "", "")]) + elif address == "127.0.0.3": + assert hostnames == [] # No hostnames for this IP + assert ports == sorted([("80", "http", "Apache", "Apache"), ("443", "https", "", "")]) + + # Assert that all expected IPs were found + assert found_ips == expected_ips diff --git a/bbot/test/test_step_2/module_tests/test_module_ntlm.py b/bbot/test/test_step_2/module_tests/test_module_ntlm.py index 790f2e0d22..7b834ef2f9 100644 --- a/bbot/test/test_step_2/module_tests/test_module_ntlm.py +++ b/bbot/test/test_step_2/module_tests/test_module_ntlm.py @@ -7,16 +7,17 @@ class TestNTLM(ModuleTestBase): config_overrides = {"modules": {"ntlm": {"try_all": True}}} async def setup_after_prep(self, module_test): - request_args = dict(uri="/", headers={"test": "header"}) + request_args = {"uri": "/", "headers": {"test": "header"}} module_test.set_expect_requests(request_args, {}) - request_args = dict( - uri="/oab/", headers={"Authorization": "NTLM TlRMTVNTUAABAAAAl4II4gAAAAAAAAAAAAAAAAAAAAAKAGFKAAAADw=="} - ) - respond_args = dict( - headers={ + request_args = { + "uri": "/oab/", + "headers": {"Authorization": "NTLM TlRMTVNTUAABAAAAl4II4gAAAAAAAAAAAAAAAAAAAAAKAGFKAAAADw=="}, + } + respond_args = { + "headers": { "WWW-Authenticate": "NTLM TlRMTVNTUAACAAAABgAGADgAAAAVgoni89aZT4Q0mH0AAAAAAAAAAHYAdgA+AAAABgGxHQAAAA9WAE4ATwACAAYAVgBOAE8AAQAKAEUAWABDADAAMQAEABIAdgBuAG8ALgBsAG8AYwBhAGwAAwAeAEUAWABDADAAMQAuAHYAbgBvAC4AbABvAGMAYQBsAAUAEgB2AG4AbwAuAGwAbwBjAGEAbAAHAAgAXxo0p/6L2QEAAAAA" } - ) + } module_test.set_expect_requests(request_args, respond_args) def check(self, module_test, events): diff --git a/bbot/test/test_step_2/module_tests/test_module_oauth.py b/bbot/test/test_step_2/module_tests/test_module_oauth.py index 85fe4f9172..1e7078e840 100644 --- a/bbot/test/test_step_2/module_tests/test_module_oauth.py +++ b/bbot/test/test_step_2/module_tests/test_module_oauth.py @@ -167,7 +167,7 @@ class TestOAUTH(ModuleTestBase): async def setup_after_prep(self, module_test): await module_test.mock_dns({"evilcorp.com": {"A": ["127.0.0.1"]}}) module_test.httpx_mock.add_response( - url=f"https://login.microsoftonline.com/getuserrealm.srf?login=test@evilcorp.com", + url="https://login.microsoftonline.com/getuserrealm.srf?login=test@evilcorp.com", json=Azure_Realm.response_json, ) module_test.httpx_mock.add_response( diff --git a/bbot/test/test_step_2/module_tests/test_module_otx.py b/bbot/test/test_step_2/module_tests/test_module_otx.py index 1c41cd962d..9c533ca96e 100644 --- a/bbot/test/test_step_2/module_tests/test_module_otx.py +++ b/bbot/test/test_step_2/module_tests/test_module_otx.py @@ -4,7 +4,7 @@ class TestOTX(ModuleTestBase): async def setup_after_prep(self, module_test): module_test.httpx_mock.add_response( - url=f"https://otx.alienvault.com/api/v1/indicators/domain/blacklanternsecurity.com/passive_dns", + url="https://otx.alienvault.com/api/v1/indicators/domain/blacklanternsecurity.com/passive_dns", json={ "passive_dns": [ { diff --git a/bbot/test/test_step_2/module_tests/test_module_paramminer_cookies.py b/bbot/test/test_step_2/module_tests/test_module_paramminer_cookies.py index 58d76ff198..6c4ecda526 100644 --- a/bbot/test/test_step_2/module_tests/test_module_paramminer_cookies.py +++ b/bbot/test/test_step_2/module_tests/test_module_paramminer_cookies.py @@ -28,7 +28,7 @@ async def setup_after_prep(self, module_test): module_test.monkeypatch.setattr( helper.HttpCompare, "gen_cache_buster", lambda *args, **kwargs: {"AAAAAA": "1"} ) - expect_args = dict(headers={"Cookie": "admincookie=AAAAAAAAAAAAAA"}) + expect_args = {"headers": {"Cookie": "admincookie=AAAAAAAAAAAAAA"}} respond_args = {"response_data": self.cookies_body_match} module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) @@ -36,7 +36,6 @@ async def setup_after_prep(self, module_test): module_test.set_expect_requests(respond_args=respond_args) def check(self, module_test, events): - found_reflected_cookie = False false_positive_match = False diff --git a/bbot/test/test_step_2/module_tests/test_module_paramminer_getparams.py b/bbot/test/test_step_2/module_tests/test_module_paramminer_getparams.py index 1bf290c416..e74e067a38 100644 --- a/bbot/test/test_step_2/module_tests/test_module_paramminer_getparams.py +++ b/bbot/test/test_step_2/module_tests/test_module_paramminer_getparams.py @@ -89,7 +89,6 @@ async def setup_after_prep(self, module_test): module_test.set_expect_requests(respond_args=respond_args) def check(self, module_test, events): - emitted_boring_parameter = False for e in events: if e.type == "WEB_PARAMETER": @@ -106,7 +105,6 @@ class TestParamminer_Getparams_boring_on(TestParamminer_Getparams_boring_off): } def check(self, module_test, events): - emitted_boring_parameter = False for e in events: @@ -160,15 +158,12 @@ async def setup_after_prep(self, module_test): module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) def check(self, module_test, events): - excavate_extracted_web_parameter = False found_hidden_getparam_recycled = False emitted_excavate_paramminer_duplicate = False for e in events: - if e.type == "WEB_PARAMETER": - if ( "http://127.0.0.1:8888/test2.php" in e.data["url"] and "HTTP Extracted Parameter [abcd1234] (HTML Tags Submodule)" in e.data["description"] @@ -213,7 +208,6 @@ class TestParamminer_Getparams_xmlspeculative(Paramminer_Headers): """ async def setup_after_prep(self, module_test): - module_test.scan.modules["paramminer_getparams"].rand_string = lambda *args, **kwargs: "AAAAAAAAAAAAAA" module_test.monkeypatch.setattr( helper.HttpCompare, "gen_cache_buster", lambda *args, **kwargs: {"AAAAAA": "1"} diff --git a/bbot/test/test_step_2/module_tests/test_module_paramminer_headers.py b/bbot/test/test_step_2/module_tests/test_module_paramminer_headers.py index 0f66e5e877..9d04b9d22a 100644 --- a/bbot/test/test_step_2/module_tests/test_module_paramminer_headers.py +++ b/bbot/test/test_step_2/module_tests/test_module_paramminer_headers.py @@ -31,7 +31,7 @@ async def setup_after_prep(self, module_test): module_test.monkeypatch.setattr( helper.HttpCompare, "gen_cache_buster", lambda *args, **kwargs: {"AAAAAA": "1"} ) - expect_args = dict(headers={"tracestate": "AAAAAAAAAAAAAA"}) + expect_args = {"headers": {"tracestate": "AAAAAAAAAAAAAA"}} respond_args = {"response_data": self.headers_body_match} module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) @@ -39,7 +39,6 @@ async def setup_after_prep(self, module_test): module_test.set_expect_requests(respond_args=respond_args) def check(self, module_test, events): - found_reflected_header = False false_positive_match = False @@ -60,7 +59,6 @@ class TestParamminer_Headers(Paramminer_Headers): class TestParamminer_Headers_noreflection(Paramminer_Headers): - found_nonreflected_header = False headers_body_match = """ @@ -82,7 +80,6 @@ def check(self, module_test, events): class TestParamminer_Headers_extract(Paramminer_Headers): - modules_overrides = ["httpx", "paramminer_headers", "excavate"] config_overrides = { "modules": { @@ -115,7 +112,7 @@ async def setup_after_prep(self, module_test): module_test.monkeypatch.setattr( helper.HttpCompare, "gen_cache_buster", lambda *args, **kwargs: {"AAAAAA": "1"} ) - expect_args = dict(headers={"foo": "AAAAAAAAAAAAAA"}) + expect_args = {"headers": {"foo": "AAAAAAAAAAAAAA"}} respond_args = {"response_data": self.headers_body_match} module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) @@ -123,7 +120,6 @@ async def setup_after_prep(self, module_test): module_test.set_expect_requests(respond_args=respond_args) def check(self, module_test, events): - excavate_extracted_web_parameter = False used_recycled_parameter = False @@ -139,17 +135,14 @@ def check(self, module_test, events): class TestParamminer_Headers_extract_norecycle(TestParamminer_Headers_extract): - modules_overrides = ["httpx", "excavate"] config_overrides = {} async def setup_after_prep(self, module_test): - respond_args = {"response_data": self.headers_body} module_test.set_expect_requests(respond_args=respond_args) def check(self, module_test, events): - excavate_extracted_web_parameter = False for e in events: diff --git a/bbot/test/test_step_2/module_tests/test_module_passivetotal.py b/bbot/test/test_step_2/module_tests/test_module_passivetotal.py index 9048a41e0a..55be613468 100644 --- a/bbot/test/test_step_2/module_tests/test_module_passivetotal.py +++ b/bbot/test/test_step_2/module_tests/test_module_passivetotal.py @@ -2,15 +2,17 @@ class TestPassiveTotal(ModuleTestBase): - config_overrides = {"modules": {"passivetotal": {"username": "jon@bls.fakedomain", "api_key": "asdf"}}} + config_overrides = {"modules": {"passivetotal": {"api_key": "jon@bls.fakedomain:asdf"}}} async def setup_before_prep(self, module_test): module_test.httpx_mock.add_response( url="https://api.passivetotal.org/v2/account/quota", + match_headers={"Authorization": "Basic am9uQGJscy5mYWtlZG9tYWluOmFzZGY="}, json={"user": {"counts": {"search_api": 10}, "limits": {"search_api": 20}}}, ) module_test.httpx_mock.add_response( url="https://api.passivetotal.org/v2/enrichment/subdomains?query=blacklanternsecurity.com", + match_headers={"Authorization": "Basic am9uQGJscy5mYWtlZG9tYWluOmFzZGY="}, json={"subdomains": ["asdf"]}, ) diff --git a/bbot/test/test_step_2/module_tests/test_module_pgp.py b/bbot/test/test_step_2/module_tests/test_module_pgp.py index e6f122dd93..dc493d7b52 100644 --- a/bbot/test/test_step_2/module_tests/test_module_pgp.py +++ b/bbot/test/test_step_2/module_tests/test_module_pgp.py @@ -9,10 +9,10 @@ class TestPGP(ModuleTestBase):

Search results for 'blacklanternsecurity.com'

Type bits/keyID            cr. time   exp time   key expir
 
diff --git a/bbot/test/test_step_2/module_tests/test_module_portscan.py b/bbot/test/test_step_2/module_tests/test_module_portscan.py index 56536cb5dd..06a2fcef40 100644 --- a/bbot/test/test_step_2/module_tests/test_module_portscan.py +++ b/bbot/test/test_step_2/module_tests/test_module_portscan.py @@ -21,7 +21,6 @@ class TestPortscan(ModuleTestBase): masscan_output_ping = """{ "ip": "8.8.8.8", "timestamp": "1719862594", "ports": [ {"port": 0, "proto": "icmp", "status": "open", "reason": "none", "ttl": 54} ] }""" async def setup_after_prep(self, module_test): - from bbot.modules.base import BaseModule class DummyModule(BaseModule): @@ -109,10 +108,12 @@ def check(self, module_test, events): if e.type == "DNS_NAME" and e.data == "dummy.asdf.evilcorp.net" and str(e.module) == "dummy_module" ] ) - assert 2 <= len([e for e in events if e.type == "IP_ADDRESS" and e.data == "8.8.8.8"]) <= 3 - assert 2 <= len([e for e in events if e.type == "IP_ADDRESS" and e.data == "8.8.4.4"]) <= 3 - assert 2 <= len([e for e in events if e.type == "IP_ADDRESS" and e.data == "8.8.4.5"]) <= 3 - assert 2 <= len([e for e in events if e.type == "IP_ADDRESS" and e.data == "8.8.4.6"]) <= 3 + # the reason these numbers aren't exactly predictable is because we can't predict which one arrives first + # to the portscan module. Sometimes, one that would normally be deduped is force-emitted because it led to a new open port. + assert 2 <= len([e for e in events if e.type == "IP_ADDRESS" and e.data == "8.8.8.8"]) <= 4 + assert 2 <= len([e for e in events if e.type == "IP_ADDRESS" and e.data == "8.8.4.4"]) <= 4 + assert 2 <= len([e for e in events if e.type == "IP_ADDRESS" and e.data == "8.8.4.5"]) <= 4 + assert 2 <= len([e for e in events if e.type == "IP_ADDRESS" and e.data == "8.8.4.6"]) <= 4 assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "8.8.8.8:443"]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "8.8.4.5:80"]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "8.8.4.6:631"]) @@ -121,7 +122,7 @@ def check(self, module_test, events): assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "asdf.evilcorp.net:80"]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "dummy.asdf.evilcorp.net:80"]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "dummy.evilcorp.com:631"]) - assert not any([e for e in events if e.type == "OPEN_TCP_PORT" and e.host == "dummy.www.evilcorp.com"]) + assert not any(e for e in events if e.type == "OPEN_TCP_PORT" and e.host == "dummy.www.evilcorp.com") class TestPortscanPingFirst(TestPortscan): @@ -135,7 +136,7 @@ def check(self, module_test, events): assert self.ping_runs == 1 open_port_events = [e for e in events if e.type == "OPEN_TCP_PORT"] assert len(open_port_events) == 3 - assert set([e.data for e in open_port_events]) == {"8.8.8.8:443", "evilcorp.com:443", "www.evilcorp.com:443"} + assert {e.data for e in open_port_events} == {"8.8.8.8:443", "evilcorp.com:443", "www.evilcorp.com:443"} class TestPortscanPingOnly(TestPortscan): @@ -153,4 +154,4 @@ def check(self, module_test, events): assert len(open_port_events) == 0 ip_events = [e for e in events if e.type == "IP_ADDRESS"] assert len(ip_events) == 1 - assert set([e.data for e in ip_events]) == {"8.8.8.8"} + assert {e.data for e in ip_events} == {"8.8.8.8"} diff --git a/bbot/test/test_step_2/module_tests/test_module_postgres.py b/bbot/test/test_step_2/module_tests/test_module_postgres.py new file mode 100644 index 0000000000..ea6c00210c --- /dev/null +++ b/bbot/test/test_step_2/module_tests/test_module_postgres.py @@ -0,0 +1,74 @@ +import time +import asyncio + +from .base import ModuleTestBase + + +class TestPostgres(ModuleTestBase): + targets = ["evilcorp.com"] + skip_distro_tests = True + + async def setup_before_prep(self, module_test): + process = await asyncio.create_subprocess_exec( + "docker", + "run", + "--name", + "bbot-test-postgres", + "--rm", + "-e", + "POSTGRES_PASSWORD=bbotislife", + "-e", + "POSTGRES_USER=postgres", + "-p", + "5432:5432", + "-d", + "postgres", + ) + + import asyncpg + + # wait for the container to start + start_time = time.time() + while True: + try: + # Connect to the default 'postgres' database to create 'bbot' + conn = await asyncpg.connect( + user="postgres", password="bbotislife", database="postgres", host="127.0.0.1" + ) + await conn.execute("CREATE DATABASE bbot") + await conn.close() + break + except asyncpg.exceptions.DuplicateDatabaseError: + # If the database already exists, break the loop + break + except Exception as e: + if time.time() - start_time > 60: # timeout after 60 seconds + self.log.error("PostgreSQL server did not start in time.") + raise e + await asyncio.sleep(1) + + if process.returncode != 0: + self.log.error("Failed to start PostgreSQL server") + + async def check(self, module_test, events): + import asyncpg + + # Connect to the PostgreSQL database + conn = await asyncpg.connect(user="postgres", password="bbotislife", database="bbot", host="127.0.0.1") + + try: + events = await conn.fetch("SELECT * FROM event") + assert len(events) == 3, "No events found in PostgreSQL database" + scans = await conn.fetch("SELECT * FROM scan") + assert len(scans) == 1, "No scans found in PostgreSQL database" + targets = await conn.fetch("SELECT * FROM target") + assert len(targets) == 1, "No targets found in PostgreSQL database" + finally: + await conn.close() + process = await asyncio.create_subprocess_exec( + "docker", "stop", "bbot-test-postgres", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await process.communicate() + + if process.returncode != 0: + raise Exception(f"Failed to stop PostgreSQL server: {stderr.decode()}") diff --git a/bbot/test/test_step_2/module_tests/test_module_rapiddns.py b/bbot/test/test_step_2/module_tests/test_module_rapiddns.py index 2b3d3aaf0a..df8d45fbd8 100644 --- a/bbot/test/test_step_2/module_tests/test_module_rapiddns.py +++ b/bbot/test/test_step_2/module_tests/test_module_rapiddns.py @@ -11,7 +11,7 @@ class TestRapidDNS(ModuleTestBase): async def setup_after_prep(self, module_test): module_test.module.abort_if = lambda e: False module_test.httpx_mock.add_response( - url=f"https://rapiddns.io/subdomain/blacklanternsecurity.com?full=1#result", text=self.web_body + url="https://rapiddns.io/subdomain/blacklanternsecurity.com?full=1#result", text=self.web_body ) def check(self, module_test, events): @@ -45,10 +45,10 @@ async def custom_callback(request): def check(self, module_test, events): assert module_test.module.api_failure_abort_threshold == 10 - assert module_test.module.errored == False + assert module_test.module.errored is False assert module_test.module._api_request_failures == 3 assert module_test.module.api_retries == 3 - assert set([e.data for e in events if e.type == "DNS_NAME"]) == {"blacklanternsecurity.com"} + assert {e.data for e in events if e.type == "DNS_NAME"} == {"blacklanternsecurity.com"} assert self.url_count == { "https://rapiddns.io/subdomain/blacklanternsecurity.com?full=1#result": 3, } @@ -59,10 +59,10 @@ class TestRapidDNSAbortThreshold2(TestRapidDNSAbortThreshold1): def check(self, module_test, events): assert module_test.module.api_failure_abort_threshold == 10 - assert module_test.module.errored == False + assert module_test.module.errored is False assert module_test.module._api_request_failures == 6 assert module_test.module.api_retries == 3 - assert set([e.data for e in events if e.type == "DNS_NAME"]) == {"blacklanternsecurity.com", "evilcorp.com"} + assert {e.data for e in events if e.type == "DNS_NAME"} == {"blacklanternsecurity.com", "evilcorp.com"} assert self.url_count == { "https://rapiddns.io/subdomain/blacklanternsecurity.com?full=1#result": 3, "https://rapiddns.io/subdomain/evilcorp.com?full=1#result": 3, @@ -74,10 +74,10 @@ class TestRapidDNSAbortThreshold3(TestRapidDNSAbortThreshold1): def check(self, module_test, events): assert module_test.module.api_failure_abort_threshold == 10 - assert module_test.module.errored == False + assert module_test.module.errored is False assert module_test.module._api_request_failures == 9 assert module_test.module.api_retries == 3 - assert set([e.data for e in events if e.type == "DNS_NAME"]) == { + assert {e.data for e in events if e.type == "DNS_NAME"} == { "blacklanternsecurity.com", "evilcorp.com", "evilcorp.net", @@ -94,10 +94,10 @@ class TestRapidDNSAbortThreshold4(TestRapidDNSAbortThreshold1): def check(self, module_test, events): assert module_test.module.api_failure_abort_threshold == 10 - assert module_test.module.errored == True + assert module_test.module.errored is True assert module_test.module._api_request_failures == 10 assert module_test.module.api_retries == 3 - assert set([e.data for e in events if e.type == "DNS_NAME"]) == { + assert {e.data for e in events if e.type == "DNS_NAME"} == { "blacklanternsecurity.com", "evilcorp.com", "evilcorp.net", diff --git a/bbot/test/test_step_2/module_tests/test_module_secretsdb.py b/bbot/test/test_step_2/module_tests/test_module_secretsdb.py deleted file mode 100644 index f735035bcc..0000000000 --- a/bbot/test/test_step_2/module_tests/test_module_secretsdb.py +++ /dev/null @@ -1,14 +0,0 @@ -from .base import ModuleTestBase - - -class TestSecretsDB(ModuleTestBase): - targets = ["http://127.0.0.1:8888"] - modules_overrides = ["httpx", "secretsdb"] - - async def setup_before_prep(self, module_test): - expect_args = {"method": "GET", "uri": "/"} - respond_args = {"response_data": "-----BEGIN PGP PRIVATE KEY BLOCK-----"} - module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) - - def check(self, module_test, events): - assert any(e.type == "FINDING" for e in events) diff --git a/bbot/test/test_step_2/module_tests/test_module_sitedossier.py b/bbot/test/test_step_2/module_tests/test_module_sitedossier.py index a5b57b8001..ed93307664 100644 --- a/bbot/test/test_step_2/module_tests/test_module_sitedossier.py +++ b/bbot/test/test_step_2/module_tests/test_module_sitedossier.py @@ -136,11 +136,11 @@ async def setup_after_prep(self, module_test): } ) module_test.httpx_mock.add_response( - url=f"http://www.sitedossier.com/parentdomain/evilcorp.com", + url="http://www.sitedossier.com/parentdomain/evilcorp.com", text=page1, ) module_test.httpx_mock.add_response( - url=f"http://www.sitedossier.com/parentdomain/evilcorp.com/101", + url="http://www.sitedossier.com/parentdomain/evilcorp.com/101", text=page2, ) diff --git a/bbot/test/test_step_2/module_tests/test_module_smuggler.py b/bbot/test/test_step_2/module_tests/test_module_smuggler.py index 7e076cf07e..fb86b9ae92 100644 --- a/bbot/test/test_step_2/module_tests/test_module_smuggler.py +++ b/bbot/test/test_step_2/module_tests/test_module_smuggler.py @@ -1,13 +1,13 @@ from .base import ModuleTestBase smuggler_text = r""" - ______ _ - / _____) | | - ( (____ ____ _ _ ____ ____| | _____ ____ + ______ _ + / _____) | | + ( (____ ____ _ _ ____ ____| | _____ ____ \____ \| \| | | |/ _ |/ _ | || ___ |/ ___) - _____) ) | | | |_| ( (_| ( (_| | || ____| | - (______/|_|_|_|____/ \___ |\___ |\_)_____)_| - (_____(_____| + _____) ) | | | |_| ( (_| ( (_| | || ____| | + (______/|_|_|_|____/ \___ |\___ |\_)_____)_| + (_____(_____| @defparam v1.1 @@ -16,13 +16,13 @@ [+] Endpoint : / [+] Configfile : default.py [+] Timeout : 5.0 seconds - [+] Cookies : 1 (Appending to the attack) - [nameprefix1] : Checking TECL... - [nameprefix1] : Checking CLTE... - [nameprefix1] : OK (TECL: 0.61 - 405) (CLTE: 0.62 - 405) - [tabprefix1] : Checking TECL...git - [tabprefix1] : Checking CLTE... - [tabprefix1] : Checking TECL... + [+] Cookies : 1 (Appending to the attack) + [nameprefix1] : Checking TECL... + [nameprefix1] : Checking CLTE... + [nameprefix1] : OK (TECL: 0.61 - 405) (CLTE: 0.62 - 405) + [tabprefix1] : Checking TECL...git + [tabprefix1] : Checking CLTE... + [tabprefix1] : Checking TECL... [tabprefix1] : Checking CLTE... [tabprefix1] : Checking TECL... [tabprefix1] : Checking CLTE... @@ -39,7 +39,7 @@ async def setup_after_prep(self, module_test): old_run_live = module_test.scan.helpers.run_live async def smuggler_mock_run_live(*command, **kwargs): - if not "smuggler" in command[0][1]: + if "smuggler" not in command[0][1]: async for l in old_run_live(*command, **kwargs): yield l else: diff --git a/bbot/test/test_step_2/module_tests/test_module_speculate.py b/bbot/test/test_step_2/module_tests/test_module_speculate.py index 8b6150919f..55db777e7b 100644 --- a/bbot/test/test_step_2/module_tests/test_module_speculate.py +++ b/bbot/test/test_step_2/module_tests/test_module_speculate.py @@ -62,10 +62,8 @@ def check(self, module_test, events): for e in module_test.scan.modules["dummy"].events: events_data.add(e.data) assert all( - [ - x in events_data - for x in ("evilcorp.com:80", "evilcorp.com:443", "asdf.evilcorp.com:80", "asdf.evilcorp.com:443") - ] + x in events_data + for x in ("evilcorp.com:80", "evilcorp.com:443", "asdf.evilcorp.com:80", "asdf.evilcorp.com:443") ) @@ -79,8 +77,6 @@ def check(self, module_test, events): for e in module_test.scan.modules["dummy"].events: events_data.add(e.data) assert not any( - [ - x in events_data - for x in ("evilcorp.com:80", "evilcorp.com:443", "asdf.evilcorp.com:80", "asdf.evilcorp.com:443") - ] + x in events_data + for x in ("evilcorp.com:80", "evilcorp.com:443", "asdf.evilcorp.com:80", "asdf.evilcorp.com:443") ) diff --git a/bbot/test/test_step_2/module_tests/test_module_splunk.py b/bbot/test/test_step_2/module_tests/test_module_splunk.py index d55ed17c27..8366a6289b 100644 --- a/bbot/test/test_step_2/module_tests/test_module_splunk.py +++ b/bbot/test/test_step_2/module_tests/test_module_splunk.py @@ -52,7 +52,7 @@ async def custom_callback(request): module_test.httpx_mock.add_response() def check(self, module_test, events): - assert self.got_event == True - assert self.headers_correct == True - assert self.method_correct == True - assert self.url_correct == True + assert self.got_event is True + assert self.headers_correct is True + assert self.method_correct is True + assert self.url_correct is True diff --git a/bbot/test/test_step_2/module_tests/test_module_sqlite.py b/bbot/test/test_step_2/module_tests/test_module_sqlite.py index 809d68c47a..ec80b7555d 100644 --- a/bbot/test/test_step_2/module_tests/test_module_sqlite.py +++ b/bbot/test/test_step_2/module_tests/test_module_sqlite.py @@ -10,9 +10,9 @@ def check(self, module_test, events): assert sqlite_output_file.exists(), "SQLite output file not found" with sqlite3.connect(sqlite_output_file) as db: cursor = db.cursor() - cursor.execute("SELECT * FROM event") - assert len(cursor.fetchall()) > 0, "No events found in SQLite database" - cursor.execute("SELECT * FROM scan") - assert len(cursor.fetchall()) > 0, "No scans found in SQLite database" - cursor.execute("SELECT * FROM target") - assert len(cursor.fetchall()) > 0, "No targets found in SQLite database" + results = cursor.execute("SELECT * FROM event").fetchall() + assert len(results) == 3, "No events found in SQLite database" + results = cursor.execute("SELECT * FROM scan").fetchall() + assert len(results) == 1, "No scans found in SQLite database" + results = cursor.execute("SELECT * FROM target").fetchall() + assert len(results) == 1, "No targets found in SQLite database" diff --git a/bbot/test/test_step_2/module_tests/test_module_subdomaincenter.py b/bbot/test/test_step_2/module_tests/test_module_subdomaincenter.py index 2ec5e03612..aa95473a48 100644 --- a/bbot/test/test_step_2/module_tests/test_module_subdomaincenter.py +++ b/bbot/test/test_step_2/module_tests/test_module_subdomaincenter.py @@ -4,7 +4,7 @@ class TestSubdomainCenter(ModuleTestBase): async def setup_after_prep(self, module_test): module_test.httpx_mock.add_response( - url=f"https://api.subdomain.center/?domain=blacklanternsecurity.com", + url="https://api.subdomain.center/?domain=blacklanternsecurity.com", json=["asdf.blacklanternsecurity.com", "zzzz.blacklanternsecurity.com"], ) diff --git a/bbot/test/test_step_2/module_tests/test_module_subdomains.py b/bbot/test/test_step_2/module_tests/test_module_subdomains.py index 65b9a8a031..e7fb494591 100644 --- a/bbot/test/test_step_2/module_tests/test_module_subdomains.py +++ b/bbot/test/test_step_2/module_tests/test_module_subdomains.py @@ -6,7 +6,7 @@ class TestSubdomains(ModuleTestBase): async def setup_after_prep(self, module_test): module_test.httpx_mock.add_response( - url=f"https://api.subdomain.center/?domain=blacklanternsecurity.com", + url="https://api.subdomain.center/?domain=blacklanternsecurity.com", json=["asdfasdf.blacklanternsecurity.com", "zzzzzzzz.blacklanternsecurity.com"], ) diff --git a/bbot/test/test_step_2/module_tests/test_module_telerik.py b/bbot/test/test_step_2/module_tests/test_module_telerik.py index 98c511f2ab..21c4d2b86b 100644 --- a/bbot/test/test_step_2/module_tests/test_module_telerik.py +++ b/bbot/test/test_step_2/module_tests/test_module_telerik.py @@ -11,7 +11,7 @@ async def setup_before_prep(self, module_test): # Simulate Telerik.Web.UI.WebResource.axd?type=rau detection expect_args = {"method": "GET", "uri": "/Telerik.Web.UI.WebResource.axd", "query_string": "type=rau"} respond_args = { - "response_data": '{ "message" : "RadAsyncUpload handler is registered succesfully, however, it may not be accessed directly." }' + "response_data": '{ "message" : "RadAsyncUpload handler is registered successfully, however, it may not be accessed directly." }' } module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) diff --git a/bbot/test/test_step_2/module_tests/test_module_trufflehog.py b/bbot/test/test_step_2/module_tests/test_module_trufflehog.py index 46798dd94e..7f78977f1e 100644 --- a/bbot/test/test_step_2/module_tests/test_module_trufflehog.py +++ b/bbot/test/test_step_2/module_tests/test_module_trufflehog.py @@ -1144,7 +1144,7 @@ def check(self, module_test, events): assert content == self.file_content, "File content doesn't match" filesystem_events = [e.parent for e in vuln_events] assert len(filesystem_events) == 4 - assert all([e.type == "FILESYSTEM" for e in filesystem_events]) + assert all(e.type == "FILESYSTEM" for e in filesystem_events) assert 1 == len( [ e @@ -1193,7 +1193,7 @@ def check(self, module_test, events): or e.data["host"] == "github.com" or e.data["host"] == "www.postman.com" ) - and "Potential Secret Found." in e.data["description"] + and "Possible Secret Found." in e.data["description"] and "Raw result: [https://admin:admin@internal.host.com]" in e.data["description"] ] # Trufflehog should find 4 unverifiable secrets, 1 from the github, 1 from the workflow log, 1 from the docker image and 1 from the postman. @@ -1206,7 +1206,7 @@ def check(self, module_test, events): assert content == self.file_content, "File content doesn't match" filesystem_events = [e.parent for e in finding_events] assert len(filesystem_events) == 4 - assert all([e.type == "FILESYSTEM" for e in filesystem_events]) + assert all(e.type == "FILESYSTEM" for e in filesystem_events) assert 1 == len( [ e @@ -1240,3 +1240,35 @@ def check(self, module_test, events): and Path(e.data["path"]).is_file() ] ), "Failed to find blacklanternsecurity postman workspace" + + +class TestTrufflehog_HTTPResponse(ModuleTestBase): + targets = ["http://127.0.0.1:8888"] + modules_overrides = ["httpx", "trufflehog"] + config_overrides = {"modules": {"trufflehog": {"only_verified": False}}} + + async def setup_before_prep(self, module_test): + expect_args = {"method": "GET", "uri": "/"} + respond_args = {"response_data": "https://admin:admin@internal.host.com"} + module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) + + def check(self, module_test, events): + assert any(e.type == "FINDING" for e in events) + + +class TestTrufflehog_RAWText(ModuleTestBase): + targets = ["http://127.0.0.1:8888/test.pdf"] + modules_overrides = ["httpx", "trufflehog", "filedownload", "extractous"] + config_overrides = {"modules": {"trufflehog": {"only_verified": False}}} + + async def setup_before_prep(self, module_test): + expect_args = {"method": "GET", "uri": "/test.pdf"} + respond_args = { + "response_data": b"%PDF-1.4\n%\xc7\xec\x8f\xa2\n%%Invocation: path/gs -P- -dSAFER -dCompatibilityLevel=1.4 -dWriteXRefStm=false -dWriteObjStms=false -q -P- -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sstdout=? -sOutputFile=? -P- -dSAFER -dCompatibilityLevel=1.4 -dWriteXRefStm=false -dWriteObjStms=false -\n5 0 obj\n<>\nstream\nx\x9c-\x8c\xb1\x0e\x82@\x10D\xfb\xfd\x8a-\xa1\xe0\xd8\xe5@\xe1*c\xb4\xb1\xd3lba,\xc8\x81\x82\xf1@\xe4\xfe?\x02\x92If\x92\x97\x99\x19\x90\x14#\xcdZ\xd3: |\xc2\x00\xbcP\\\xc3:\xdc\x0b\xc4\x97\xed\x0c\xe4\x01\xff2\xe36\xc5\x9c6Jk\x8d\xe2\xe0\x16\\\xeb\n\x0f\xb5E\xce\x913\x93\x15F3&\x94\xa4a\x94fD\x01\x87w9M7\xc5z3Q\x8cx\xd9'(\x15\x04\x8d\xf7\x9f\xd1\xc4qY\xb9\xb63\x8b\xef\xda\xce\xd7\xdf\xae|\xab\xa6\x1f\xbd\xb2\xbd\x0b\xe5\x05G\x81\xf3\xa4\x1f~q-\xc7endstream\nendobj\n6 0 obj\n155\nendobj\n4 0 obj\n<>\n/Contents 5 0 R\n>>\nendobj\n3 0 obj\n<< /Type /Pages /Kids [\n4 0 R\n] /Count 1\n>>\nendobj\n1 0 obj\n<>\nendobj\n11 0 obj\n<>\nendobj\n9 0 obj\n<>\nendobj\n7 0 obj\n<>\nendobj\n10 0 obj\n<>\nendobj\n12 0 obj\n<>stream\nx\x9c\x9dT{TS\xf7\x1d\xbf\x91ps\x8f\xa0\xb2\xdc\x06\x1f\xe8\xbdX[|\xa0\x85\xaa\xad\xa7\xf4\x14P\x1eG9\x05\x9c\xa2\x08\xb4\xee@\x88\xc83\x08\x04\x84\x80\x84@B\xd3\x1f84!@\x12\x08\xe0\x8b\x97S\xe9\xc4U\xf4\x06\xb5\x15\xdd:5\xc8&j=\xb2\xad:'T9\xeb\xce\xbe\xb7\xe7\xban\xbf\x80\x16\xdb\xd3\xed\x8f\x9d\x93?n\xee\xe3\xf3\xfb~\x1e\xdf\x8f\x88\x10\xcf D\"\x11\x15\xa6T\xe5\xa5+\xf2\\\xd7\xabx\x1f\x11\xbfp\x06\xbf\xc8\r\tQ\xfc\xd8\xb7\xab\xdcy\xc6\x93\xa8\xf1\x14!O7\xe4)n_H\x19\xa4\xd0\xfb3\xa8\x9d\x03\xc5^\x84X$Z\x17\x9dd]\xb6mK\xfcr\x7f\xff\x95a\xca\xdc\xe2\xbc\xf4\xb4\xdd\x05\xbe\xab\x03\xdf\\\xeb\x9bR\xec\xfb\xfc\x89o\xb8\"?=-\xc7\xd7\x0f_\x14*\xb2\x94\xb9\xd9\x8a\x9c\x82\x98\xf4\xec\x14U\xbeo\xb42G\xe9\xbby\xab\xef\x16E\x9a*+9\xef\x87w\xa7\x11\xff\xbf3\x08\x82\x90\xe6(s\xf3\xf2\x0b\x92\xe5\xa9\x8a\xdd\xe9Y\xd9o\x04\x04\x85\x12D,\xb1\x99\xf89\xb1\x95\x88#\xb6\x11\x1b\x88p\"\x82\x88$6\x11QD4\x11C\xcc!\xbc\x08\x1fb1Acq\x081\xa1'\x06E\x1bE}3>\x9cq\xc1m\x93[\x9fx\x89\xb8P\x0c\xee\x91\xee\x95\xe4\xab\xe4zRIvJ\xd6\xf3\xe3\xb3\xf9q\xc4\xc1}N:\x08\xee\xf1\x0eht\xcc\xa5Ga=\xbfN\x16D\xaa**KJ\xcc\xdaV\x96\x1e\xe9\x10\x9crR\xa5\xd1\xaaK\x1a\xf0\x7f\x98G\xb6\x9aM6\xab\xc6T\xc8\xcaAG^\xf9\xe3a\xcb\x15t\x02\xb5\xe8\xda\x8a\x0f\x155\x14\xa0\\J\xa8PJ\xa6\xdf\x17\x91\xf6\x86\xe7\xef\xe7\xc0G\xe4\xed\x88\xc1\x00\x86\x1e\x8dAi\xc5\xdb\xb7Rx\x025\x07O9\xd15\x07\xfc\xdb\xe1\x06\x9f\xf1\x112a\xc1k\xcb\x05Z\xf0\xfaf)x\x83\xf7\xdf\x9f\x80\x14\xe6\xbc6!\xd0\xacn\x87\xec\x9b\xbb\xa1\xcb\xfc\xdf\r\xf6\xf3\x0b\x1a\x19\x7f|\xf7\xf6\x13\x16\x03\x08Q\x1c,\xe6`\x90\xdb\xc5Im0\x1f\x13\xf9\x1a\x13y\x04+0\x11\xbf\x97\x88|u\xeeYu\"I?*t\x8d\xe6\xba\x03\xdb\xc8\xb6)**\x96~\x18\x00\x05\xe4\xa7[.\xee\x19F\x14H\xc7\x1f\x81\x07K/\x00O\xff\x87\xc2+\xeb\x93\xf2cv0t\"\x04\x1f\x97=\xb9\x15\x11\xb8:$\xdc\x7fE\xc8\xd0\x83\xbf\xdc\xba\xf97vJC'\x97\xc2I\xe1\x17\xf8\xdc\x1b`\xc4\xe7\n\xb3\xc8\xc2r\xadZ\xddP\xd1\xca\xde\x10\x9c\x81\xf8_E\xe9\x94\x1e\xceI=,\xe5\xf5E\xac\xb0\x01RI:p\x1c\x88\x9e\xb6>\x1f;j\xd6\x1e\xca7V\xed7\x98\x10e1\x9b\xad\xf5:\xd3^\x0b\x9b\xdb\xae2e\xa1x\xf4\xc1\x9e5\xefM\xe9\xb5\xdb\x0e\xdfq\xe9v)x\\\x82\xc3\x97\xe6\xd2\xef\xc3\n\x98)\xb3j\xcc\xa5%ZM!\x13$)4ilV\x93\xd9\xce\xd0=Y\xa7\x06\xd4W|`\xe6\xfdKwN\x14\xfd*\xb3\x95\xcdh\xdbe\x8e>\xb0\xa6^_\xa3j,6k,\xa8\x89\xea\x1d\xe8\xb89|>7\xa5\x8e\xa9-6j-\x88\xb2\x99\xcc\xad\xecu\t\xbd\xb0UkV\x97UT\x94\x1a0\xd2\x91\xf4\x9d\x8d\xdb|\xfcB\x137f4gu\x16\xb3\x1d\xc5\x1dU\x7f\xa8\xba\xa8;\xa2;Rzx\x9fU\x85\n\xa9\xc4\xf7\xd3\xde~g\xe3\xf1\xd3\xcc\x94\xad\x7f\xe2D\xe0\x8bM\x8d\xc3\x82\x80X\xd2\xaa\xad/\xc1\x03\x161\x828\x12\xe7c\xd2\x966\xac\x8e\x99\x0c\xf9m\xc2\xd7g/\x99\x9b\xfb\x99\x93M\xd6Fd\xa1\x9a4\xe62}\xf5\xc7:-\x93\xaa\x8aT\xc7!jSJ\xe7Y\x16L\x90!q9f\xd3\x18U\xec\x94\x14\x1c\xbc\xc5\x81\x07'\xc5\xf9\xe9w\xc4\xc3\xfc\xb9t\x1e\xbf\xda{b:\xa3ti\"\x98\xc8\xe1\xf0\x01\x7fE\xd4\xbe\xbdqL\x99\xbe\xaa\x12\x95SefMc\xdd\xfe\x9a_62\x9f5\x9f6v#\xca\xd9\x9f\xbd\x93\x8d\x96\xc4Z\xf2\xf6\xefD\x94\xe0\xbd6v5Kk\x83\xbf\xd8>v\xe3b\xdb\xc0U,\xc0eqTl|A$\xa26&w\xf5\x7f\xee\xfc\xe4\xe9\x99~}e\x0f\xfb\"\xc2\xd8\x90;.\xff\xf9]\xbcL&\xef\xdan\xdb\x8ca\x16-_)\xcc\x17dc\x01\xe0s\xed\xf7-'\x06\xd8N\xbb\xa5\x19K\xde\xa81\xef\xab\xd4\x1b\xb4Z&\xe1\xc3\x98\x820D-\x0euN\xfccx\xe8\x9f\xf7\xae)\x12\x0e\xb0\xb5E\xc6\xca)\x1f\xec\xec\x03\t\x1d\x88}()\xa9\xc4\xde\xbe }\x7f\x92\xf4\xe7\x0ehvQ>\xc7\xd7\xf1Oq\xd6\xbfO\xf69a\x17\xb9s0\xb6+\x1c\x8f0g\xd9R\xc1K\xf0z\xe2\x07\xb3\x87\xaev_>\x83\x15\t\x9d\x90|\xafO\")\x14\xc1}\x9c\xeb\xd0e,\xdd\xe3\x1f\x1c\x8c\xa3=2>vk\xe4\xf1s\x17\xd7r\xb0\x90\x13\xf1\xed\x10/3J\x0eJ\xe0\x95\xa5\x8f\x85\x05\xc2\xbc\xd7W\t\xb3\x84y z\x1d\xd8q\xf0\xe8?\xe5\xb2LWm\xd0U2\xf2\xec0U,Z\x82\xde\xfb]\xd9\x18\xc5\x89m\xf7n^\xf8+z\x88\x86\xe3\xacA\xd4\x8b\xc6\xc1\xd3\x8b\xc0\xc3\x01M8\x1e!?\x9a\xfd\x99\xe1Gu\xd3\xf0|G\xe5PM\x1e\xed\xb4\xb5\x1c\xa8\xeb8t\xb4\xfe\x14\xeaEvW\xe9\xec\xc5\xa5\xa3\xc4\xa5#\x97Lo\xf6\x0f\xbe\xaa\"\xefE\x0e\xae\x8cM)\xda\x9e\xc4\xbcX\xd7\x07\xe0.\x85\x83\xce\x84\xc9\xa6\xb8\xe3\xda\xd8w\xa6\xab\x02\xdc\x05\xa7\x100=\x12|7\r\x87\xef\xd3\x13\x06\xfe\xba,Bpw\x92\x93p\xbc\x01\x939\x8a\x99\xdc\xc1L\x84uS\xc3\xbb\xb2\rn\xcf\x0c\xff\x03\xc7\xf5\xb1k\x95\xa5\x07@\xbc\x83\x835\xae\x9f\xab\x81g\xe2q\xde}\xa9\xb8n\xe0\x06\xce!\xe9Q\x17\x0en\x94\x16W\xa7b\x1c\xabm\xb2\xb8\xbeT\x82\x91<1\xd0\xd9~\x1cQ]\xc72w\xb3\xc2\xf5\xbb\xd3\xf6\xe6L>\xech\xefAT\xcf\xb1\xectV\x18\xba+y\xa9\x8f\x0f\x91W\x12\xce\xc7\xa4d\x97$\xc9\x99\xfc3\x99\xad\xc9\x88\xa2G\xe5(G\x9d\xa5pyUj\x17A?x\xc9\x923\xb3SS\xbb\xb3N\xb3f\xf2tw\xe7'\xbd\x99\x9d\xc9\xae\xdc\xf3\xeao\xc5\xb2\xba\xfa\x9aZTG5\x96\x9b\xcb\xca\xab\xf4\xa5U\x8c\xf0\xe5\xbfB\xaa+?\xaeF\xfa\xf9\xfb\x1a4M\r\x07\xeb,\x07\x99I0~\xd1O\xe1u\xf5N\xe2i\xe0\xec\x7f;'\xe6<\x04p\xbc''z\xea\x18u\x80\x97\xc3\x8d\x7f\x13^\x95\xf5\xe2%767T\x99\xca\xf7\xb3`\x97<\nw\xbe!Po\x0bn\xc2JFX#Aa-\xd1'w\x9c\x8c\xffM\xfeUD\xdd\x1e\xe99\x8eW\xaeT\xa77T\xeb\xd9=\xf9\x19\x9aD\x94\x842l{Nf\xf7\xa9/\xa2\xcb\x14\x04J@z\xf5\xab?\x7fq\xf6\x83(F.Y\xf2QX,ZGm\x18\x8c\xbbg6\xd5\xd461\xe7\xc5j\x83\x1eU *N\xd1\xfd\xe9\x85\x81_\x0f\xd5\xb0\xb3\xd5V\xfe-+x7\x1ck$\x1d39\x8f>\x93\xa7g\x9f\xd1s\x16A\xfc\x07\xbe\x9e\x12\xf0\nendstream\nendobj\n8 0 obj\n<>\nendobj\n13 0 obj\n<>stream\nx\x9c\x9d\x93{PSg\x1a\xc6O\x80\x9c\x9c\xad\xb4\"\xd9S\xd4\xb6Iv\xba\xabh\x91\x11\xa4\xad\xbbu\xb7\xd3B\xcb\xb6\x16G\xc1\x16P\xa0\x18\x03$\x84\\ AHBX\x92p1\xbc\x04\xb9$\xe1\x12 @@B@.\xca\x1dA\xb7\x8a\x80\x8e\x8b\xbb\x9d\xae\xb3\xf62\xbb\xba[;[hw\xc3\xd4\xef\x8cGg\xf6$\xe8t\xf7\xdf\xfd\xeb\x9cy\xbfs\xde\xf7\xf9~\xcf\xf3\xb2\xb0\xa0\x00\x8c\xc5b=\x1b\xab(,\x90d\x15\xecy[\x91'\xf2\x15\"\xa8\x17X\xd4\x8b\x01\xd4K\x81\xfa\x12\xea1\xf5\x98M\xf1\x82\xb1\x9a`\x16\x04\x07BpP\xc7\x8b\x9c\x0b\xa1\xc8\xb3\x05\xc1f\xa4\r\xc1\x82X\xac\xd7\xdfOi\x0e\xff01y\xd7+\xafD\xc4*\x94\x9a\x02I\x8eX-\x88\xde\x1b\x15#\x10j\x04ON\x04qY*I\x8e\\\xb0\x83y9\x95\x95\xa7P\xca\xb2\xe4\xeaC\x12\x99\xb0P%HP\xc8\x15\x82\xc3I\x02\x9f\x80\xff-\xfd\xd8\xee\xff\x1b\x80a\xd8\xe6\xb8\x93\xa2\xac\xe4\xbdQ\xd1\xfbb^\x15\xec\xff\xe5\xaf0\xec\x17X\x1c\xf6\x0e\xf6.\xb6\x1f\xdb\x82\x85b\\\xec\xa7\x18\x89=\x8f\xb1\xb0m\xd8v\xec\x05,\x84\x81\x82\x05aE\x18\xc5r\x07\x04\x04X\x03\x1e\x04&\x05^\tJ\x0bZ`\xc7\xb3\xdfg/\xe1\xb1\xb8\x86Z}\x8eZ\x05/z\xe8eQ\x89\x08\x0b\xfc\xa3\x97\xcc\xaaV\x17C\x1eh\xad\xbaf\xa3\xad\xbc\xf5\xb4\x0b\x08\x94\x89\xa3\xe8*\x14\xf8\xef\x1a\x14ALr\x00\xed\xa19h\x13\xbd\xd3L\xd0b\\\t\xa6jC\x85\xce`\xd0\x82\xd6\xf7W\x8b\xd1Z\xde`\xee\xaa&\x10F?$\xd1\xc3\x1f8\xf7\xcf\xac\xbck\t'28\x10\x91p$\xfc\x0c\xc1\x8c,\xf1\xa2j/k\x8e\x99H\x8dQ89\xad\xeb\xcc),3\x15\x97\xf3\xb2\xda\x8fY\x8f\x02A\xef\x11\xec\xa6\xf9\x87;S\xc6D\xfc\xb9\xb4\xebEk\xf0\x19\xdc\xb0\x8f9';\xbb{\xe1,\xd1\xa7r\xc9J\rU&\x03\xefd\xae\xd4\xf8\x06\xf3='q\xf4\xcf_,^\xfafb\xc8\xa4\xeb\xe17\x95\xd7\x9bjuu\x85\xb5\x15\x8d\xe5V\x93\xa3\xa2\x05\xda\xc0\xd1hon\xb4Yl\xd0\xeb\x13P\xea\x8dr\xa2\x15o\xa8\x1bah\x02aa\xdc)j\x80\xfa\x9e\xa4\x83\xf1\xfc\xa7\xf7\xd1\x81\x06\xb4\x8d%-\x06{\xb9\xed\xf4Y \x9a~\x86\x8b\xdc\xa9\xad\x89\xf0\x1bH,J\xcbL\xcbT%\xc1\x07p\xd0\x954\x939\x93y\xb5\xe86,\xc0\x85\xa6\x8b\x1e\x82[,C\xc1\x1c\x17\xd8-\xd6:\x87\xcd\xd6\x06\xed\xe009\xf4\xb6\xb2\x06\xa3E\x01\xc4\xefp\xba\x1e\x95\x90\xb3\xe0)\xeb\xcbw\x15\xb6HAFp\xa7\xde:\x9c\x1a\x93\x9e\xdb\xd4\xa3\xe4\xa9\xba\xf5\x1e\x18\x00O\x8b\xc7\xd5}\xb6w\xc0>\x0b\x1b\xc0n\xdf\xff\x0bc\xd2<\xdaO\x8eq\xd0v:p\x8d\x8e\xa0w\xd1\xecp\x9a\xa4\xc3P@$\x8a\xfe\xd4\xdb\xe6\x9c\xe2\xf5\xd8\x9aZ\xa1\x93p\x17v\xcb\xcb\xca\xcc\xa7KyQ\xea\xfc\xaat\xd8\x0f\xa9\xae\x82K\x84\xe5>\xe9\x98^\x18X\x81\x15\xb8*mK\xf7u\x06'\x95\xe0e\xa1\xcb\xc8F~M\xdb\xd8\x88\xc0\x17)a\x7f][\x07\x9c\xdd\xc6\x08o\xd5\xdb\x9f\x08\xa7\xc3\x9e\xb21\x1a4>\xaf\x1b\x19\xaf\xed&\xbb\xb9\x17\x88\x8bx.m\x8cE\x1f\xb3i\x0c\x8f\xa5?\xceEF\xf6\x04\xeeC`\xfb\x11A+\x83\xa0\xd1\xf0\xa4\x93\x12\xca\x99NZ\x83Q\x07E\xa0ph\xfb\xab\x96\x1f\t\xb7\xa2gpF\x91\xdeK\xfd\xda\xcb\xba\xc38s\xca\x17\x90v\xf4\x1d\t\xf7\xe4wR\xe7s\x86\x8e\xb7\x1f\x81#p\\\x93#NM\x91\x1f\x80}D\x14\x07b\xdco\xcc\xa5\x0e\x8bg5\x0b\x8c\x03\xb3\xed\xc3Css\xee\xcf\xe1.A\xdf]%\xd7&\xaf\xdf\xba5\xf9\xc1.\xde\xcf9\xbb3\x0e\xc6\xc7g\xdcX\xe5m$\xfe\xae\x93\x85\xaa\x99\xf6\xe8\x01\xf5\x98\xa4e\x1f\x9d0\xe8\xf5 \xdf&\xebR\xf5\xd9jk\xea\x9c\xbc/;\xd9\x8f\xb6\xec\xe6\xe4\xffw\xbcuV\xed\xc6Rt3K\xf1\t>\xedj?\xe7\xbf\x17\xdfw1%\x10\xbb}\xf2a\x9d\x8ad\x9cz\xd9\xd7\\\xbeN\xa2f\x94\xe5\x1e\x84\xaf\x88\x07\x91_\xd0!\x87\x92\x8a\xc4B\x9eX\xa6L\x03)\xa1\xecQ\xbb\xbb\x9dM\xed\xf5<\xbb\xa7\xc6b\xb5u\xb9\x06[\xce\x03q}V\x9c\x96\xa7+\xde\x19\xc3\x17\xe6\xbc\x93H\x13Q\x15\x95[\x05\x94\xf0\x1e\x07\\fk\x85\xcd\xd0\xaa\xb5\x16\x83\x14\xb4\xba*1\xe1\xc7\x85\xbes^\xf3\x86R;\x11\xf6\xaa/\xca\xdf 7\xf5\x13R\xaa*\x94\xcb\x9d\xda!3\x7f\xcal7;M\xd3\x9a>)H\xe0T\x99ZW\x9a\xaf\xce1\xc6\xc3A\x90\xd7\xa9\x1cZ[\xa5\xa5\x14\x88<\xb5Z\x9e\xf2U.\n\xbdw\xb9yp\x8a?s\xce\xfd\t\\\x85\xc5\xec\xb9\xb8s\x04\xf7_\x8bC\xbd\xa3\xf3\xdba\xbcx\\\xea\x11\x8d$w\xc43&\x06\x86'\x1f\x91\xbb\xd4\xee\xd6\x96z\x9b\x95?0\xd8k\xfb=\x10\x7f\x18\xcf?!:)I\xe3\xfb)\xbb}\xd2X\xe8[\x9f\x8d\xc9\xd4\x1aI\xbf\x84\xd3U\x8fH\xf6\xeb\xa8G.\xe1\x14\x80\xd1l\xa8\xdc@KH\\\x9ai\x1e\xda\x8a\xcf\xf8\x99:\xf4V\xbe\xa1\xa1\xdcRXC\xb89\xe7k\xba:\x98\x8d\xf0/\x91\xa1\xde_\xa4\xb1\xe7i\x1e\x8ex(\x97\xbdA \xdf\xfbW&\xc4\x1c&3\x19>\xee*\xaa\x92D\xc7\xf0.h\xb14>M`\x9b?\x81\r~\xa3\xe8kt\x1f\x9e\xdb\xad\xf2\xd8\xcf\xd44\xb4\xf0\xc6\x9c\xd3\xcd\x1e nNd\xc4\xbf\x95.\xd9\xf1\x9e\xa2\xa1[\xc6/i6\xd5\x96\x00!/P+\x92\xee\x9f@!\xdf.t\xccL\xf1\x87G\x9d\xf3p\x85@[\xf6~M\x87\xc8\xf3*\rb_\xa06D\xbc\xb6\x8e\xf6yC\x99\xe0\x863:D\xfeG\x18w\x95z\x13-\x91W\x86\xddSp\x91\xf8>\xf2\x0e\xbd\x89\xde\x14y`g\xaa;\xf3J6\x8f\xebM\xc8\x96\xa6\x1c\xde\xfe\xf2\xdf\xe3P\x18\xda\xfa\x8f?\xad_\x93\xce'\x8c\xf0\xb8\xab4\x17\t\xc9\xa5\ti\xfa\xb1\x13\xd2\x84C\x99\x8333\xe3\x03\xcb|\xae\x97v\x04-\xcf\xe7d\x1cO\xcf\xfd\xed{i\x833\xd3\xf3\xc3\xcb>\xd6\xfa\x1fP\xe8::\xeae=\xf0\xb1\x8eC\xfd\xa4\x92f\xed{s\x07\x18\xe1t\x8d\xa1V[o\xb0\x18\x80\x90\x15\xa8e\xa2\xd9\xfcO\xff\xf9\xe5\x85\xcfW\xf8\x97\x96z?\x83\xbf\xc1-\xcdm\xe5\xb4\xe8\xe6\xa1\xc1\xd7 \x1eR\x8b\xb3E\x92\x9c\xe2T8\xca\x18|7\x1aa\xb3\xa3m\xe3\x93<\x13\xdaL\xe6g\x1c\xcb\x15\x02\x91,\x1c\xbf\xbc4<\xbcx\xe3\x9c\xf8@\xab\x7f4\xe3\xf0\xb2\x9e<\xefq\x8f\x8e\xe4\xf5\x8b\xf8\x1a>stream\n\n\n\n\n\n2024-12-18T15:59:31-05:00\n2024-12-18T15:59:31-05:00\nGNU Enscript 1.6.6\n\nEnscript Output\n\n\n \n \n\nendstream\nendobj\n2 0 obj\n<>endobj\nxref\n0 15\n0000000000 65535 f \n0000000711 00000 n \n0000007145 00000 n \n0000000652 00000 n \n0000000510 00000 n \n0000000266 00000 n \n0000000491 00000 n \n0000001145 00000 n \n0000003652 00000 n \n0000000815 00000 n \n0000001471 00000 n \n0000000776 00000 n \n0000001773 00000 n \n0000003974 00000 n \n0000005817 00000 n \ntrailer\n<< /Size 15 /Root 1 0 R /Info 2 0 R\n/ID [<9BB34E42BF7AF21FE61720F4EBDFCCF8><9BB34E42BF7AF21FE61720F4EBDFCCF8>]\n>>\nstartxref\n7334\n%%EOF\n" + } + module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) + + def check(self, module_test, events): + finding_events = [e for e in events if e.type == "FINDING"] + assert len(finding_events) == 1 + assert "Possible Secret Found" in finding_events[0].data["description"] diff --git a/bbot/test/test_step_2/module_tests/test_module_viewdns.py b/bbot/test/test_step_2/module_tests/test_module_viewdns.py index d196981ba1..e8b2fe2339 100644 --- a/bbot/test/test_step_2/module_tests/test_module_viewdns.py +++ b/bbot/test/test_step_2/module_tests/test_module_viewdns.py @@ -66,7 +66,7 @@ def check(self, module_test, events): -
ViewDNS.info > Tools > + ViewDNS.info > Tools >

Reverse Whois Lookup



This free tool will allow you to find domain names owned by an individual person or company. Simply enter the email address or name of the person or company to find other domains registered using those same details. FAQ.

diff --git a/bbot/test/test_step_2/module_tests/test_module_wayback.py b/bbot/test/test_step_2/module_tests/test_module_wayback.py index cf09d8e2c5..7582e54173 100644 --- a/bbot/test/test_step_2/module_tests/test_module_wayback.py +++ b/bbot/test/test_step_2/module_tests/test_module_wayback.py @@ -4,7 +4,7 @@ class TestWayback(ModuleTestBase): async def setup_after_prep(self, module_test): module_test.httpx_mock.add_response( - url=f"http://web.archive.org/cdx/search/cdx?url=blacklanternsecurity.com&matchType=domain&output=json&fl=original&collapse=original", + url="http://web.archive.org/cdx/search/cdx?url=blacklanternsecurity.com&matchType=domain&output=json&fl=original&collapse=original", json=[["original"], ["http://asdf.blacklanternsecurity.com"]], ) diff --git a/bbot/test/test_step_2/module_tests/test_module_web_report.py b/bbot/test/test_step_2/module_tests/test_module_web_report.py index c34eef00f2..cfaa90f217 100644 --- a/bbot/test/test_step_2/module_tests/test_module_web_report.py +++ b/bbot/test/test_step_2/module_tests/test_module_web_report.py @@ -3,10 +3,11 @@ class TestWebReport(ModuleTestBase): targets = ["http://127.0.0.1:8888"] - modules_overrides = ["httpx", "wappalyzer", "badsecrets", "web_report", "secretsdb"] + modules_overrides = ["httpx", "wappalyzer", "badsecrets", "web_report", "trufflehog"] + config_overrides = {"modules": {"trufflehog": {"only_verified": False}}} async def setup_before_prep(self, module_test): - # secretsdb --> FINDING + # trufflehog --> FINDING # wappalyzer --> TECHNOLOGY # badsecrets --> VULNERABILITY respond_args = {"response_data": web_body} @@ -23,7 +24,7 @@ def check(self, module_test, events):
  • http://127.0.0.1:8888/""" in report_content ) - assert """Possible secret (Asymmetric Private Key)""" in report_content + assert """Possible Secret Found. Detector Type: [PrivateKey]""" in report_content assert "

    TECHNOLOGY

    " in report_content assert "

    flask

    " in report_content @@ -45,7 +46,22 @@ def check(self, module_test, events): -

    -----BEGIN PGP PRIVATE KEY BLOCK-----

    +

    -----BEGIN PRIVATE KEY----- +MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOBY2pd9PSQvuxqu +WXFNVgILTWuUc721Wc2sFNvp4beowhUe1lfxaq5ZfCJcz7z4QsqFhOeks69O9UIb +oiOTDocPDog9PHO8yZXopHm0StFZvSjjKSNuFvy/WopPTGpxUZ5boCaF1CXumY7W +FL+jIap5faimLL9prIwaQKBwv80lAgMBAAECgYEAxvpHtgCgD849tqZYMgOTevCn +U/kwxltoMOClB39icNA+gxj8prc6FTTMwnVq0oGmS5UskX8k1yHCqUV1AvRU9o+q +I8L8a3F3TQKQieI/YjiUNK8A87bKkaiN65ooOnhT+I3ZjZMPR5YEyycimMp22jsv +LyX/35J/wf1rNiBs/YECQQDvtxgmMhE+PeajXqw1w2C3Jds27hI3RPDnamEyWr/L +KkSplbKTF6FuFDYOFdJNPrfxm1tx2MZ2cBfs+h/GnCJVAkEA75Z9w7q8obbqGBHW +9bpuFvLjW7bbqO7HBuXYX9zQcZL6GSArFP0ba5lhgH1qsVQfxVWVyiV9/chme7xc +ljfvkQJBAJ7MpSPQcRnRefNp6R0ok+5gFqt55PlWI1y6XS81bO7Szm+laooE0n0Q +yIpmLE3dqY9VgquVlkupkD/9poU0s40CQD118ZVAVht1/N9n1Cj9RjiE3mYspnTT +rCLM25Db6Gz6M0Y2xlaAB4S2uBhqE/Chj/TjW6WbsJJl0kRzsZynhMECQFYKiM1C +T4LB26ynW00VE8z4tEWSoYt4/Vn/5wFhalVjzoSJ8Hm2qZiObRYLQ1m0X4KnkShk +Gnl54dJHT+EhlfY= +-----END PRIVATE KEY-----

    """ diff --git a/bbot/wordlists/devops_mutations.txt b/bbot/wordlists/devops_mutations.txt index bfde86c591..b3fc8deda1 100644 --- a/bbot/wordlists/devops_mutations.txt +++ b/bbot/wordlists/devops_mutations.txt @@ -105,4 +105,4 @@ store home production auto -cn \ No newline at end of file +cn diff --git a/bbot/wordlists/ffuf_shortname_candidates.txt b/bbot/wordlists/ffuf_shortname_candidates.txt index 4439d6d744..2d57ee9463 100644 --- a/bbot/wordlists/ffuf_shortname_candidates.txt +++ b/bbot/wordlists/ffuf_shortname_candidates.txt @@ -107979,4 +107979,4 @@ zzz zzzindex zzztest zzzz -zzzzz \ No newline at end of file +zzzzz diff --git a/bbot/wordlists/nameservers.txt b/bbot/wordlists/nameservers.txt index d350e56f9c..9153631946 100644 --- a/bbot/wordlists/nameservers.txt +++ b/bbot/wordlists/nameservers.txt @@ -2370,4 +2370,4 @@ 8.25.185.131 203.39.3.133 118.69.187.252 -108.56.80.135 \ No newline at end of file +108.56.80.135 diff --git a/bbot/wordlists/paramminer_headers.txt b/bbot/wordlists/paramminer_headers.txt index 53ea11e8b4..3fe2366059 100644 --- a/bbot/wordlists/paramminer_headers.txt +++ b/bbot/wordlists/paramminer_headers.txt @@ -1147,4 +1147,4 @@ http_sm_userdn http_sm_usermsg x-remote-ip traceparent -tracestate \ No newline at end of file +tracestate diff --git a/bbot/wordlists/paramminer_parameters.txt b/bbot/wordlists/paramminer_parameters.txt index 2022323fb3..501878d987 100644 --- a/bbot/wordlists/paramminer_parameters.txt +++ b/bbot/wordlists/paramminer_parameters.txt @@ -6520,4 +6520,4 @@ shell_path user_token adminCookie fullapp -LandingUrl \ No newline at end of file +LandingUrl diff --git a/bbot/wordlists/raft-small-extensions-lowercase_CLEANED.txt b/bbot/wordlists/raft-small-extensions-lowercase_CLEANED.txt index 6e2aca6506..b5f461182f 100644 --- a/bbot/wordlists/raft-small-extensions-lowercase_CLEANED.txt +++ b/bbot/wordlists/raft-small-extensions-lowercase_CLEANED.txt @@ -830,4 +830,4 @@ .z .zdat .zif -.zip \ No newline at end of file +.zip diff --git a/bbot/wordlists/valid_url_schemes.txt b/bbot/wordlists/valid_url_schemes.txt index f0a440da9b..721a854aee 100644 --- a/bbot/wordlists/valid_url_schemes.txt +++ b/bbot/wordlists/valid_url_schemes.txt @@ -379,4 +379,4 @@ xri ymsgr z39.50 z39.50r -z39.50s \ No newline at end of file +z39.50s diff --git a/docs/data/chord_graph/entities.json b/docs/data/chord_graph/entities.json index 7fb11654fa..00633c95fa 100644 --- a/docs/data/chord_graph/entities.json +++ b/docs/data/chord_graph/entities.json @@ -23,11 +23,11 @@ ] }, { - "id": 128, + "id": 131, "name": "AZURE_TENANT", "parent": 88888888, "consumes": [ - 127 + 130 ], "produces": [] }, @@ -36,20 +36,20 @@ "name": "CODE_REPOSITORY", "parent": 88888888, "consumes": [ - 61, - 81, - 84, + 63, + 83, 86, - 116, - 135 + 88, + 119, + 138 ], "produces": [ 42, - 62, - 82, - 83, + 64, + 84, 85, - 115 + 87, + 118 ] }, { @@ -87,35 +87,37 @@ 58, 59, 60, - 66, - 78, - 82, - 89, - 93, + 62, + 68, + 80, + 84, + 91, 95, - 101, - 102, + 97, + 103, + 104, 106, - 107, - 111, - 112, - 113, - 117, + 109, + 110, + 114, + 115, + 116, 120, - 121, - 122, 123, 124, + 125, + 126, 127, 130, - 131, - 132, + 133, 134, + 135, 137, 140, - 141, + 143, 144, - 147 + 147, + 150 ], "produces": [ 6, @@ -136,31 +138,32 @@ 58, 59, 60, - 78, - 89, - 93, + 61, + 80, + 91, 95, - 101, - 102, + 97, + 103, 104, - 106, 107, - 111, - 117, + 109, + 110, + 114, 120, - 122, 123, - 127, - 129, + 125, + 126, 130, - 131, + 132, + 133, 134, 137, - 138, 140, 141, + 143, 144, - 147 + 147, + 150 ] }, { @@ -169,8 +172,8 @@ "parent": 88888888, "consumes": [ 21, - 127, - 132 + 130, + 135 ], "produces": [] }, @@ -179,18 +182,19 @@ "name": "EMAIL_ADDRESS", "parent": 88888888, "consumes": [ - 67 + 69 ], "produces": [ 45, 52, 58, - 66, - 93, - 112, - 121, + 62, + 68, + 95, + 115, 124, - 129 + 127, + 132 ] }, { @@ -198,18 +202,18 @@ "name": "FILESYSTEM", "parent": 88888888, "consumes": [ - 71, - 100, - 135 + 73, + 102, + 138 ], "produces": [ 8, - 61, - 75, - 81, - 84, - 100, - 116 + 63, + 77, + 83, + 86, + 102, + 119 ] }, { @@ -218,7 +222,7 @@ "parent": 88888888, "consumes": [ 14, - 145 + 148 ], "produces": [ 1, @@ -233,33 +237,33 @@ 34, 37, 51, - 80, - 85, - 90, + 82, + 87, 92, - 95, - 103, - 104, + 94, + 97, 105, + 107, 108, - 109, - 119, - 125, - 127, - 133, - 135, + 111, + 112, + 122, + 128, + 130, 136, - 146 + 138, + 139, + 149 ] }, { - "id": 97, + "id": 99, "name": "GEOLOCATION", "parent": 88888888, "consumes": [], "produces": [ - 96, - 99 + 98, + 101 ] }, { @@ -281,24 +285,25 @@ 14, 26, 51, - 65, - 68, - 75, - 85, - 90, - 103, - 104, - 108, - 109, - 110, - 119, - 127, - 133, - 143, - 146 + 67, + 70, + 77, + 87, + 92, + 105, + 106, + 107, + 111, + 112, + 113, + 122, + 130, + 136, + 146, + 149 ], "produces": [ - 91 + 93 ] }, { @@ -308,26 +313,28 @@ "consumes": [ 11, 14, - 95, - 96, + 97, 98, - 99, - 113, - 127 + 100, + 101, + 106, + 116, + 130 ], "produces": [ 14, - 98, - 127 + 61, + 100, + 130 ] }, { - "id": 114, + "id": 117, "name": "IP_RANGE", "parent": 88888888, "consumes": [ - 113, - 127 + 116, + 130 ], "produces": [] }, @@ -339,7 +346,7 @@ 8 ], "produces": [ - 86 + 88 ] }, { @@ -348,29 +355,30 @@ "parent": 88888888, "consumes": [ 14, - 76, - 91, - 129 + 78, + 93, + 106, + 132 ], "produces": [ 14, - 95, - 113, - 127 + 97, + 116, + 130 ] }, { - "id": 63, + "id": 65, "name": "ORG_STUB", "parent": 88888888, "consumes": [ - 62, - 83, - 86, - 115 + 64, + 85, + 88, + 118 ], "produces": [ - 127 + 130 ] }, { @@ -384,12 +392,14 @@ ] }, { - "id": 77, + "id": 79, "name": "PROTOCOL", "parent": 88888888, - "consumes": [], + "consumes": [ + 106 + ], "produces": [ - 76 + 78 ] }, { @@ -398,36 +408,38 @@ "parent": 88888888, "consumes": [], "produces": [ - 54 + 54, + 61, + 62 ] }, { - "id": 69, + "id": 71, "name": "RAW_TEXT", "parent": 88888888, "consumes": [ - 68 + 70 ], "produces": [ - 71 + 73 ] }, { - "id": 64, + "id": 66, "name": "SOCIAL", "parent": 88888888, "consumes": [ - 62, - 83, + 64, 85, 87, - 115, - 127 + 89, + 118, + 130 ], "produces": [ - 62, - 85, - 126 + 64, + 87, + 129 ] }, { @@ -442,7 +454,7 @@ 32, 33, 34, - 127 + 130 ], "produces": [ 29, @@ -458,19 +470,19 @@ "parent": 88888888, "consumes": [ 14, - 85, - 145, - 146 + 87, + 148, + 149 ], "produces": [ 26, - 65, - 85, + 67, 87, - 95, - 105, - 143, - 146 + 89, + 97, + 108, + 146, + 149 ] }, { @@ -482,37 +494,37 @@ 14, 23, 37, - 72, - 79, - 80, - 87, - 91, - 94, - 104, - 105, - 118, - 125, - 127, - 133, + 74, + 81, + 82, + 89, + 93, + 96, + 107, + 108, + 121, + 128, + 130, 136, - 138, - 142, - 145 + 139, + 141, + 145, + 148 ], "produces": [ - 87, - 91 + 89, + 93 ] }, { - "id": 74, + "id": 76, "name": "URL_HINT", "parent": 88888888, "consumes": [ - 73 + 75 ], "produces": [ - 94 + 96 ] }, { @@ -521,11 +533,11 @@ "parent": 88888888, "consumes": [ 42, - 75, - 91, - 106, - 126, - 127 + 77, + 93, + 109, + 129, + 130 ], "produces": [ 18, @@ -534,17 +546,18 @@ 54, 58, 62, - 68, - 72, - 73, - 82, - 87, - 93, - 118, + 64, + 70, + 74, + 75, + 84, + 89, + 95, 121, - 137, - 144, - 146 + 124, + 140, + 147, + 149 ] }, { @@ -552,7 +565,7 @@ "name": "USERNAME", "parent": 88888888, "consumes": [ - 127 + 130 ], "produces": [ 45, @@ -560,14 +573,14 @@ ] }, { - "id": 139, + "id": 142, "name": "VHOST", "parent": 88888888, "consumes": [ - 145 + 148 ], "produces": [ - 138 + 141 ] }, { @@ -576,7 +589,7 @@ "parent": 88888888, "consumes": [ 14, - 145 + 148 ], "produces": [ 1, @@ -585,13 +598,13 @@ 25, 26, 51, - 65, - 79, - 95, - 105, - 133, - 135, - 146 + 67, + 81, + 97, + 108, + 136, + 138, + 149 ] }, { @@ -602,33 +615,33 @@ 14 ], "produces": [ - 142 + 145 ] }, { - "id": 88, + "id": 90, "name": "WEBSCREENSHOT", "parent": 88888888, "consumes": [], "produces": [ - 87 + 89 ] }, { - "id": 70, + "id": 72, "name": "WEB_PARAMETER", "parent": 88888888, "consumes": [ - 92, - 108, - 109, - 110 + 94, + 111, + 112, + 113 ], "produces": [ - 68, - 108, - 109, - 110 + 70, + 111, + 112, + 113 ] }, { @@ -1101,6 +1114,30 @@ }, { "id": 61, + "name": "dnsresolve", + "parent": 99999999, + "consumes": [], + "produces": [ + 7, + 12, + 55 + ] + }, + { + "id": 62, + "name": "dnstlsrpt", + "parent": 99999999, + "consumes": [ + 7 + ], + "produces": [ + 46, + 55, + 19 + ] + }, + { + "id": 63, "name": "docker_pull", "parent": 99999999, "consumes": [ @@ -1111,21 +1148,21 @@ ] }, { - "id": 62, + "id": 64, "name": "dockerhub", "parent": 99999999, "consumes": [ - 63, - 64 + 65, + 66 ], "produces": [ 43, - 64, + 66, 19 ] }, { - "id": 65, + "id": 67, "name": "dotnetnuke", "parent": 99999999, "consumes": [ @@ -1137,7 +1174,7 @@ ] }, { - "id": 66, + "id": 68, "name": "emailformat", "parent": 99999999, "consumes": [ @@ -1148,7 +1185,7 @@ ] }, { - "id": 67, + "id": 69, "name": "emails", "parent": 99999999, "consumes": [ @@ -1157,31 +1194,31 @@ "produces": [] }, { - "id": 68, + "id": 70, "name": "excavate", "parent": 99999999, "consumes": [ 2, - 69 + 71 ], "produces": [ 19, - 70 + 72 ] }, { - "id": 71, + "id": 73, "name": "extractous", "parent": 99999999, "consumes": [ 10 ], "produces": [ - 69 + 71 ] }, { - "id": 72, + "id": 74, "name": "ffuf", "parent": 99999999, "consumes": [ @@ -1192,18 +1229,18 @@ ] }, { - "id": 73, + "id": 75, "name": "ffuf_shortnames", "parent": 99999999, "consumes": [ - 74 + 76 ], "produces": [ 19 ] }, { - "id": 75, + "id": 77, "name": "filedownload", "parent": 99999999, "consumes": [ @@ -1215,18 +1252,18 @@ ] }, { - "id": 76, + "id": 78, "name": "fingerprintx", "parent": 99999999, "consumes": [ 15 ], "produces": [ - 77 + 79 ] }, { - "id": 78, + "id": 80, "name": "fullhunt", "parent": 99999999, "consumes": [ @@ -1237,7 +1274,7 @@ ] }, { - "id": 79, + "id": 81, "name": "generic_ssrf", "parent": 99999999, "consumes": [ @@ -1248,7 +1285,7 @@ ] }, { - "id": 80, + "id": 82, "name": "git", "parent": 99999999, "consumes": [ @@ -1259,7 +1296,7 @@ ] }, { - "id": 81, + "id": 83, "name": "git_clone", "parent": 99999999, "consumes": [ @@ -1270,7 +1307,7 @@ ] }, { - "id": 82, + "id": 84, "name": "github_codesearch", "parent": 99999999, "consumes": [ @@ -1282,19 +1319,19 @@ ] }, { - "id": 83, + "id": 85, "name": "github_org", "parent": 99999999, "consumes": [ - 63, - 64 + 65, + 66 ], "produces": [ 43 ] }, { - "id": 84, + "id": 86, "name": "github_workflows", "parent": 99999999, "consumes": [ @@ -1305,50 +1342,50 @@ ] }, { - "id": 85, + "id": 87, "name": "gitlab", "parent": 99999999, "consumes": [ 2, - 64, + 66, 16 ], "produces": [ 43, 4, - 64, + 66, 16 ] }, { - "id": 86, + "id": 88, "name": "google_playstore", "parent": 99999999, "consumes": [ 43, - 63 + 65 ], "produces": [ 9 ] }, { - "id": 87, + "id": 89, "name": "gowitness", "parent": 99999999, "consumes": [ - 64, + 66, 3 ], "produces": [ 16, 3, 19, - 88 + 90 ] }, { - "id": 89, + "id": 91, "name": "hackertarget", "parent": 99999999, "consumes": [ @@ -1359,7 +1396,7 @@ ] }, { - "id": 90, + "id": 92, "name": "host_header", "parent": 99999999, "consumes": [ @@ -1370,7 +1407,7 @@ ] }, { - "id": 91, + "id": 93, "name": "httpx", "parent": 99999999, "consumes": [ @@ -1384,18 +1421,18 @@ ] }, { - "id": 92, + "id": 94, "name": "hunt", "parent": 99999999, "consumes": [ - 70 + 72 ], "produces": [ 4 ] }, { - "id": 93, + "id": 95, "name": "hunterio", "parent": 99999999, "consumes": [ @@ -1408,18 +1445,18 @@ ] }, { - "id": 94, + "id": 96, "name": "iis_shortnames", "parent": 99999999, "consumes": [ 3 ], "produces": [ - 74 + 76 ] }, { - "id": 95, + "id": 97, "name": "internetdb", "parent": 99999999, "consumes": [ @@ -1435,18 +1472,18 @@ ] }, { - "id": 96, + "id": 98, "name": "ip2location", "parent": 99999999, "consumes": [ 12 ], "produces": [ - 97 + 99 ] }, { - "id": 98, + "id": 100, "name": "ipneighbor", "parent": 99999999, "consumes": [ @@ -1457,18 +1494,18 @@ ] }, { - "id": 99, + "id": 101, "name": "ipstack", "parent": 99999999, "consumes": [ 12 ], "produces": [ - 97 + 99 ] }, { - "id": 100, + "id": 102, "name": "jadx", "parent": 99999999, "consumes": [ @@ -1479,7 +1516,7 @@ ] }, { - "id": 101, + "id": 103, "name": "leakix", "parent": 99999999, "consumes": [ @@ -1490,7 +1527,7 @@ ] }, { - "id": 102, + "id": 104, "name": "myssl", "parent": 99999999, "consumes": [ @@ -1501,7 +1538,7 @@ ] }, { - "id": 103, + "id": 105, "name": "newsletters", "parent": 99999999, "consumes": [ @@ -1512,7 +1549,20 @@ ] }, { - "id": 104, + "id": 106, + "name": "nmap_xml", + "parent": 99999999, + "consumes": [ + 7, + 2, + 12, + 15, + 79 + ], + "produces": [] + }, + { + "id": 107, "name": "ntlm", "parent": 99999999, "consumes": [ @@ -1525,7 +1575,7 @@ ] }, { - "id": 105, + "id": 108, "name": "nuclei", "parent": 99999999, "consumes": [ @@ -1538,7 +1588,7 @@ ] }, { - "id": 106, + "id": 109, "name": "oauth", "parent": 99999999, "consumes": [ @@ -1550,7 +1600,7 @@ ] }, { - "id": 107, + "id": 110, "name": "otx", "parent": 99999999, "consumes": [ @@ -1561,45 +1611,45 @@ ] }, { - "id": 108, + "id": 111, "name": "paramminer_cookies", "parent": 99999999, "consumes": [ 2, - 70 + 72 ], "produces": [ 4, - 70 + 72 ] }, { - "id": 109, + "id": 112, "name": "paramminer_getparams", "parent": 99999999, "consumes": [ 2, - 70 + 72 ], "produces": [ 4, - 70 + 72 ] }, { - "id": 110, + "id": 113, "name": "paramminer_headers", "parent": 99999999, "consumes": [ 2, - 70 + 72 ], "produces": [ - 70 + 72 ] }, { - "id": 111, + "id": 114, "name": "passivetotal", "parent": 99999999, "consumes": [ @@ -1610,7 +1660,7 @@ ] }, { - "id": 112, + "id": 115, "name": "pgp", "parent": 99999999, "consumes": [ @@ -1621,32 +1671,32 @@ ] }, { - "id": 113, + "id": 116, "name": "portscan", "parent": 99999999, "consumes": [ 7, 12, - 114 + 117 ], "produces": [ 15 ] }, { - "id": 115, + "id": 118, "name": "postman", "parent": 99999999, "consumes": [ - 63, - 64 + 65, + 66 ], "produces": [ 43 ] }, { - "id": 116, + "id": 119, "name": "postman_download", "parent": 99999999, "consumes": [ @@ -1657,7 +1707,7 @@ ] }, { - "id": 117, + "id": 120, "name": "rapiddns", "parent": 99999999, "consumes": [ @@ -1668,7 +1718,7 @@ ] }, { - "id": 118, + "id": 121, "name": "robots", "parent": 99999999, "consumes": [ @@ -1679,7 +1729,7 @@ ] }, { - "id": 119, + "id": 122, "name": "secretsdb", "parent": 99999999, "consumes": [ @@ -1690,7 +1740,7 @@ ] }, { - "id": 120, + "id": 123, "name": "securitytrails", "parent": 99999999, "consumes": [ @@ -1701,7 +1751,7 @@ ] }, { - "id": 121, + "id": 124, "name": "securitytxt", "parent": 99999999, "consumes": [ @@ -1713,7 +1763,7 @@ ] }, { - "id": 122, + "id": 125, "name": "shodan_dns", "parent": 99999999, "consumes": [ @@ -1724,7 +1774,7 @@ ] }, { - "id": 123, + "id": 126, "name": "sitedossier", "parent": 99999999, "consumes": [ @@ -1735,7 +1785,7 @@ ] }, { - "id": 124, + "id": 127, "name": "skymem", "parent": 99999999, "consumes": [ @@ -1746,7 +1796,7 @@ ] }, { - "id": 125, + "id": 128, "name": "smuggler", "parent": 99999999, "consumes": [ @@ -1757,28 +1807,28 @@ ] }, { - "id": 126, + "id": 129, "name": "social", "parent": 99999999, "consumes": [ 19 ], "produces": [ - 64 + 66 ] }, { - "id": 127, + "id": 130, "name": "speculate", "parent": 99999999, "consumes": [ - 128, + 131, 7, 22, 2, 12, - 114, - 64, + 117, + 66, 24, 3, 19, @@ -1789,11 +1839,11 @@ 4, 12, 15, - 63 + 65 ] }, { - "id": 129, + "id": 132, "name": "sslcert", "parent": 99999999, "consumes": [ @@ -1805,7 +1855,7 @@ ] }, { - "id": 130, + "id": 133, "name": "subdomaincenter", "parent": 99999999, "consumes": [ @@ -1816,7 +1866,7 @@ ] }, { - "id": 131, + "id": 134, "name": "subdomainradar", "parent": 99999999, "consumes": [ @@ -1827,7 +1877,7 @@ ] }, { - "id": 132, + "id": 135, "name": "subdomains", "parent": 99999999, "consumes": [ @@ -1837,7 +1887,7 @@ "produces": [] }, { - "id": 133, + "id": 136, "name": "telerik", "parent": 99999999, "consumes": [ @@ -1850,7 +1900,7 @@ ] }, { - "id": 134, + "id": 137, "name": "trickest", "parent": 99999999, "consumes": [ @@ -1861,7 +1911,7 @@ ] }, { - "id": 135, + "id": 138, "name": "trufflehog", "parent": 99999999, "consumes": [ @@ -1874,7 +1924,7 @@ ] }, { - "id": 136, + "id": 139, "name": "url_manipulation", "parent": 99999999, "consumes": [ @@ -1885,7 +1935,7 @@ ] }, { - "id": 137, + "id": 140, "name": "urlscan", "parent": 99999999, "consumes": [ @@ -1897,7 +1947,7 @@ ] }, { - "id": 138, + "id": 141, "name": "vhost", "parent": 99999999, "consumes": [ @@ -1905,11 +1955,11 @@ ], "produces": [ 7, - 139 + 142 ] }, { - "id": 140, + "id": 143, "name": "viewdns", "parent": 99999999, "consumes": [ @@ -1920,7 +1970,7 @@ ] }, { - "id": 141, + "id": 144, "name": "virustotal", "parent": 99999999, "consumes": [ @@ -1931,7 +1981,7 @@ ] }, { - "id": 142, + "id": 145, "name": "wafw00f", "parent": 99999999, "consumes": [ @@ -1942,7 +1992,7 @@ ] }, { - "id": 143, + "id": 146, "name": "wappalyzer", "parent": 99999999, "consumes": [ @@ -1953,7 +2003,7 @@ ] }, { - "id": 144, + "id": 147, "name": "wayback", "parent": 99999999, "consumes": [ @@ -1965,20 +2015,20 @@ ] }, { - "id": 145, + "id": 148, "name": "web_report", "parent": 99999999, "consumes": [ 4, 16, 3, - 139, + 142, 5 ], "produces": [] }, { - "id": 146, + "id": 149, "name": "wpscan", "parent": 99999999, "consumes": [ @@ -1993,7 +2043,7 @@ ] }, { - "id": 147, + "id": 150, "name": "zoomeye", "parent": 99999999, "consumes": [ diff --git a/docs/data/chord_graph/rels.json b/docs/data/chord_graph/rels.json index 96980c3fd0..f25e491f3e 100644 --- a/docs/data/chord_graph/rels.json +++ b/docs/data/chord_graph/rels.json @@ -585,32 +585,32 @@ "type": "produces" }, { - "source": 61, - "target": 43, - "type": "consumes" + "source": 7, + "target": 61, + "type": "produces" }, { - "source": 10, + "source": 12, "target": 61, "type": "produces" }, { - "source": 62, - "target": 63, - "type": "consumes" + "source": 55, + "target": 61, + "type": "produces" }, { "source": 62, - "target": 64, + "target": 7, "type": "consumes" }, { - "source": 43, + "source": 46, "target": 62, "type": "produces" }, { - "source": 64, + "source": 55, "target": 62, "type": "produces" }, @@ -620,1083 +620,1143 @@ "type": "produces" }, { - "source": 65, + "source": 63, + "target": 43, + "type": "consumes" + }, + { + "source": 10, + "target": 63, + "type": "produces" + }, + { + "source": 64, + "target": 65, + "type": "consumes" + }, + { + "source": 64, + "target": 66, + "type": "consumes" + }, + { + "source": 43, + "target": 64, + "type": "produces" + }, + { + "source": 66, + "target": 64, + "type": "produces" + }, + { + "source": 19, + "target": 64, + "type": "produces" + }, + { + "source": 67, "target": 2, "type": "consumes" }, { "source": 16, - "target": 65, + "target": 67, "type": "produces" }, { "source": 5, - "target": 65, + "target": 67, "type": "produces" }, { - "source": 66, + "source": 68, "target": 7, "type": "consumes" }, { "source": 46, - "target": 66, + "target": 68, "type": "produces" }, { - "source": 67, + "source": 69, "target": 46, "type": "consumes" }, { - "source": 68, + "source": 70, "target": 2, "type": "consumes" }, { - "source": 68, - "target": 69, + "source": 70, + "target": 71, "type": "consumes" }, { "source": 19, - "target": 68, + "target": 70, "type": "produces" }, { - "source": 70, - "target": 68, + "source": 72, + "target": 70, "type": "produces" }, { - "source": 71, + "source": 73, "target": 10, "type": "consumes" }, { - "source": 69, - "target": 71, + "source": 71, + "target": 73, "type": "produces" }, { - "source": 72, + "source": 74, "target": 3, "type": "consumes" }, { "source": 19, - "target": 72, + "target": 74, "type": "produces" }, { - "source": 73, - "target": 74, + "source": 75, + "target": 76, "type": "consumes" }, { "source": 19, - "target": 73, + "target": 75, "type": "produces" }, { - "source": 75, + "source": 77, "target": 2, "type": "consumes" }, { - "source": 75, + "source": 77, "target": 19, "type": "consumes" }, { "source": 10, - "target": 75, + "target": 77, "type": "produces" }, { - "source": 76, + "source": 78, "target": 15, "type": "consumes" }, { - "source": 77, - "target": 76, + "source": 79, + "target": 78, "type": "produces" }, { - "source": 78, + "source": 80, "target": 7, "type": "consumes" }, { "source": 7, - "target": 78, + "target": 80, "type": "produces" }, { - "source": 79, + "source": 81, "target": 3, "type": "consumes" }, { "source": 5, - "target": 79, + "target": 81, "type": "produces" }, { - "source": 80, + "source": 82, "target": 3, "type": "consumes" }, { "source": 4, - "target": 80, + "target": 82, "type": "produces" }, { - "source": 81, + "source": 83, "target": 43, "type": "consumes" }, { "source": 10, - "target": 81, + "target": 83, "type": "produces" }, { - "source": 82, + "source": 84, "target": 7, "type": "consumes" }, { "source": 43, - "target": 82, + "target": 84, "type": "produces" }, { "source": 19, - "target": 82, + "target": 84, "type": "produces" }, { - "source": 83, - "target": 63, + "source": 85, + "target": 65, "type": "consumes" }, { - "source": 83, - "target": 64, + "source": 85, + "target": 66, "type": "consumes" }, { "source": 43, - "target": 83, + "target": 85, "type": "produces" }, { - "source": 84, + "source": 86, "target": 43, "type": "consumes" }, { "source": 10, - "target": 84, + "target": 86, "type": "produces" }, { - "source": 85, + "source": 87, "target": 2, "type": "consumes" }, { - "source": 85, - "target": 64, + "source": 87, + "target": 66, "type": "consumes" }, { - "source": 85, + "source": 87, "target": 16, "type": "consumes" }, { "source": 43, - "target": 85, + "target": 87, "type": "produces" }, { "source": 4, - "target": 85, + "target": 87, "type": "produces" }, { - "source": 64, - "target": 85, + "source": 66, + "target": 87, "type": "produces" }, { "source": 16, - "target": 85, + "target": 87, "type": "produces" }, { - "source": 86, + "source": 88, "target": 43, "type": "consumes" }, { - "source": 86, - "target": 63, + "source": 88, + "target": 65, "type": "consumes" }, { "source": 9, - "target": 86, + "target": 88, "type": "produces" }, { - "source": 87, - "target": 64, + "source": 89, + "target": 66, "type": "consumes" }, { - "source": 87, + "source": 89, "target": 3, "type": "consumes" }, { "source": 16, - "target": 87, + "target": 89, "type": "produces" }, { "source": 3, - "target": 87, + "target": 89, "type": "produces" }, { "source": 19, - "target": 87, + "target": 89, "type": "produces" }, { - "source": 88, - "target": 87, + "source": 90, + "target": 89, "type": "produces" }, { - "source": 89, + "source": 91, "target": 7, "type": "consumes" }, { "source": 7, - "target": 89, + "target": 91, "type": "produces" }, { - "source": 90, + "source": 92, "target": 2, "type": "consumes" }, { "source": 4, - "target": 90, + "target": 92, "type": "produces" }, { - "source": 91, + "source": 93, "target": 15, "type": "consumes" }, { - "source": 91, + "source": 93, "target": 3, "type": "consumes" }, { - "source": 91, + "source": 93, "target": 19, "type": "consumes" }, { "source": 2, - "target": 91, + "target": 93, "type": "produces" }, { "source": 3, - "target": 91, + "target": 93, "type": "produces" }, { - "source": 92, - "target": 70, + "source": 94, + "target": 72, "type": "consumes" }, { "source": 4, - "target": 92, + "target": 94, "type": "produces" }, { - "source": 93, + "source": 95, "target": 7, "type": "consumes" }, { "source": 7, - "target": 93, + "target": 95, "type": "produces" }, { "source": 46, - "target": 93, + "target": 95, "type": "produces" }, { "source": 19, - "target": 93, + "target": 95, "type": "produces" }, { - "source": 94, + "source": 96, "target": 3, "type": "consumes" }, { - "source": 74, - "target": 94, + "source": 76, + "target": 96, "type": "produces" }, { - "source": 95, + "source": 97, "target": 7, "type": "consumes" }, { - "source": 95, + "source": 97, "target": 12, "type": "consumes" }, { "source": 7, - "target": 95, + "target": 97, "type": "produces" }, { "source": 4, - "target": 95, + "target": 97, "type": "produces" }, { "source": 15, - "target": 95, + "target": 97, "type": "produces" }, { "source": 16, - "target": 95, + "target": 97, "type": "produces" }, { "source": 5, - "target": 95, + "target": 97, "type": "produces" }, { - "source": 96, + "source": 98, "target": 12, "type": "consumes" }, { - "source": 97, - "target": 96, + "source": 99, + "target": 98, "type": "produces" }, { - "source": 98, + "source": 100, "target": 12, "type": "consumes" }, { "source": 12, - "target": 98, + "target": 100, "type": "produces" }, { - "source": 99, + "source": 101, "target": 12, "type": "consumes" }, { - "source": 97, - "target": 99, + "source": 99, + "target": 101, "type": "produces" }, { - "source": 100, + "source": 102, "target": 10, "type": "consumes" }, { "source": 10, - "target": 100, + "target": 102, "type": "produces" }, { - "source": 101, + "source": 103, "target": 7, "type": "consumes" }, { "source": 7, - "target": 101, + "target": 103, "type": "produces" }, { - "source": 102, + "source": 104, "target": 7, "type": "consumes" }, { "source": 7, - "target": 102, + "target": 104, "type": "produces" }, { - "source": 103, + "source": 105, "target": 2, "type": "consumes" }, { "source": 4, - "target": 103, + "target": 105, "type": "produces" }, { - "source": 104, + "source": 106, + "target": 7, + "type": "consumes" + }, + { + "source": 106, "target": 2, "type": "consumes" }, { - "source": 104, + "source": 106, + "target": 12, + "type": "consumes" + }, + { + "source": 106, + "target": 15, + "type": "consumes" + }, + { + "source": 106, + "target": 79, + "type": "consumes" + }, + { + "source": 107, + "target": 2, + "type": "consumes" + }, + { + "source": 107, "target": 3, "type": "consumes" }, { "source": 7, - "target": 104, + "target": 107, "type": "produces" }, { "source": 4, - "target": 104, + "target": 107, "type": "produces" }, { - "source": 105, + "source": 108, "target": 3, "type": "consumes" }, { "source": 4, - "target": 105, + "target": 108, "type": "produces" }, { "source": 16, - "target": 105, + "target": 108, "type": "produces" }, { "source": 5, - "target": 105, + "target": 108, "type": "produces" }, { - "source": 106, + "source": 109, "target": 7, "type": "consumes" }, { - "source": 106, + "source": 109, "target": 19, "type": "consumes" }, { "source": 7, - "target": 106, + "target": 109, "type": "produces" }, { - "source": 107, + "source": 110, "target": 7, "type": "consumes" }, { "source": 7, - "target": 107, + "target": 110, "type": "produces" }, { - "source": 108, + "source": 111, "target": 2, "type": "consumes" }, { - "source": 108, - "target": 70, + "source": 111, + "target": 72, "type": "consumes" }, { "source": 4, - "target": 108, + "target": 111, "type": "produces" }, { - "source": 70, - "target": 108, + "source": 72, + "target": 111, "type": "produces" }, { - "source": 109, + "source": 112, "target": 2, "type": "consumes" }, { - "source": 109, - "target": 70, + "source": 112, + "target": 72, "type": "consumes" }, { "source": 4, - "target": 109, + "target": 112, "type": "produces" }, { - "source": 70, - "target": 109, + "source": 72, + "target": 112, "type": "produces" }, { - "source": 110, + "source": 113, "target": 2, "type": "consumes" }, { - "source": 110, - "target": 70, + "source": 113, + "target": 72, "type": "consumes" }, { - "source": 70, - "target": 110, + "source": 72, + "target": 113, "type": "produces" }, { - "source": 111, + "source": 114, "target": 7, "type": "consumes" }, { "source": 7, - "target": 111, + "target": 114, "type": "produces" }, { - "source": 112, + "source": 115, "target": 7, "type": "consumes" }, { "source": 46, - "target": 112, + "target": 115, "type": "produces" }, { - "source": 113, + "source": 116, "target": 7, "type": "consumes" }, { - "source": 113, + "source": 116, "target": 12, "type": "consumes" }, { - "source": 113, - "target": 114, + "source": 116, + "target": 117, "type": "consumes" }, { "source": 15, - "target": 113, + "target": 116, "type": "produces" }, { - "source": 115, - "target": 63, + "source": 118, + "target": 65, "type": "consumes" }, { - "source": 115, - "target": 64, + "source": 118, + "target": 66, "type": "consumes" }, { "source": 43, - "target": 115, + "target": 118, "type": "produces" }, { - "source": 116, + "source": 119, "target": 43, "type": "consumes" }, { "source": 10, - "target": 116, + "target": 119, "type": "produces" }, { - "source": 117, + "source": 120, "target": 7, "type": "consumes" }, { "source": 7, - "target": 117, + "target": 120, "type": "produces" }, { - "source": 118, + "source": 121, "target": 3, "type": "consumes" }, { "source": 19, - "target": 118, + "target": 121, "type": "produces" }, { - "source": 119, + "source": 122, "target": 2, "type": "consumes" }, { "source": 4, - "target": 119, + "target": 122, "type": "produces" }, { - "source": 120, + "source": 123, "target": 7, "type": "consumes" }, { "source": 7, - "target": 120, + "target": 123, "type": "produces" }, { - "source": 121, + "source": 124, "target": 7, "type": "consumes" }, { "source": 46, - "target": 121, + "target": 124, "type": "produces" }, { "source": 19, - "target": 121, + "target": 124, "type": "produces" }, { - "source": 122, + "source": 125, "target": 7, "type": "consumes" }, { "source": 7, - "target": 122, + "target": 125, "type": "produces" }, { - "source": 123, + "source": 126, "target": 7, "type": "consumes" }, { "source": 7, - "target": 123, + "target": 126, "type": "produces" }, { - "source": 124, + "source": 127, "target": 7, "type": "consumes" }, { "source": 46, - "target": 124, + "target": 127, "type": "produces" }, { - "source": 125, + "source": 128, "target": 3, "type": "consumes" }, { "source": 4, - "target": 125, + "target": 128, "type": "produces" }, { - "source": 126, + "source": 129, "target": 19, "type": "consumes" }, { - "source": 64, - "target": 126, + "source": 66, + "target": 129, "type": "produces" }, { - "source": 127, - "target": 128, + "source": 130, + "target": 131, "type": "consumes" }, { - "source": 127, + "source": 130, "target": 7, "type": "consumes" }, { - "source": 127, + "source": 130, "target": 22, "type": "consumes" }, { - "source": 127, + "source": 130, "target": 2, "type": "consumes" }, { - "source": 127, + "source": 130, "target": 12, "type": "consumes" }, { - "source": 127, - "target": 114, + "source": 130, + "target": 117, "type": "consumes" }, { - "source": 127, - "target": 64, + "source": 130, + "target": 66, "type": "consumes" }, { - "source": 127, + "source": 130, "target": 24, "type": "consumes" }, { - "source": 127, + "source": 130, "target": 3, "type": "consumes" }, { - "source": 127, + "source": 130, "target": 19, "type": "consumes" }, { - "source": 127, + "source": 130, "target": 49, "type": "consumes" }, { "source": 7, - "target": 127, + "target": 130, "type": "produces" }, { "source": 4, - "target": 127, + "target": 130, "type": "produces" }, { "source": 12, - "target": 127, + "target": 130, "type": "produces" }, { "source": 15, - "target": 127, + "target": 130, "type": "produces" }, { - "source": 63, - "target": 127, + "source": 65, + "target": 130, "type": "produces" }, { - "source": 129, + "source": 132, "target": 15, "type": "consumes" }, { "source": 7, - "target": 129, + "target": 132, "type": "produces" }, { "source": 46, - "target": 129, + "target": 132, "type": "produces" }, { - "source": 130, + "source": 133, "target": 7, "type": "consumes" }, { "source": 7, - "target": 130, + "target": 133, "type": "produces" }, { - "source": 131, + "source": 134, "target": 7, "type": "consumes" }, { "source": 7, - "target": 131, + "target": 134, "type": "produces" }, { - "source": 132, + "source": 135, "target": 7, "type": "consumes" }, { - "source": 132, + "source": 135, "target": 22, "type": "consumes" }, { - "source": 133, + "source": 136, "target": 2, "type": "consumes" }, { - "source": 133, + "source": 136, "target": 3, "type": "consumes" }, { "source": 4, - "target": 133, + "target": 136, "type": "produces" }, { "source": 5, - "target": 133, + "target": 136, "type": "produces" }, { - "source": 134, + "source": 137, "target": 7, "type": "consumes" }, { "source": 7, - "target": 134, + "target": 137, "type": "produces" }, { - "source": 135, + "source": 138, "target": 43, "type": "consumes" }, { - "source": 135, + "source": 138, "target": 10, "type": "consumes" }, { "source": 4, - "target": 135, + "target": 138, "type": "produces" }, { "source": 5, - "target": 135, + "target": 138, "type": "produces" }, { - "source": 136, + "source": 139, "target": 3, "type": "consumes" }, { "source": 4, - "target": 136, + "target": 139, "type": "produces" }, { - "source": 137, + "source": 140, "target": 7, "type": "consumes" }, { "source": 7, - "target": 137, + "target": 140, "type": "produces" }, { "source": 19, - "target": 137, + "target": 140, "type": "produces" }, { - "source": 138, + "source": 141, "target": 3, "type": "consumes" }, { "source": 7, - "target": 138, + "target": 141, "type": "produces" }, { - "source": 139, - "target": 138, + "source": 142, + "target": 141, "type": "produces" }, { - "source": 140, + "source": 143, "target": 7, "type": "consumes" }, { "source": 7, - "target": 140, + "target": 143, "type": "produces" }, { - "source": 141, + "source": 144, "target": 7, "type": "consumes" }, { "source": 7, - "target": 141, + "target": 144, "type": "produces" }, { - "source": 142, + "source": 145, "target": 3, "type": "consumes" }, { "source": 17, - "target": 142, + "target": 145, "type": "produces" }, { - "source": 143, + "source": 146, "target": 2, "type": "consumes" }, { "source": 16, - "target": 143, + "target": 146, "type": "produces" }, { - "source": 144, + "source": 147, "target": 7, "type": "consumes" }, { "source": 7, - "target": 144, + "target": 147, "type": "produces" }, { "source": 19, - "target": 144, + "target": 147, "type": "produces" }, { - "source": 145, + "source": 148, "target": 4, "type": "consumes" }, { - "source": 145, + "source": 148, "target": 16, "type": "consumes" }, { - "source": 145, + "source": 148, "target": 3, "type": "consumes" }, { - "source": 145, - "target": 139, + "source": 148, + "target": 142, "type": "consumes" }, { - "source": 145, + "source": 148, "target": 5, "type": "consumes" }, { - "source": 146, + "source": 149, "target": 2, "type": "consumes" }, { - "source": 146, + "source": 149, "target": 16, "type": "consumes" }, { "source": 4, - "target": 146, + "target": 149, "type": "produces" }, { "source": 16, - "target": 146, + "target": 149, "type": "produces" }, { "source": 19, - "target": 146, + "target": 149, "type": "produces" }, { "source": 5, - "target": 146, + "target": 149, "type": "produces" }, { - "source": 147, + "source": 150, "target": 7, "type": "consumes" }, { "source": 7, - "target": 147, + "target": 150, "type": "produces" } ] \ No newline at end of file diff --git a/docs/dev/dev_environment.md b/docs/dev/dev_environment.md index d3fdee3cf6..b73f660574 100644 --- a/docs/dev/dev_environment.md +++ b/docs/dev/dev_environment.md @@ -33,7 +33,7 @@ bbot --help ```bash # auto-format code indentation, etc. -black . +ruff format # run tests ./bbot/test/run_tests.sh diff --git a/docs/dev/helpers/index.md b/docs/dev/helpers/index.md index 60d64f793d..cc27ed1f2b 100644 --- a/docs/dev/helpers/index.md +++ b/docs/dev/helpers/index.md @@ -6,7 +6,7 @@ The vast majority of these helpers can be accessed directly from the `.helpers` ```python class MyModule(BaseModule): - + ... async def handle_event(self, event): diff --git a/docs/dev/target.md b/docs/dev/target.md index b2e4bffe31..6740cfb744 100644 --- a/docs/dev/target.md +++ b/docs/dev/target.md @@ -1 +1,9 @@ -::: bbot.scanner.target.Target +::: bbot.scanner.target.BaseTarget + +::: bbot.scanner.target.ScanSeeds + +::: bbot.scanner.target.ScanWhitelist + +::: bbot.scanner.target.ScanBlacklist + +::: bbot.scanner.target.BBOTTarget diff --git a/docs/dev/tests.md b/docs/dev/tests.md index f5d05fcf99..4381981812 100644 --- a/docs/dev/tests.md +++ b/docs/dev/tests.md @@ -2,20 +2,20 @@ BBOT takes tests seriously. Every module *must* have a custom-written test that *actually tests* its functionality. Don't worry if you want to contribute but you aren't used to writing tests. If you open a draft PR, we will help write them :) -We use [black](https://github.com/psf/black) and [flake8](https://flake8.pycqa.org/en/latest/) for linting, and [pytest](https://docs.pytest.org/en/8.2.x/) for tests. +We use [ruff](https://docs.astral.sh/ruff/) for linting, and [pytest](https://docs.pytest.org/en/8.2.x/) for tests. ## Running tests locally -We have Github actions that automatically run tests whenever you open a Pull Request. However, you can also run the tests locally with `pytest`: +We have GitHub Actions that automatically run tests whenever you open a Pull Request. However, you can also run the tests locally with `pytest`: ```bash -# format code with black -poetry run black . +# lint with ruff +poetry run ruff check -# lint with flake8 -poetry run flake8 +# format code with ruff +poetry run ruff format -# run all tests with pytest (takes rougly 30 minutes) +# run all tests with pytest (takes roughly 30 minutes) poetry run pytest ``` diff --git a/docs/index.md b/docs/index.md index 3d6c5ef267..355d58e8b2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -34,6 +34,8 @@ bbot --help Docker images are provided, along with helper script `bbot-docker.sh` to persist your scan data. +Scans are output to `~/.bbot/scans` (the usual place for BBOT scan data). + ```bash # bleeding edge (dev) docker run -it blacklanternsecurity/bbot --help @@ -46,6 +48,16 @@ git clone https://github.com/blacklanternsecurity/bbot && cd bbot ./bbot-docker.sh --help ``` +Note: If you need to pass in a custom preset, you can do so by mapping the preset into the container: + +```bash +# use the preset `my_preset.yml` from the current directory +docker run --rm -it \ + -v "$HOME/.bbot/scans:/root/.bbot/scans" \ + -v "$PWD/my_preset.yml:/my_preset.yml" \ + blacklanternsecurity/bbot -p /my_preset.yml +``` + ## Example Commands Below are some examples of common scans. diff --git a/docs/javascripts/tablesort.min.js b/docs/javascripts/tablesort.min.js index 65a83b1138..fcd3b078ef 100644 --- a/docs/javascripts/tablesort.min.js +++ b/docs/javascripts/tablesort.min.js @@ -3,4 +3,4 @@ * http://tristen.ca/tablesort/demo/ * Copyright (c) 2021 ; Licensed MIT */ -!function(){function a(b,c){if(!(this instanceof a))return new a(b,c);if(!b||"TABLE"!==b.tagName)throw new Error("Element must be a table");this.init(b,c||{})}var b=[],c=function(a){var b;return window.CustomEvent&&"function"==typeof window.CustomEvent?b=new CustomEvent(a):(b=document.createEvent("CustomEvent"),b.initCustomEvent(a,!1,!1,void 0)),b},d=function(a,b){return a.getAttribute(b.sortAttribute||"data-sort")||a.textContent||a.innerText||""},e=function(a,b){return a=a.trim().toLowerCase(),b=b.trim().toLowerCase(),a===b?0:a0)if(a.tHead&&a.tHead.rows.length>0){for(e=0;e0&&n.push(m),o++;if(!n)return}for(o=0;o0)if(a.tHead&&a.tHead.rows.length>0){for(e=0;e0&&n.push(m),o++;if(!n)return}for(o=0;o Most of these you probably will **NOT** want to change. In particular, we advise against changing the version of Nuclei, as it's possible the latest version won't work right with BBOT. @@ -60,7 +60,7 @@ We also do not recommend changing **directory_only** mode. This will cause Nucle ### Modes ### -The modes with the Nuclei module are generally in place to help you limit the number of templates you are scanning with, to make your scans quicker. +The modes with the Nuclei module are generally in place to help you limit the number of templates you are scanning with, to make your scans quicker. #### Manual @@ -78,10 +78,10 @@ This is equivalent to the Nuclei '-as' scan option. It only use templates that m Budget mode is unique to BBOT. -For larger scans with thousands of targets, doing a FULL Nuclei scan (1000s of Requests) for each is not realistic. -As an alternative to the other modes, you can take advantage of Nuclei's "collapsible" template feature. +For larger scans with thousands of targets, doing a FULL Nuclei scan (1000s of Requests) for each is not realistic. +As an alternative to the other modes, you can take advantage of Nuclei's "collapsible" template feature. -For only the cost of one (or more) "extra" request(s) per host, it can activate several hundred modules. These are modules which happen to look at a BaseUrl, and typically look for a specific string or other attribute. Nuclei is smart about reusing the request data when it can, and we can use this to our advantage. +For only the cost of one (or more) "extra" request(s) per host, it can activate several hundred modules. These are modules which happen to look at a BaseUrl, and typically look for a specific string or other attribute. Nuclei is smart about reusing the request data when it can, and we can use this to our advantage. The budget parameter is the # of extra requests per host you are willing to send to "feed" Nuclei templates (defaults to 1). For those times when vulnerability scanning isn't the main focus, but you want to look for easy wins. diff --git a/docs/release_history.md b/docs/release_history.md index 7fd343f513..cf1f140688 100644 --- a/docs/release_history.md +++ b/docs/release_history.md @@ -1,47 +1,51 @@ +### 2.2.0 - Nov 18, 2024 +- [https://github.com/blacklanternsecurity/bbot/pull/1919](https://github.com/blacklanternsecurity/bbot/pull/1919) + ### 2.1.2 - Nov 1, 2024 -- https://github.com/blacklanternsecurity/bbot/pull/1909 +- [https://github.com/blacklanternsecurity/bbot/pull/1909](https://github.com/blacklanternsecurity/bbot/pull/1909) ### 2.1.1 - Oct 31, 2024 -- https://github.com/blacklanternsecurity/bbot/pull/1885 +- [https://github.com/blacklanternsecurity/bbot/pull/1885](https://github.com/blacklanternsecurity/bbot/pull/1885) ### 2.1.0 - Oct 18, 2024 -- https://github.com/blacklanternsecurity/bbot/pull/1724 +- [https://github.com/blacklanternsecurity/bbot/pull/1724](https://github.com/blacklanternsecurity/bbot/pull/1724) ### 2.0.1 - Aug 29, 2024 -- https://github.com/blacklanternsecurity/bbot/pull/1650 +- [https://github.com/blacklanternsecurity/bbot/pull/1650](https://github.com/blacklanternsecurity/bbot/pull/1650) ### 2.0.0 - Aug 9, 2024 -- https://github.com/blacklanternsecurity/bbot/pull/1424 +- [https://github.com/blacklanternsecurity/bbot/pull/1424](https://github.com/blacklanternsecurity/bbot/pull/1424) +- [https://github.com/blacklanternsecurity/bbot/pull/1235](https://github.com/blacklanternsecurity/bbot/pull/1235) ### 1.1.8 - May 29, 2024 -- https://github.com/blacklanternsecurity/bbot/pull/1382 +- [https://github.com/blacklanternsecurity/bbot/pull/1382](https://github.com/blacklanternsecurity/bbot/pull/1382) ### 1.1.7 - May 15, 2024 -- https://github.com/blacklanternsecurity/bbot/pull/1119 +- [https://github.com/blacklanternsecurity/bbot/pull/1119](https://github.com/blacklanternsecurity/bbot/pull/1119) ### 1.1.6 - Feb 21, 2024 -- https://github.com/blacklanternsecurity/bbot/pull/1002 +- [https://github.com/blacklanternsecurity/bbot/pull/1002](https://github.com/blacklanternsecurity/bbot/pull/1002) ### 1.1.5 - Jan 15, 2024 -- https://github.com/blacklanternsecurity/bbot/pull/996 +- [https://github.com/blacklanternsecurity/bbot/pull/996](https://github.com/blacklanternsecurity/bbot/pull/996) ### 1.1.4 - Jan 11, 2024 -- https://github.com/blacklanternsecurity/bbot/pull/837 +- [https://github.com/blacklanternsecurity/bbot/pull/837](https://github.com/blacklanternsecurity/bbot/pull/837) ### 1.1.3 - Nov 4, 2023 -- https://github.com/blacklanternsecurity/bbot/pull/823 +- [https://github.com/blacklanternsecurity/bbot/pull/823](https://github.com/blacklanternsecurity/bbot/pull/823) ### 1.1.2 - Nov 3, 2023 -- https://github.com/blacklanternsecurity/bbot/pull/777 +- [https://github.com/blacklanternsecurity/bbot/pull/777](https://github.com/blacklanternsecurity/bbot/pull/777) ### 1.1.1 - Oct 11, 2023 -- https://github.com/blacklanternsecurity/bbot/pull/668 +- [https://github.com/blacklanternsecurity/bbot/pull/668](https://github.com/blacklanternsecurity/bbot/pull/668) ### 1.1.0 - Aug 4, 2023 -- https://github.com/blacklanternsecurity/bbot/pull/598 +- [https://github.com/blacklanternsecurity/bbot/pull/598](https://github.com/blacklanternsecurity/bbot/pull/598) ### 1.0.5 - Mar 10, 2023 -- https://github.com/blacklanternsecurity/bbot/pull/352 +- [https://github.com/blacklanternsecurity/bbot/pull/352](https://github.com/blacklanternsecurity/bbot/pull/352) ### 1.0.5 - Mar 10, 2023 -- https://github.com/blacklanternsecurity/bbot/pull/352 \ No newline at end of file +- [https://github.com/blacklanternsecurity/bbot/pull/352](https://github.com/blacklanternsecurity/bbot/pull/352) diff --git a/docs/scanning/advanced.md b/docs/scanning/advanced.md index 355aabc250..88f75da5c1 100644 --- a/docs/scanning/advanced.md +++ b/docs/scanning/advanced.md @@ -38,12 +38,13 @@ usage: bbot [-h] [-t TARGET [TARGET ...]] [-w WHITELIST [WHITELIST ...]] [-m MODULE [MODULE ...]] [-l] [-lmo] [-em MODULE [MODULE ...]] [-f FLAG [FLAG ...]] [-lf] [-rf FLAG [FLAG ...]] [-ef FLAG [FLAG ...]] [--allow-deadly] [-n SCAN_NAME] [-v] [-d] - [-s] [--force] [-y] [--dry-run] [--current-preset] - [--current-preset-full] [-o DIR] [-om MODULE [MODULE ...]] - [--json] [--brief] + [-s] [--force] [-y] [--fast-mode] [--dry-run] + [--current-preset] [--current-preset-full] + [-om MODULE [MODULE ...]] [-lo] [-o DIR] [--json] [--brief] [--event-types EVENT_TYPES [EVENT_TYPES ...]] [--no-deps | --force-deps | --retry-deps | --ignore-failed-deps | --install-all-deps] - [--version] [-H CUSTOM_HEADERS [CUSTOM_HEADERS ...]] + [--version] [--proxy HTTP_PROXY] + [-H CUSTOM_HEADERS [CUSTOM_HEADERS ...]] [--custom-yara-rules CUSTOM_YARA_RULES] Bighuge BLS OSINT Tool @@ -69,14 +70,14 @@ Presets: Modules: -m MODULE [MODULE ...], --modules MODULE [MODULE ...] - Modules to enable. Choices: sitedossier,crt,postman,ipneighbor,bucket_amazon,baddns_direct,ipstack,extractous,bucket_google,host_header,internetdb,jadx,baddns_zone,bucket_azure,ajaxpro,skymem,censys,postman_download,dockerhub,generic_ssrf,ip2location,gitlab,url_manipulation,paramminer_getparams,builtwith,emailformat,gowitness,github_workflows,bevigil,wayback,subdomaincenter,nuclei,bucket_firebase,bucket_file_enum,badsecrets,httpx,apkpure,leakix,paramminer_headers,chaos,git,filedownload,git_clone,sslcert,virustotal,trufflehog,ffuf,pgp,shodan_dns,certspotter,ntlm,secretsdb,wappalyzer,c99,securitytrails,securitytxt,newsletters,urlscan,dnsdumpster,credshed,baddns,wafw00f,dastardly,azure_tenant,docker_pull,columbus,fullhunt,fingerprintx,hackertarget,github_codesearch,dnsbrute,zoomeye,affiliates,oauth,azure_realm,binaryedge,bufferoverrun,dehashed,dnscaa,dnscommonsrv,myssl,ffuf_shortnames,robots,rapiddns,digitorus,wpscan,hunterio,passivetotal,code_repository,bypass403,vhost,google_playstore,dnsbrute_mutations,anubisdb,viewdns,trickest,portscan,smuggler,iis_shortnames,paramminer_cookies,bucket_digitalocean,dnsbimi,social,otx,github_org,hunt,telerik,subdomainradar,dotnetnuke,asn + Modules to enable. Choices: affiliates,ajaxpro,anubisdb,apkpure,asn,azure_realm,azure_tenant,baddns,baddns_direct,baddns_zone,badsecrets,bevigil,binaryedge,bucket_amazon,bucket_azure,bucket_digitalocean,bucket_file_enum,bucket_firebase,bucket_google,bufferoverrun,builtwith,bypass403,c99,censys,certspotter,chaos,code_repository,columbus,credshed,crt,dastardly,dehashed,digitorus,dnsbimi,dnsbrute,dnsbrute_mutations,dnscaa,dnscommonsrv,dnsdumpster,dnstlsrpt,docker_pull,dockerhub,dotnetnuke,emailformat,extractous,ffuf,ffuf_shortnames,filedownload,fingerprintx,fullhunt,generic_ssrf,git,git_clone,github_codesearch,github_org,github_workflows,gitlab,google_playstore,gowitness,hackertarget,host_header,httpx,hunt,hunterio,iis_shortnames,internetdb,ip2location,ipneighbor,ipstack,jadx,leakix,myssl,newsletters,ntlm,nuclei,oauth,otx,paramminer_cookies,paramminer_getparams,paramminer_headers,passivetotal,pgp,portscan,postman,postman_download,rapiddns,robots,secretsdb,securitytrails,securitytxt,shodan_dns,sitedossier,skymem,smuggler,social,sslcert,subdomaincenter,subdomainradar,telerik,trickest,trufflehog,url_manipulation,urlscan,vhost,viewdns,virustotal,wafw00f,wappalyzer,wayback,wpscan,zoomeye -l, --list-modules List available modules. -lmo, --list-module-options Show all module config options -em MODULE [MODULE ...], --exclude-modules MODULE [MODULE ...] Exclude these modules. -f FLAG [FLAG ...], --flags FLAG [FLAG ...] - Enable modules by flag. Choices: email-enum,affiliates,web-screenshots,subdomain-hijack,baddns,portscan,iis-shortnames,safe,web-thorough,active,cloud-enum,web-basic,passive,report,code-enum,subdomain-enum,slow,aggressive,social-enum,deadly,web-paramminer,service-enum + Enable modules by flag. Choices: active,affiliates,aggressive,baddns,cloud-enum,code-enum,deadly,email-enum,iis-shortnames,passive,portscan,report,safe,service-enum,slow,social-enum,subdomain-enum,subdomain-hijack,web-basic,web-paramminer,web-screenshots,web-thorough -lf, --list-flags List available flags. -rf FLAG [FLAG ...], --require-flags FLAG [FLAG ...] Only enable modules with these flags (e.g. -rf passive) @@ -92,16 +93,19 @@ Scan: -s, --silent Be quiet --force Run scan even in the case of condition violations or failed module setups -y, --yes Skip scan confirmation prompt + --fast-mode Scan only the provided targets as fast as possible, with no extra discovery --dry-run Abort before executing scan --current-preset Show the current preset in YAML format --current-preset-full Show the current preset in its full form, including defaults Output: + -om MODULE [MODULE ...], --output-modules MODULE [MODULE ...] + Output module(s). Choices: asset_inventory,csv,discord,emails,http,json,mysql,neo4j,nmap_xml,postgres,python,slack,splunk,sqlite,stdout,subdomains,teams,txt,web_report,websocket + -lo, --list-output-modules + List available output modules -o DIR, --output-dir DIR Directory to output scan results - -om MODULE [MODULE ...], --output-modules MODULE [MODULE ...] - Output module(s). Choices: asset_inventory,discord,python,slack,http,json,web_report,teams,subdomains,emails,websocket,sqlite,txt,csv,stdout,neo4j,splunk --json, -j Output scan data in JSON format --brief, -br Output only the data itself --event-types EVENT_TYPES [EVENT_TYPES ...] @@ -118,6 +122,7 @@ Module dependencies: Misc: --version show BBOT version and exit + --proxy HTTP_PROXY Use this proxy for all HTTP requests -H CUSTOM_HEADERS [CUSTOM_HEADERS ...], --custom-headers CUSTOM_HEADERS [CUSTOM_HEADERS ...] List of custom headers as key value pairs (header=value). --custom-yara-rules CUSTOM_YARA_RULES, -cy CUSTOM_YARA_RULES @@ -146,6 +151,9 @@ EXAMPLES List modules: bbot -l + List output modules: + bbot -lo + List presets: bbot -lp diff --git a/docs/scanning/configuration.md b/docs/scanning/configuration.md index 51f9cc3f05..5808b149c5 100644 --- a/docs/scanning/configuration.md +++ b/docs/scanning/configuration.md @@ -73,6 +73,9 @@ folder_blobs: false ### SCOPE ### scope: + # strict scope means only exact DNS names are considered in-scope + # subdomains are not included unless they are explicitly provided in the target list + strict: false # Filter by scope distance which events are displayed in the output # 0 == show only in-scope events (affiliates are always shown) # 1 == show all events up to distance-1 (1 hop from target) @@ -130,7 +133,7 @@ dns: web: # HTTP proxy - http_proxy: + http_proxy: # Web user-agent user_agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.2151.97 # Set the maximum number of HTTP links that can be followed in a row (0 == no spidering allowed) @@ -168,6 +171,13 @@ engine: deps: ffuf: version: "2.1.0" + # How to handle installation of module dependencies + # Choices are: + # - abort_on_failure (default) - if a module dependency fails to install, abort the scan + # - retry_failed - try again to install failed dependencies + # - ignore_failed - run the scan regardless of what happens with dependency installation + # - disable - completely disable BBOT's dependency system (you are responsible for installing tools, pip packages, etc.) + behavior: abort_on_failure ### ADVANCED OPTIONS ### @@ -185,14 +195,6 @@ dnsresolve: True # Cloud provider tagging cloudcheck: True -# How to handle installation of module dependencies -# Choices are: -# - abort_on_failure (default) - if a module dependency fails to install, abort the scan -# - retry_failed - try again to install failed dependencies -# - ignore_failed - run the scan regardless of what happens with dependency installation -# - disable - completely disable BBOT's dependency system (you are responsible for installing tools, pip packages, etc.) -deps_behavior: abort_on_failure - # Strip querystring from URLs by default url_querystring_remove: True # When query string is retained, by default collapse parameter values down to a single value per parameter @@ -323,7 +325,7 @@ Many modules accept their own configuration options. These options have the abil | modules.nuclei.silent | bool | Don't display nuclei's banner or status messages | False | | modules.nuclei.tags | str | execute a subset of templates that contain the provided tags | | | modules.nuclei.templates | str | template or template directory paths to include in the scan | | -| modules.nuclei.version | str | nuclei version | 3.3.5 | +| modules.nuclei.version | str | nuclei version | 3.3.7 | | modules.oauth.try_all | bool | Check for OAUTH/IODC on every subdomain and URL. | False | | modules.paramminer_cookies.recycle_words | bool | Attempt to use words found during the scan on all other endpoints | False | | modules.paramminer_cookies.skip_boring_words | bool | Remove commonly uninteresting words from the wordlist | True | @@ -337,6 +339,8 @@ Many modules accept their own configuration options. These options have the abil | modules.portscan.adapter | str | Manually specify a network interface, such as "eth0" or "tun0". If not specified, the first network interface found with a default gateway will be used. | | | modules.portscan.adapter_ip | str | Send packets using this IP address. Not needed unless masscan's autodetection fails | | | modules.portscan.adapter_mac | str | Send packets using this as the source MAC address. Not needed unless masscan's autodetection fails | | +| modules.portscan.allowed_cdn_ports | NoneType | Comma-separated list of ports that are allowed to be scanned for CDNs | None | +| modules.portscan.cdn_tags | str | Comma-separated list of tags to skip, e.g. 'cdn,cloud' | cdn- | | modules.portscan.ping_first | bool | Only portscan hosts that reply to pings | False | | modules.portscan.ping_only | bool | Ping sweep only, no portscan | False | | modules.portscan.ports | str | Ports to scan | | @@ -378,8 +382,7 @@ Many modules accept their own configuration options. These options have the abil | modules.builtwith.api_key | str | Builtwith API key | | | modules.builtwith.redirects | bool | Also look up inbound and outbound redirects | True | | modules.c99.api_key | str | c99.nl API key | | -| modules.censys.api_id | str | Censys.io API ID | | -| modules.censys.api_secret | str | Censys.io API Secret | | +| modules.censys.api_key | str | Censys.io API Key in the format of 'key:secret' | | | modules.censys.max_pages | int | Maximum number of pages to fetch (100 results per page) | 5 | | modules.chaos.api_key | str | Chaos API key | | | modules.credshed.credshed_url | str | URL of credshed server | | @@ -394,6 +397,10 @@ Many modules accept their own configuration options. These options have the abil | modules.dnscaa.emails | bool | emit EMAIL_ADDRESS events | True | | modules.dnscaa.in_scope_only | bool | Only check in-scope domains | True | | modules.dnscaa.urls | bool | emit URL_UNVERIFIED events | True | +| modules.dnstlsrpt.emit_emails | bool | Emit EMAIL_ADDRESS events | True | +| modules.dnstlsrpt.emit_raw_dns_records | bool | Emit RAW_DNS_RECORD events | False | +| modules.dnstlsrpt.emit_urls | bool | Emit URL_UNVERIFIED events | True | +| modules.dnstlsrpt.emit_vulnerabilities | bool | Emit VULNERABILITY events | True | | modules.docker_pull.all_tags | bool | Download all tags from each registry (Default False) | False | | modules.docker_pull.output_folder | str | Folder to download docker repositories to | | | modules.extractous.extensions | list | File extensions to parse | ['bak', 'bash', 'bashrc', 'conf', 'cfg', 'crt', 'csv', 'db', 'sqlite', 'doc', 'docx', 'ica', 'indd', 'ini', 'key', 'pub', 'log', 'markdown', 'md', 'odg', 'odp', 'ods', 'odt', 'pdf', 'pem', 'pps', 'ppsx', 'ppt', 'pptx', 'ps1', 'rdp', 'sh', 'sql', 'swp', 'sxw', 'txt', 'vbs', 'wpd', 'xls', 'xlsx', 'xml', 'yml', 'yaml'] | @@ -415,8 +422,7 @@ Many modules accept their own configuration options. These options have the abil | modules.ipstack.api_key | str | IPStack GeoIP API Key | | | modules.jadx.threads | int | Maximum jadx threads for extracting apk's, default: 4 | 4 | | modules.leakix.api_key | str | LeakIX API Key | | -| modules.passivetotal.api_key | str | RiskIQ API Key | | -| modules.passivetotal.username | str | RiskIQ Username | | +| modules.passivetotal.api_key | str | PassiveTotal API Key in the format of 'username:api_key' | | | modules.pgp.search_urls | list | PGP key servers to search |` ['https://keyserver.ubuntu.com/pks/lookup?fingerprint=on&op=vindex&search=', 'http://the.earth.li:11371/pks/lookup?fingerprint=on&op=vindex&search=', 'https://pgpkeys.eu/pks/lookup?search=&op=index', 'https://pgp.mit.edu/pks/lookup?search=&op=index'] `| | modules.postman_download.api_key | str | Postman API Key | | | modules.postman_download.output_folder | str | Folder to download postman workspaces to | | @@ -430,7 +436,7 @@ Many modules accept their own configuration options. These options have the abil | modules.trufflehog.config | str | File path or URL to YAML trufflehog config | | | modules.trufflehog.deleted_forks | bool | Scan for deleted github forks. WARNING: This is SLOW. For a smaller repository, this process can take 20 minutes. For a larger repository, it could take hours. | False | | modules.trufflehog.only_verified | bool | Only report credentials that have been verified | True | -| modules.trufflehog.version | str | trufflehog version | 3.83.7 | +| modules.trufflehog.version | str | trufflehog version | 3.86.1 | | modules.urlscan.urls | bool | Emit URLs in addition to DNS_NAMEs | False | | modules.virustotal.api_key | str | VirusTotal API Key | | | modules.wayback.garbage_threshold | int | Dedupe similar urls if they are in a group of this size or higher (lower values == less garbage data) | 10 | @@ -456,9 +462,19 @@ Many modules accept their own configuration options. These options have the abil | modules.http.username | str | Username (basic auth) | | | modules.json.output_file | str | Output to file | | | modules.json.siem_friendly | bool | Output JSON in a SIEM-friendly format for ingestion into Elastic, Splunk, etc. | False | +| modules.mysql.database | str | The database name to connect to | bbot | +| modules.mysql.host | str | The server running MySQL | localhost | +| modules.mysql.password | str | The password to connect to MySQL | bbotislife | +| modules.mysql.port | int | The port to connect to MySQL | 3306 | +| modules.mysql.username | str | The username to connect to MySQL | root | | modules.neo4j.password | str | Neo4j password | bbotislife | | modules.neo4j.uri | str | Neo4j server + port | bolt://localhost:7687 | | modules.neo4j.username | str | Neo4j username | neo4j | +| modules.postgres.database | str | The database name to connect to | bbot | +| modules.postgres.host | str | The server running Postgres | localhost | +| modules.postgres.password | str | The password to connect to Postgres | bbotislife | +| modules.postgres.port | int | The port to connect to Postgres | 5432 | +| modules.postgres.username | str | The username to connect to Postgres | postgres | | modules.slack.event_types | list | Types of events to send | ['VULNERABILITY', 'FINDING'] | | modules.slack.min_severity | str | Only allow VULNERABILITY events of this severity or higher | LOW | | modules.slack.webhook_url | str | Discord webhook URL | | @@ -487,6 +503,7 @@ Many modules accept their own configuration options. These options have the abil | modules.excavate.custom_yara_rules | str | Include custom Yara rules | | | modules.excavate.retain_querystring | bool | Keep the querystring intact on emitted WEB_PARAMETERS | False | | modules.excavate.yara_max_match_data | int | Sets the maximum amount of text that can extracted from a YARA regex | 2000 | +| modules.speculate.essential_only | bool | Only enable essential speculate features (no extra discovery) | False | | modules.speculate.max_hosts | int | Max number of IP_RANGE hosts to convert into IP_ADDRESS events | 65536 | | modules.speculate.ports | str | The set of ports to speculate on | 80,443 | diff --git a/docs/scanning/events.md b/docs/scanning/events.md index 48a98515a2..f512ac770d 100644 --- a/docs/scanning/events.md +++ b/docs/scanning/events.md @@ -104,41 +104,41 @@ Below is a full list of event types along with which modules produce/consume the ## List of Event Types -| Event Type | # Consuming Modules | # Producing Modules | Consuming Modules | Producing Modules | -|---------------------|-----------------------|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| * | 16 | 0 | affiliates, cloudcheck, csv, discord, dnsresolve, http, json, neo4j, python, slack, splunk, sqlite, stdout, teams, txt, websocket | | -| ASN | 0 | 1 | | asn | -| AZURE_TENANT | 1 | 0 | speculate | | -| CODE_REPOSITORY | 6 | 6 | docker_pull, git_clone, github_workflows, google_playstore, postman_download, trufflehog | code_repository, dockerhub, github_codesearch, github_org, gitlab, postman | -| DNS_NAME | 59 | 43 | anubisdb, asset_inventory, azure_realm, azure_tenant, baddns, baddns_zone, bevigil, binaryedge, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_firebase, bucket_google, bufferoverrun, builtwith, c99, censys, certspotter, chaos, columbus, credshed, crt, dehashed, digitorus, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, emailformat, fullhunt, github_codesearch, hackertarget, hunterio, internetdb, leakix, myssl, oauth, otx, passivetotal, pgp, portscan, rapiddns, securitytrails, securitytxt, shodan_dns, sitedossier, skymem, speculate, subdomaincenter, subdomainradar, subdomains, trickest, urlscan, viewdns, virustotal, wayback, zoomeye | anubisdb, azure_tenant, bevigil, binaryedge, bufferoverrun, builtwith, c99, censys, certspotter, chaos, columbus, crt, digitorus, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, fullhunt, hackertarget, hunterio, internetdb, leakix, myssl, ntlm, oauth, otx, passivetotal, rapiddns, securitytrails, shodan_dns, sitedossier, speculate, sslcert, subdomaincenter, subdomainradar, trickest, urlscan, vhost, viewdns, virustotal, wayback, zoomeye | -| DNS_NAME_UNRESOLVED | 3 | 0 | baddns, speculate, subdomains | | -| EMAIL_ADDRESS | 1 | 9 | emails | credshed, dehashed, dnscaa, emailformat, hunterio, pgp, securitytxt, skymem, sslcert | -| FILESYSTEM | 3 | 7 | extractous, jadx, trufflehog | apkpure, docker_pull, filedownload, git_clone, github_workflows, jadx, postman_download | -| FINDING | 2 | 29 | asset_inventory, web_report | ajaxpro, baddns, baddns_direct, baddns_zone, badsecrets, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_firebase, bucket_google, bypass403, dastardly, git, gitlab, host_header, hunt, internetdb, newsletters, ntlm, nuclei, paramminer_cookies, paramminer_getparams, secretsdb, smuggler, speculate, telerik, trufflehog, url_manipulation, wpscan | -| GEOLOCATION | 0 | 2 | | ip2location, ipstack | -| HASHED_PASSWORD | 0 | 2 | | credshed, dehashed | -| HTTP_RESPONSE | 19 | 1 | ajaxpro, asset_inventory, badsecrets, dastardly, dotnetnuke, excavate, filedownload, gitlab, host_header, newsletters, ntlm, paramminer_cookies, paramminer_getparams, paramminer_headers, secretsdb, speculate, telerik, wappalyzer, wpscan | httpx | -| IP_ADDRESS | 8 | 3 | asn, asset_inventory, internetdb, ip2location, ipneighbor, ipstack, portscan, speculate | asset_inventory, ipneighbor, speculate | -| IP_RANGE | 2 | 0 | portscan, speculate | | -| MOBILE_APP | 1 | 1 | apkpure | google_playstore | -| OPEN_TCP_PORT | 4 | 4 | asset_inventory, fingerprintx, httpx, sslcert | asset_inventory, internetdb, portscan, speculate | -| ORG_STUB | 4 | 1 | dockerhub, github_org, google_playstore, postman | speculate | -| PASSWORD | 0 | 2 | | credshed, dehashed | -| PROTOCOL | 0 | 1 | | fingerprintx | -| RAW_DNS_RECORD | 0 | 1 | | dnsbimi | -| RAW_TEXT | 1 | 1 | excavate | extractous | -| SOCIAL | 6 | 3 | dockerhub, github_org, gitlab, gowitness, postman, speculate | dockerhub, gitlab, social | -| STORAGE_BUCKET | 8 | 5 | baddns_direct, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, speculate | bucket_amazon, bucket_azure, bucket_digitalocean, bucket_firebase, bucket_google | -| TECHNOLOGY | 4 | 8 | asset_inventory, gitlab, web_report, wpscan | badsecrets, dotnetnuke, gitlab, gowitness, internetdb, nuclei, wappalyzer, wpscan | -| URL | 20 | 2 | ajaxpro, asset_inventory, baddns_direct, bypass403, ffuf, generic_ssrf, git, gowitness, httpx, iis_shortnames, ntlm, nuclei, robots, smuggler, speculate, telerik, url_manipulation, vhost, wafw00f, web_report | gowitness, httpx | -| URL_HINT | 1 | 1 | ffuf_shortnames | iis_shortnames | -| URL_UNVERIFIED | 6 | 17 | code_repository, filedownload, httpx, oauth, social, speculate | azure_realm, bevigil, bucket_file_enum, dnsbimi, dnscaa, dockerhub, excavate, ffuf, ffuf_shortnames, github_codesearch, gowitness, hunterio, robots, securitytxt, urlscan, wayback, wpscan | -| USERNAME | 1 | 2 | speculate | credshed, dehashed | -| VHOST | 1 | 1 | web_report | vhost | -| VULNERABILITY | 2 | 13 | asset_inventory, web_report | ajaxpro, baddns, baddns_direct, baddns_zone, badsecrets, dastardly, dotnetnuke, generic_ssrf, internetdb, nuclei, telerik, trufflehog, wpscan | -| WAF | 1 | 1 | asset_inventory | wafw00f | -| WEBSCREENSHOT | 0 | 1 | | gowitness | -| WEB_PARAMETER | 4 | 4 | hunt, paramminer_cookies, paramminer_getparams, paramminer_headers | excavate, paramminer_cookies, paramminer_getparams, paramminer_headers | +| Event Type | # Consuming Modules | # Producing Modules | Consuming Modules | Producing Modules | +|---------------------|-----------------------|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| * | 18 | 0 | affiliates, cloudcheck, csv, discord, dnsresolve, http, json, mysql, neo4j, postgres, python, slack, splunk, sqlite, stdout, teams, txt, websocket | | +| ASN | 0 | 1 | | asn | +| AZURE_TENANT | 1 | 0 | speculate | | +| CODE_REPOSITORY | 6 | 6 | docker_pull, git_clone, github_workflows, google_playstore, postman_download, trufflehog | code_repository, dockerhub, github_codesearch, github_org, gitlab, postman | +| DNS_NAME | 61 | 44 | anubisdb, asset_inventory, azure_realm, azure_tenant, baddns, baddns_zone, bevigil, binaryedge, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_firebase, bucket_google, bufferoverrun, builtwith, c99, censys, certspotter, chaos, columbus, credshed, crt, dehashed, digitorus, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, emailformat, fullhunt, github_codesearch, hackertarget, hunterio, internetdb, leakix, myssl, nmap_xml, oauth, otx, passivetotal, pgp, portscan, rapiddns, securitytrails, securitytxt, shodan_dns, sitedossier, skymem, speculate, subdomaincenter, subdomainradar, subdomains, trickest, urlscan, viewdns, virustotal, wayback, zoomeye | anubisdb, azure_tenant, bevigil, binaryedge, bufferoverrun, builtwith, c99, censys, certspotter, chaos, columbus, crt, digitorus, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnsresolve, fullhunt, hackertarget, hunterio, internetdb, leakix, myssl, ntlm, oauth, otx, passivetotal, rapiddns, securitytrails, shodan_dns, sitedossier, speculate, sslcert, subdomaincenter, subdomainradar, trickest, urlscan, vhost, viewdns, virustotal, wayback, zoomeye | +| DNS_NAME_UNRESOLVED | 3 | 0 | baddns, speculate, subdomains | | +| EMAIL_ADDRESS | 1 | 10 | emails | credshed, dehashed, dnscaa, dnstlsrpt, emailformat, hunterio, pgp, securitytxt, skymem, sslcert | +| FILESYSTEM | 3 | 7 | extractous, jadx, trufflehog | apkpure, docker_pull, filedownload, git_clone, github_workflows, jadx, postman_download | +| FINDING | 2 | 29 | asset_inventory, web_report | ajaxpro, baddns, baddns_direct, baddns_zone, badsecrets, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_firebase, bucket_google, bypass403, dastardly, git, gitlab, host_header, hunt, internetdb, newsletters, ntlm, nuclei, paramminer_cookies, paramminer_getparams, secretsdb, smuggler, speculate, telerik, trufflehog, url_manipulation, wpscan | +| GEOLOCATION | 0 | 2 | | ip2location, ipstack | +| HASHED_PASSWORD | 0 | 2 | | credshed, dehashed | +| HTTP_RESPONSE | 20 | 1 | ajaxpro, asset_inventory, badsecrets, dastardly, dotnetnuke, excavate, filedownload, gitlab, host_header, newsletters, nmap_xml, ntlm, paramminer_cookies, paramminer_getparams, paramminer_headers, secretsdb, speculate, telerik, wappalyzer, wpscan | httpx | +| IP_ADDRESS | 9 | 4 | asn, asset_inventory, internetdb, ip2location, ipneighbor, ipstack, nmap_xml, portscan, speculate | asset_inventory, dnsresolve, ipneighbor, speculate | +| IP_RANGE | 2 | 0 | portscan, speculate | | +| MOBILE_APP | 1 | 1 | apkpure | google_playstore | +| OPEN_TCP_PORT | 5 | 4 | asset_inventory, fingerprintx, httpx, nmap_xml, sslcert | asset_inventory, internetdb, portscan, speculate | +| ORG_STUB | 4 | 1 | dockerhub, github_org, google_playstore, postman | speculate | +| PASSWORD | 0 | 2 | | credshed, dehashed | +| PROTOCOL | 1 | 1 | nmap_xml | fingerprintx | +| RAW_DNS_RECORD | 0 | 3 | | dnsbimi, dnsresolve, dnstlsrpt | +| RAW_TEXT | 1 | 1 | excavate | extractous | +| SOCIAL | 6 | 3 | dockerhub, github_org, gitlab, gowitness, postman, speculate | dockerhub, gitlab, social | +| STORAGE_BUCKET | 8 | 5 | baddns_direct, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, speculate | bucket_amazon, bucket_azure, bucket_digitalocean, bucket_firebase, bucket_google | +| TECHNOLOGY | 4 | 8 | asset_inventory, gitlab, web_report, wpscan | badsecrets, dotnetnuke, gitlab, gowitness, internetdb, nuclei, wappalyzer, wpscan | +| URL | 20 | 2 | ajaxpro, asset_inventory, baddns_direct, bypass403, ffuf, generic_ssrf, git, gowitness, httpx, iis_shortnames, ntlm, nuclei, robots, smuggler, speculate, telerik, url_manipulation, vhost, wafw00f, web_report | gowitness, httpx | +| URL_HINT | 1 | 1 | ffuf_shortnames | iis_shortnames | +| URL_UNVERIFIED | 6 | 18 | code_repository, filedownload, httpx, oauth, social, speculate | azure_realm, bevigil, bucket_file_enum, dnsbimi, dnscaa, dnstlsrpt, dockerhub, excavate, ffuf, ffuf_shortnames, github_codesearch, gowitness, hunterio, robots, securitytxt, urlscan, wayback, wpscan | +| USERNAME | 1 | 2 | speculate | credshed, dehashed | +| VHOST | 1 | 1 | web_report | vhost | +| VULNERABILITY | 2 | 13 | asset_inventory, web_report | ajaxpro, baddns, baddns_direct, baddns_zone, badsecrets, dastardly, dotnetnuke, generic_ssrf, internetdb, nuclei, telerik, trufflehog, wpscan | +| WAF | 1 | 1 | asset_inventory | wafw00f | +| WEBSCREENSHOT | 0 | 1 | | gowitness | +| WEB_PARAMETER | 4 | 4 | hunt, paramminer_cookies, paramminer_getparams, paramminer_headers | excavate, paramminer_cookies, paramminer_getparams, paramminer_headers | ## Findings Vs. Vulnerabilities diff --git a/docs/scanning/index.md b/docs/scanning/index.md index a7359730a2..b947319c45 100644 --- a/docs/scanning/index.md +++ b/docs/scanning/index.md @@ -22,6 +22,11 @@ Targets declare what's in-scope, and seed a scan with initial data. BBOT accepts - `IP_RANGE` (`1.2.3.0/24`) - `OPEN_TCP_PORT` (`192.168.0.1:80`) - `URL` (`https://www.evilcorp.com`) +- `EMAIL_ADDRESS` (`bob@evilcorp.com`) +- `ORG_STUB` (`ORG:evilcorp`) +- `USER_STUB` (`USER:bobsmith`) +- `FILESYSTEM` (`FILESYSTEM:/tmp/asdf`) +- `MOBILE_APP` (`MOBILE_APP:https://play.google.com/store/apps/details?id=com.evilcorp.app`) Note that BBOT only discriminates down to the host level. This means, for example, if you specify a URL `https://www.evilcorp.com` as the target, the scan will be *seeded* with that URL, but the scope of the scan will be the entire host, `www.evilcorp.com`. Other ports/URLs on that same host may also be scanned. @@ -107,31 +112,31 @@ A single module can have multiple flags. For example, the `securitytrails` modul ### List of Flags -| Flag | # Modules | Description | Modules | -|------------------|-------------|----------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| safe | 91 | Non-intrusive, safe to run | affiliates, aggregate, ajaxpro, anubisdb, apkpure, asn, azure_realm, azure_tenant, baddns, baddns_direct, baddns_zone, badsecrets, bevigil, binaryedge, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bufferoverrun, builtwith, c99, censys, certspotter, chaos, code_repository, columbus, credshed, crt, dehashed, digitorus, dnsbimi, dnscaa, dnscommonsrv, dnsdumpster, docker_pull, dockerhub, emailformat, extractous, filedownload, fingerprintx, fullhunt, git, git_clone, github_codesearch, github_org, github_workflows, gitlab, google_playstore, gowitness, hackertarget, httpx, hunt, hunterio, iis_shortnames, internetdb, ip2location, ipstack, jadx, leakix, myssl, newsletters, ntlm, oauth, otx, passivetotal, pgp, portscan, postman, postman_download, rapiddns, robots, secretsdb, securitytrails, securitytxt, shodan_dns, sitedossier, skymem, social, sslcert, subdomaincenter, subdomainradar, trickest, trufflehog, urlscan, viewdns, virustotal, wappalyzer, wayback, zoomeye | -| passive | 66 | Never connects to target systems | affiliates, aggregate, anubisdb, apkpure, asn, azure_realm, azure_tenant, bevigil, binaryedge, bucket_file_enum, bufferoverrun, builtwith, c99, censys, certspotter, chaos, code_repository, columbus, credshed, crt, dehashed, digitorus, dnsbimi, dnscaa, dnsdumpster, docker_pull, dockerhub, emailformat, excavate, extractous, fullhunt, git_clone, github_codesearch, github_org, github_workflows, google_playstore, hackertarget, hunterio, internetdb, ip2location, ipneighbor, ipstack, jadx, leakix, myssl, otx, passivetotal, pgp, postman, postman_download, rapiddns, securitytrails, shodan_dns, sitedossier, skymem, social, speculate, subdomaincenter, subdomainradar, trickest, trufflehog, urlscan, viewdns, virustotal, wayback, zoomeye | -| subdomain-enum | 52 | Enumerates subdomains | anubisdb, asn, azure_realm, azure_tenant, baddns_direct, baddns_zone, bevigil, binaryedge, bufferoverrun, builtwith, c99, censys, certspotter, chaos, columbus, crt, digitorus, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, fullhunt, github_codesearch, github_org, hackertarget, httpx, hunterio, internetdb, ipneighbor, leakix, myssl, oauth, otx, passivetotal, postman, postman_download, rapiddns, securitytrails, securitytxt, shodan_dns, sitedossier, sslcert, subdomaincenter, subdomainradar, subdomains, trickest, urlscan, virustotal, wayback, zoomeye | -| active | 47 | Makes active connections to target systems | ajaxpro, baddns, baddns_direct, baddns_zone, badsecrets, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_firebase, bucket_google, bypass403, dastardly, dnsbrute, dnsbrute_mutations, dnscommonsrv, dotnetnuke, ffuf, ffuf_shortnames, filedownload, fingerprintx, generic_ssrf, git, gitlab, gowitness, host_header, httpx, hunt, iis_shortnames, newsletters, ntlm, nuclei, oauth, paramminer_cookies, paramminer_getparams, paramminer_headers, portscan, robots, secretsdb, securitytxt, smuggler, sslcert, telerik, url_manipulation, vhost, wafw00f, wappalyzer, wpscan | -| aggressive | 20 | Generates a large amount of network traffic | bypass403, dastardly, dnsbrute, dnsbrute_mutations, dotnetnuke, ffuf, ffuf_shortnames, generic_ssrf, host_header, ipneighbor, nuclei, paramminer_cookies, paramminer_getparams, paramminer_headers, smuggler, telerik, url_manipulation, vhost, wafw00f, wpscan | -| web-basic | 18 | Basic, non-intrusive web scan functionality | azure_realm, baddns, badsecrets, bucket_amazon, bucket_azure, bucket_firebase, bucket_google, filedownload, git, httpx, iis_shortnames, ntlm, oauth, robots, secretsdb, securitytxt, sslcert, wappalyzer | -| cloud-enum | 15 | Enumerates cloud resources | azure_realm, azure_tenant, baddns, baddns_direct, baddns_zone, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, dnsbimi, httpx, oauth, securitytxt | -| code-enum | 14 | Find public code repositories and search them for secrets etc. | apkpure, code_repository, docker_pull, dockerhub, git, git_clone, github_codesearch, github_org, github_workflows, gitlab, google_playstore, postman, postman_download, trufflehog | -| web-thorough | 12 | More advanced web scanning functionality | ajaxpro, bucket_digitalocean, bypass403, dastardly, dotnetnuke, ffuf_shortnames, generic_ssrf, host_header, hunt, smuggler, telerik, url_manipulation | -| slow | 11 | May take a long time to complete | bucket_digitalocean, dastardly, dnsbrute_mutations, docker_pull, fingerprintx, git_clone, paramminer_cookies, paramminer_getparams, paramminer_headers, smuggler, vhost | -| affiliates | 9 | Discovers affiliated hostnames/domains | affiliates, azure_realm, azure_tenant, builtwith, oauth, sslcert, trickest, viewdns, zoomeye | -| email-enum | 8 | Enumerates email addresses | dehashed, dnscaa, emailformat, emails, hunterio, pgp, skymem, sslcert | -| deadly | 4 | Highly aggressive | dastardly, ffuf, nuclei, vhost | -| baddns | 3 | Runs all modules from the DNS auditing tool BadDNS | baddns, baddns_direct, baddns_zone | -| web-paramminer | 3 | Discovers HTTP parameters through brute-force | paramminer_cookies, paramminer_getparams, paramminer_headers | -| iis-shortnames | 2 | Scans for IIS Shortname vulnerability | ffuf_shortnames, iis_shortnames | -| portscan | 2 | Discovers open ports | internetdb, portscan | -| report | 2 | Generates a report at the end of the scan | affiliates, asn | -| social-enum | 2 | Enumerates social media | httpx, social | -| service-enum | 1 | Identifies protocols running on open ports | fingerprintx | -| subdomain-hijack | 1 | Detects hijackable subdomains | baddns | -| web-screenshots | 1 | Takes screenshots of web pages | gowitness | - +| Flag | # Modules | Description | Modules | +|------------------|-------------|----------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| safe | 92 | Non-intrusive, safe to run | affiliates, aggregate, ajaxpro, anubisdb, apkpure, asn, azure_realm, azure_tenant, baddns, baddns_direct, baddns_zone, badsecrets, bevigil, binaryedge, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bufferoverrun, builtwith, c99, censys, certspotter, chaos, code_repository, columbus, credshed, crt, dehashed, digitorus, dnsbimi, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, docker_pull, dockerhub, emailformat, extractous, filedownload, fingerprintx, fullhunt, git, git_clone, github_codesearch, github_org, github_workflows, gitlab, google_playstore, gowitness, hackertarget, httpx, hunt, hunterio, iis_shortnames, internetdb, ip2location, ipstack, jadx, leakix, myssl, newsletters, ntlm, oauth, otx, passivetotal, pgp, portscan, postman, postman_download, rapiddns, robots, secretsdb, securitytrails, securitytxt, shodan_dns, sitedossier, skymem, social, sslcert, subdomaincenter, subdomainradar, trickest, trufflehog, urlscan, viewdns, virustotal, wappalyzer, wayback, zoomeye | +| passive | 67 | Never connects to target systems | affiliates, aggregate, anubisdb, apkpure, asn, azure_realm, azure_tenant, bevigil, binaryedge, bucket_file_enum, bufferoverrun, builtwith, c99, censys, certspotter, chaos, code_repository, columbus, credshed, crt, dehashed, digitorus, dnsbimi, dnscaa, dnsdumpster, dnstlsrpt, docker_pull, dockerhub, emailformat, excavate, extractous, fullhunt, git_clone, github_codesearch, github_org, github_workflows, google_playstore, hackertarget, hunterio, internetdb, ip2location, ipneighbor, ipstack, jadx, leakix, myssl, otx, passivetotal, pgp, postman, postman_download, rapiddns, securitytrails, shodan_dns, sitedossier, skymem, social, speculate, subdomaincenter, subdomainradar, trickest, trufflehog, urlscan, viewdns, virustotal, wayback, zoomeye | +| subdomain-enum | 53 | Enumerates subdomains | anubisdb, asn, azure_realm, azure_tenant, baddns_direct, baddns_zone, bevigil, binaryedge, bufferoverrun, builtwith, c99, censys, certspotter, chaos, columbus, crt, digitorus, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, fullhunt, github_codesearch, github_org, hackertarget, httpx, hunterio, internetdb, ipneighbor, leakix, myssl, oauth, otx, passivetotal, postman, postman_download, rapiddns, securitytrails, securitytxt, shodan_dns, sitedossier, sslcert, subdomaincenter, subdomainradar, subdomains, trickest, urlscan, virustotal, wayback, zoomeye | +| active | 47 | Makes active connections to target systems | ajaxpro, baddns, baddns_direct, baddns_zone, badsecrets, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_firebase, bucket_google, bypass403, dastardly, dnsbrute, dnsbrute_mutations, dnscommonsrv, dotnetnuke, ffuf, ffuf_shortnames, filedownload, fingerprintx, generic_ssrf, git, gitlab, gowitness, host_header, httpx, hunt, iis_shortnames, newsletters, ntlm, nuclei, oauth, paramminer_cookies, paramminer_getparams, paramminer_headers, portscan, robots, secretsdb, securitytxt, smuggler, sslcert, telerik, url_manipulation, vhost, wafw00f, wappalyzer, wpscan | +| aggressive | 20 | Generates a large amount of network traffic | bypass403, dastardly, dnsbrute, dnsbrute_mutations, dotnetnuke, ffuf, ffuf_shortnames, generic_ssrf, host_header, ipneighbor, nuclei, paramminer_cookies, paramminer_getparams, paramminer_headers, smuggler, telerik, url_manipulation, vhost, wafw00f, wpscan | +| web-basic | 18 | Basic, non-intrusive web scan functionality | azure_realm, baddns, badsecrets, bucket_amazon, bucket_azure, bucket_firebase, bucket_google, filedownload, git, httpx, iis_shortnames, ntlm, oauth, robots, secretsdb, securitytxt, sslcert, wappalyzer | +| cloud-enum | 16 | Enumerates cloud resources | azure_realm, azure_tenant, baddns, baddns_direct, baddns_zone, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, dnsbimi, dnstlsrpt, httpx, oauth, securitytxt | +| code-enum | 14 | Find public code repositories and search them for secrets etc. | apkpure, code_repository, docker_pull, dockerhub, git, git_clone, github_codesearch, github_org, github_workflows, gitlab, google_playstore, postman, postman_download, trufflehog | +| web-thorough | 12 | More advanced web scanning functionality | ajaxpro, bucket_digitalocean, bypass403, dastardly, dotnetnuke, ffuf_shortnames, generic_ssrf, host_header, hunt, smuggler, telerik, url_manipulation | +| slow | 11 | May take a long time to complete | bucket_digitalocean, dastardly, dnsbrute_mutations, docker_pull, fingerprintx, git_clone, paramminer_cookies, paramminer_getparams, paramminer_headers, smuggler, vhost | +| affiliates | 9 | Discovers affiliated hostnames/domains | affiliates, azure_realm, azure_tenant, builtwith, oauth, sslcert, trickest, viewdns, zoomeye | +| email-enum | 9 | Enumerates email addresses | dehashed, dnscaa, dnstlsrpt, emailformat, emails, hunterio, pgp, skymem, sslcert | +| deadly | 4 | Highly aggressive | dastardly, ffuf, nuclei, vhost | +| baddns | 3 | Runs all modules from the DNS auditing tool BadDNS | baddns, baddns_direct, baddns_zone | +| web-paramminer | 3 | Discovers HTTP parameters through brute-force | paramminer_cookies, paramminer_getparams, paramminer_headers | +| iis-shortnames | 2 | Scans for IIS Shortname vulnerability | ffuf_shortnames, iis_shortnames | +| portscan | 2 | Discovers open ports | internetdb, portscan | +| report | 2 | Generates a report at the end of the scan | affiliates, asn | +| social-enum | 2 | Enumerates social media | httpx, social | +| service-enum | 1 | Identifies protocols running on open ports | fingerprintx | +| subdomain-hijack | 1 | Detects hijackable subdomains | baddns | +| web-screenshots | 1 | Takes screenshots of web pages | gowitness | + ## Dependencies @@ -178,6 +183,8 @@ Note that `--strict-scope` only applies to targets and whitelists, but not black BBOT allows precise control over scope with whitelists and blacklists. These both use the same syntax as `--target`, meaning they accept the same event types, and you can specify an unlimited number of them, via a file, the CLI, or both. +#### Whitelists + `--whitelist` enables you to override what's in scope. For example, if you want to run nuclei against `evilcorp.com`, but stay only inside their corporate IP range of `1.2.3.0/24`, you can accomplish this like so: ```bash @@ -185,6 +192,8 @@ BBOT allows precise control over scope with whitelists and blacklists. These bot bbot -t evilcorp.com --whitelist 1.2.3.0/24 -f subdomain-enum -m nmap nuclei --allow-deadly ``` +#### Blacklists + `--blacklist` takes ultimate precedence. Anything in the blacklist is completely excluded from the scan, even if it's in the whitelist. ```bash @@ -192,6 +201,49 @@ bbot -t evilcorp.com --whitelist 1.2.3.0/24 -f subdomain-enum -m nmap nuclei --a bbot -t evilcorp.com --blacklist internal.evilcorp.com -f subdomain-enum -m nmap nuclei --allow-deadly ``` +#### Blacklist by Regex + +Blacklists also accept regex patterns. These regexes are are checked against the full URL, including the host and path. + +To specify a regex, prefix the pattern with `RE:`. For example, to exclude all events containing "signout", you could do: + +```bash +bbot -t evilcorp.com --blacklist "RE:signout" +``` + +Note that this would blacklist both of the following events: + +- `[URL] http://evilcorp.com/signout.aspx` +- `[DNS_NAME] signout.evilcorp.com` + +If you only want to blacklist the URL, you could narrow the regex like so: + +```bash +bbot -t evilcorp.com --blacklist 'RE:signout\.aspx$' +``` + +Similar to targets and whitelists, blacklists can be specified in your preset. The `spider` preset makes use of this to prevent the spider from following logout links: + +```yaml title="spider.yml" +description: Recursive web spider + +modules: + - httpx + +blacklist: + # Prevent spider from invalidating sessions by logging out + - "RE:/.*(sign|log)[_-]?out" + +config: + web: + # how many links to follow in a row + spider_distance: 2 + # don't follow links whose directory depth is higher than 4 + spider_depth: 4 + # maximum number of links to follow per page + spider_links_per_page: 25 +``` + ## DNS Wildcards BBOT has robust wildcard detection built-in. It can reliably detect wildcard domains, and will tag them accordingly: diff --git a/docs/scanning/output.md b/docs/scanning/output.md index 7efdf48620..66d9b1c70c 100644 --- a/docs/scanning/output.md +++ b/docs/scanning/output.md @@ -90,10 +90,11 @@ mail.evilcorp.com BBOT supports output via webhooks to `discord`, `slack`, and `teams`. To use them, you must specify a webhook URL either in the config: -```yaml title="~/.bbot/config/bbot.yml" -modules: - discord: - webhook_url: https://discord.com/api/webhooks/1234/deadbeef +```yaml title="discord_preset.yml" +config: + modules: + discord: + webhook_url: https://discord.com/api/webhooks/1234/deadbeef ``` ...or on the command line: @@ -103,13 +104,14 @@ bbot -t evilcorp.com -om discord -c modules.discord.webhook_url=https://discord. By default, only `VULNERABILITY` and `FINDING` events are sent, but this can be customized by setting `event_types` in the config like so: -```yaml title="~/.bbot/config/bbot.yml" -modules: - discord: - event_types: - - VULNERABILITY - - FINDING - - STORAGE_BUCKET +```yaml title="discord_preset.yml" +config: + modules: + discord: + event_types: + - VULNERABILITY + - FINDING + - STORAGE_BUCKET ``` ...or on the command line: @@ -120,10 +122,11 @@ bbot -t evilcorp.com -om discord -c modules.discord.event_types=["STORAGE_BUCKET You can also filter on the severity of `VULNERABILITY` events by setting `min_severity`: -```yaml title="~/.bbot/config/bbot.yml" -modules: - discord: - min_severity: HIGH +```yaml title="discord_preset.yml" +config: + modules: + discord: + min_severity: HIGH ``` ### HTTP @@ -137,16 +140,42 @@ bbot -t evilcorp.com -om http -c modules.http.url=http://localhost:8000 You can customize the HTTP method if needed. Authentication is also supported: -```yaml title="~/.bbot/config/bbot.yml" -modules: - http: - url: https://localhost:8000 - method: PUT - # Authorization: Bearer - bearer: - # OR - username: bob - password: P@ssw0rd +```yaml title="http_preset.yml" +config: + modules: + http: + url: https://localhost:8000 + method: PUT + # Authorization: Bearer + bearer: + # OR + username: bob + password: P@ssw0rd +``` + +### Elasticsearch + +When outputting to Elastic, use the `http` output module with the following settings (replace `` with your desired index, e.g. `bbot`): + +```bash +# send scan results directly to elasticsearch +bbot -t evilcorp.com -om http -c \ + modules.http.url=http://localhost:8000//_doc \ + modules.http.siem_friendly=true \ + modules.http.username=elastic \ + modules.http.password=changeme +``` + +Alternatively, via a preset: + +```yaml title="elastic_preset.yml" +config: + modules: + http: + url: http://localhost:8000//_doc + siem_friendly: true + username: elastic + password: changeme ``` ### Splunk @@ -155,17 +184,18 @@ The `splunk` output module sends [events](events.md) in JSON format to a desired You can customize this output with the following config options: -```yaml title="~/.bbot/config/bbot.yml" -modules: - splunk: - # The full URL with the URI `/services/collector/event` - url: https://localhost:8088/services/collector/event - # Generated from splunk webui - hectoken: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - # Defaults to `main` if not set - index: my-specific-index - # Defaults to `bbot` if not set - source: /my/source.json +```yaml title="splunk_preset.yml" +config: + modules: + splunk: + # The full URL with the URI `/services/collector/event` + url: https://localhost:8088/services/collector/event + # Generated from splunk webui + hectoken: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + # Defaults to `main` if not set + index: my-specific-index + # Defaults to `bbot` if not set + source: /my/source.json ``` ### Asset Inventory @@ -187,6 +217,46 @@ The `sqlite` output module produces a SQLite database containing all events, sca bbot -t evilcorp.com -om sqlite -c modules.sqlite.database=/tmp/bbot.sqlite ``` +### Postgres + +The `postgres` output module allows you to ingest events, scans, and targets into a Postgres database. By default, it will connect to the server on `localhost` with a username of `postgres` and password of `bbotislife`. You can change this behavior in the config. + +```bash +# specifying an alternate database +bbot -t evilcorp.com -om postgres -c modules.postgres.database=custom_bbot_db +``` + +```yaml title="postgres_preset.yml" +config: + modules: + postgres: + host: psq.fsociety.local + database: custom_bbot_db + port: 5432 + username: postgres + password: bbotislife +``` + +### MySQL + +The `mysql` output module allows you to ingest events, scans, and targets into a MySQL database. By default, it will connect to the server on `localhost` with a username of `root` and password of `bbotislife`. You can change this behavior in the config. + +```bash +# specifying an alternate database +bbot -t evilcorp.com -om mysql -c modules.mysql.database=custom_bbot_db +``` + +```yaml title="mysql_preset.yml" +config: + modules: + mysql: + host: mysql.fsociety.local + database: custom_bbot_db + port: 3306 + username: root + password: bbotislife +``` + ### Subdomains The `subdomains` output module produces simple text file containing only in-scope and resolved subdomains: @@ -221,7 +291,7 @@ bbot -f subdomain-enum -t evilcorp.com -om neo4j ### Cypher Queries and Tips -Neo4j uses the Cypher Query Language for its graph query language. Cypher uses common clauses to craft relational queries and present the desired data in multiple formats. +Neo4j uses the Cypher Query Language for its graph query language. Cypher uses common clauses to craft relational queries and present the desired data in multiple formats. Cypher queries can be broken down into three required pieces; selection, filter, and presentation. The selection piece identifies what data that will be searched against - 90% of the time the "MATCH" clause will be enough but there are means to read from csv or json data files. In all of these examples the "MATCH" clause will be used. The filter piece helps to focus in on the required data and used the "WHERE" clause to accomplish this effort (most basic operators can be used). Finally, the presentation section identifies how the data should be presented back to the querier. While neo4j is a graph database, it can be used in a traditional table view. @@ -230,7 +300,7 @@ A simple query to grab every URL event with ".com" in the BBOT data field would In this query the following can be identified: - Within the MATCH statement "u" is a variable and can be any value needed by the user while the "URL" label is a direct relationship to the BBOT event type. -- The WHERE statement allows the query to filter on any of the BBOT event properties like data, tag, or even the label itself. +- The WHERE statement allows the query to filter on any of the BBOT event properties like data, tag, or even the label itself. - The RETURN statement is a general presentation of the whole URL event but this can be narrowed down to present any of the specific properties of the BBOT event (`RETURN u.data, u.tags`). The following are a few recommended queries to get started with: @@ -267,6 +337,6 @@ RETURN n.data, collect(distinct port) MATCH (n) DETACH DELETE n ``` -This is not an exhaustive list of clauses, filters, or other means to use cypher and should be considered a starting point. To build more advanced queries consider reading Neo4j's Cypher [documentation](https://neo4j.com/docs/cypher-manual/current/introduction/). +This is not an exhaustive list of clauses, filters, or other means to use cypher and should be considered a starting point. To build more advanced queries consider reading Neo4j's Cypher [documentation](https://neo4j.com/docs/cypher-manual/current/introduction/). -Additional note: these sample queries are dependent on the existence of the data in the target neo4j database. +Additional note: these sample queries are dependent on the existence of the data in the target neo4j database. diff --git a/docs/scanning/presets.md b/docs/scanning/presets.md index f68a62dc33..7fa8f8c93b 100644 --- a/docs/scanning/presets.md +++ b/docs/scanning/presets.md @@ -37,7 +37,7 @@ bbot -lp Enable them with `-p`: ```bash -# do a subdomain enumeration +# do a subdomain enumeration bbot -t evilcorp.com -p subdomain-enum # multiple presets - subdomain enumeration + web spider diff --git a/docs/scanning/presets_list.md b/docs/scanning/presets_list.md index bc06e5be02..11407fead5 100644 --- a/docs/scanning/presets_list.md +++ b/docs/scanning/presets_list.md @@ -42,7 +42,7 @@ Enumerate cloud resources such as storage buckets, etc. -Modules: [59]("`anubisdb`, `asn`, `azure_realm`, `azure_tenant`, `baddns_direct`, `baddns_zone`, `baddns`, `bevigil`, `binaryedge`, `bucket_amazon`, `bucket_azure`, `bucket_digitalocean`, `bucket_file_enum`, `bucket_firebase`, `bucket_google`, `bufferoverrun`, `builtwith`, `c99`, `censys`, `certspotter`, `chaos`, `columbus`, `crt`, `digitorus`, `dnsbimi`, `dnsbrute_mutations`, `dnsbrute`, `dnscaa`, `dnscommonsrv`, `dnsdumpster`, `fullhunt`, `github_codesearch`, `github_org`, `hackertarget`, `httpx`, `hunterio`, `internetdb`, `ipneighbor`, `leakix`, `myssl`, `oauth`, `otx`, `passivetotal`, `postman_download`, `postman`, `rapiddns`, `securitytrails`, `securitytxt`, `shodan_dns`, `sitedossier`, `social`, `sslcert`, `subdomaincenter`, `subdomainradar`, `trickest`, `urlscan`, `virustotal`, `wayback`, `zoomeye`") +Modules: [60]("`anubisdb`, `asn`, `azure_realm`, `azure_tenant`, `baddns_direct`, `baddns_zone`, `baddns`, `bevigil`, `binaryedge`, `bucket_amazon`, `bucket_azure`, `bucket_digitalocean`, `bucket_file_enum`, `bucket_firebase`, `bucket_google`, `bufferoverrun`, `builtwith`, `c99`, `censys`, `certspotter`, `chaos`, `columbus`, `crt`, `digitorus`, `dnsbimi`, `dnsbrute_mutations`, `dnsbrute`, `dnscaa`, `dnscommonsrv`, `dnsdumpster`, `dnstlsrpt`, `fullhunt`, `github_codesearch`, `github_org`, `hackertarget`, `httpx`, `hunterio`, `internetdb`, `ipneighbor`, `leakix`, `myssl`, `oauth`, `otx`, `passivetotal`, `postman_download`, `postman`, `rapiddns`, `securitytrails`, `securitytxt`, `shodan_dns`, `sitedossier`, `social`, `sslcert`, `subdomaincenter`, `subdomainradar`, `trickest`, `urlscan`, `virustotal`, `wayback`, `zoomeye`") ## **code-enum** @@ -163,7 +163,6 @@ Comprehensive scan for all IIS/.NET specific modules and module settings extensions: asp,aspx,ashx,asmx,ascx telerik: exploit_RAU_crypto: True - ``` Category: web @@ -187,7 +186,35 @@ Enumerate email addresses from APIs, web crawling, etc. -Modules: [7]("`dehashed`, `dnscaa`, `emailformat`, `hunterio`, `pgp`, `skymem`, `sslcert`") +Modules: [8]("`dehashed`, `dnscaa`, `dnstlsrpt`, `emailformat`, `hunterio`, `pgp`, `skymem`, `sslcert`") + +## **fast** + +Scan only the provided targets as fast as possible - no extra discovery + +??? note "`fast.yml`" + ```yaml title="~/.bbot/presets/fast.yml" + description: Scan only the provided targets as fast as possible - no extra discovery + + exclude_modules: + - excavate + + config: + # only scan the exact targets specified + scope: + strict: true + # speed up dns resolution by doing A/AAAA only - not MX/NS/SRV/etc + dns: + minimal: true + # essential speculation only + modules: + speculate: + essential_only: true + ``` + + + +Modules: [0]("") ## **iis-shortnames** @@ -235,13 +262,11 @@ Everything everywhere all at once modules: baddns: enable_references: True - - ``` -Modules: [86]("`anubisdb`, `apkpure`, `asn`, `azure_realm`, `azure_tenant`, `baddns_direct`, `baddns_zone`, `baddns`, `badsecrets`, `bevigil`, `binaryedge`, `bucket_amazon`, `bucket_azure`, `bucket_digitalocean`, `bucket_file_enum`, `bucket_firebase`, `bucket_google`, `bufferoverrun`, `builtwith`, `c99`, `censys`, `certspotter`, `chaos`, `code_repository`, `columbus`, `crt`, `dehashed`, `digitorus`, `dnsbimi`, `dnsbrute_mutations`, `dnsbrute`, `dnscaa`, `dnscommonsrv`, `dnsdumpster`, `docker_pull`, `dockerhub`, `emailformat`, `ffuf_shortnames`, `ffuf`, `filedownload`, `fullhunt`, `git_clone`, `git`, `github_codesearch`, `github_org`, `github_workflows`, `gitlab`, `google_playstore`, `gowitness`, `hackertarget`, `httpx`, `hunterio`, `iis_shortnames`, `internetdb`, `ipneighbor`, `leakix`, `myssl`, `ntlm`, `oauth`, `otx`, `paramminer_cookies`, `paramminer_getparams`, `paramminer_headers`, `passivetotal`, `pgp`, `postman_download`, `postman`, `rapiddns`, `robots`, `secretsdb`, `securitytrails`, `securitytxt`, `shodan_dns`, `sitedossier`, `skymem`, `social`, `sslcert`, `subdomaincenter`, `subdomainradar`, `trickest`, `trufflehog`, `urlscan`, `virustotal`, `wappalyzer`, `wayback`, `zoomeye`") +Modules: [87]("`anubisdb`, `apkpure`, `asn`, `azure_realm`, `azure_tenant`, `baddns_direct`, `baddns_zone`, `baddns`, `badsecrets`, `bevigil`, `binaryedge`, `bucket_amazon`, `bucket_azure`, `bucket_digitalocean`, `bucket_file_enum`, `bucket_firebase`, `bucket_google`, `bufferoverrun`, `builtwith`, `c99`, `censys`, `certspotter`, `chaos`, `code_repository`, `columbus`, `crt`, `dehashed`, `digitorus`, `dnsbimi`, `dnsbrute_mutations`, `dnsbrute`, `dnscaa`, `dnscommonsrv`, `dnsdumpster`, `dnstlsrpt`, `docker_pull`, `dockerhub`, `emailformat`, `ffuf_shortnames`, `ffuf`, `filedownload`, `fullhunt`, `git_clone`, `git`, `github_codesearch`, `github_org`, `github_workflows`, `gitlab`, `google_playstore`, `gowitness`, `hackertarget`, `httpx`, `hunterio`, `iis_shortnames`, `internetdb`, `ipneighbor`, `leakix`, `myssl`, `ntlm`, `oauth`, `otx`, `paramminer_cookies`, `paramminer_getparams`, `paramminer_headers`, `passivetotal`, `pgp`, `postman_download`, `postman`, `rapiddns`, `robots`, `secretsdb`, `securitytrails`, `securitytxt`, `shodan_dns`, `sitedossier`, `skymem`, `social`, `sslcert`, `subdomaincenter`, `subdomainradar`, `trickest`, `trufflehog`, `urlscan`, `virustotal`, `wappalyzer`, `wayback`, `zoomeye`") ## **paramminer** @@ -278,6 +303,10 @@ Recursive web spider modules: - httpx + blacklist: + # Prevent spider from invalidating sessions by logging out + - "RE:/.*(sign|log)[_-]?out" + config: web: # how many links to follow in a row @@ -324,7 +353,7 @@ Enumerate subdomains via APIs, brute-force -Modules: [52]("`anubisdb`, `asn`, `azure_realm`, `azure_tenant`, `baddns_direct`, `baddns_zone`, `bevigil`, `binaryedge`, `bufferoverrun`, `builtwith`, `c99`, `censys`, `certspotter`, `chaos`, `columbus`, `crt`, `digitorus`, `dnsbimi`, `dnsbrute_mutations`, `dnsbrute`, `dnscaa`, `dnscommonsrv`, `dnsdumpster`, `fullhunt`, `github_codesearch`, `github_org`, `hackertarget`, `httpx`, `hunterio`, `internetdb`, `ipneighbor`, `leakix`, `myssl`, `oauth`, `otx`, `passivetotal`, `postman_download`, `postman`, `rapiddns`, `securitytrails`, `securitytxt`, `shodan_dns`, `sitedossier`, `social`, `sslcert`, `subdomaincenter`, `subdomainradar`, `trickest`, `urlscan`, `virustotal`, `wayback`, `zoomeye`") +Modules: [53]("`anubisdb`, `asn`, `azure_realm`, `azure_tenant`, `baddns_direct`, `baddns_zone`, `bevigil`, `binaryedge`, `bufferoverrun`, `builtwith`, `c99`, `censys`, `certspotter`, `chaos`, `columbus`, `crt`, `digitorus`, `dnsbimi`, `dnsbrute_mutations`, `dnsbrute`, `dnscaa`, `dnscommonsrv`, `dnsdumpster`, `dnstlsrpt`, `fullhunt`, `github_codesearch`, `github_org`, `hackertarget`, `httpx`, `hunterio`, `internetdb`, `ipneighbor`, `leakix`, `myssl`, `oauth`, `otx`, `passivetotal`, `postman_download`, `postman`, `rapiddns`, `securitytrails`, `securitytxt`, `shodan_dns`, `sitedossier`, `social`, `sslcert`, `subdomaincenter`, `subdomainradar`, `trickest`, `urlscan`, `virustotal`, `wayback`, `zoomeye`") ## **web-basic** @@ -397,21 +426,22 @@ Modules: [30]("`ajaxpro`, `azure_realm`, `baddns`, `badsecrets`, `bucket_amazon` Here is a the same data, but in a table: -| Preset | Category | Description | # Modules | Modules | -|-----------------|------------|--------------------------------------------------------------------------|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| baddns-thorough | | Run all baddns modules and submodules. | 4 | baddns, baddns_direct, baddns_zone, httpx | -| cloud-enum | | Enumerate cloud resources such as storage buckets, etc. | 59 | anubisdb, asn, azure_realm, azure_tenant, baddns, baddns_direct, baddns_zone, bevigil, binaryedge, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bufferoverrun, builtwith, c99, censys, certspotter, chaos, columbus, crt, digitorus, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, fullhunt, github_codesearch, github_org, hackertarget, httpx, hunterio, internetdb, ipneighbor, leakix, myssl, oauth, otx, passivetotal, postman, postman_download, rapiddns, securitytrails, securitytxt, shodan_dns, sitedossier, social, sslcert, subdomaincenter, subdomainradar, trickest, urlscan, virustotal, wayback, zoomeye | -| code-enum | | Enumerate Git repositories, Docker images, etc. | 16 | apkpure, code_repository, docker_pull, dockerhub, git, git_clone, github_codesearch, github_org, github_workflows, gitlab, google_playstore, httpx, postman, postman_download, social, trufflehog | -| dirbust-heavy | web | Recursive web directory brute-force (aggressive) | 5 | ffuf, ffuf_shortnames, httpx, iis_shortnames, wayback | -| dirbust-light | web | Basic web directory brute-force (surface-level directories only) | 4 | ffuf, ffuf_shortnames, httpx, iis_shortnames | -| dotnet-audit | web | Comprehensive scan for all IIS/.NET specific modules and module settings | 8 | ajaxpro, badsecrets, dotnetnuke, ffuf, ffuf_shortnames, httpx, iis_shortnames, telerik | -| email-enum | | Enumerate email addresses from APIs, web crawling, etc. | 7 | dehashed, dnscaa, emailformat, hunterio, pgp, skymem, sslcert | -| iis-shortnames | web | Recursively enumerate IIS shortnames | 3 | ffuf_shortnames, httpx, iis_shortnames | -| kitchen-sink | | Everything everywhere all at once | 86 | anubisdb, apkpure, asn, azure_realm, azure_tenant, baddns, baddns_direct, baddns_zone, badsecrets, bevigil, binaryedge, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bufferoverrun, builtwith, c99, censys, certspotter, chaos, code_repository, columbus, crt, dehashed, digitorus, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, docker_pull, dockerhub, emailformat, ffuf, ffuf_shortnames, filedownload, fullhunt, git, git_clone, github_codesearch, github_org, github_workflows, gitlab, google_playstore, gowitness, hackertarget, httpx, hunterio, iis_shortnames, internetdb, ipneighbor, leakix, myssl, ntlm, oauth, otx, paramminer_cookies, paramminer_getparams, paramminer_headers, passivetotal, pgp, postman, postman_download, rapiddns, robots, secretsdb, securitytrails, securitytxt, shodan_dns, sitedossier, skymem, social, sslcert, subdomaincenter, subdomainradar, trickest, trufflehog, urlscan, virustotal, wappalyzer, wayback, zoomeye | -| paramminer | web | Discover new web parameters via brute-force | 4 | httpx, paramminer_cookies, paramminer_getparams, paramminer_headers | -| spider | | Recursive web spider | 1 | httpx | -| subdomain-enum | | Enumerate subdomains via APIs, brute-force | 52 | anubisdb, asn, azure_realm, azure_tenant, baddns_direct, baddns_zone, bevigil, binaryedge, bufferoverrun, builtwith, c99, censys, certspotter, chaos, columbus, crt, digitorus, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, fullhunt, github_codesearch, github_org, hackertarget, httpx, hunterio, internetdb, ipneighbor, leakix, myssl, oauth, otx, passivetotal, postman, postman_download, rapiddns, securitytrails, securitytxt, shodan_dns, sitedossier, social, sslcert, subdomaincenter, subdomainradar, trickest, urlscan, virustotal, wayback, zoomeye | -| web-basic | | Quick web scan | 19 | azure_realm, baddns, badsecrets, bucket_amazon, bucket_azure, bucket_firebase, bucket_google, ffuf_shortnames, filedownload, git, httpx, iis_shortnames, ntlm, oauth, robots, secretsdb, securitytxt, sslcert, wappalyzer | -| web-screenshots | | Take screenshots of webpages | 3 | gowitness, httpx, social | -| web-thorough | | Aggressive web scan | 30 | ajaxpro, azure_realm, baddns, badsecrets, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_firebase, bucket_google, bypass403, dastardly, dotnetnuke, ffuf_shortnames, filedownload, generic_ssrf, git, host_header, httpx, hunt, iis_shortnames, ntlm, oauth, robots, secretsdb, securitytxt, smuggler, sslcert, telerik, url_manipulation, wappalyzer | +| Preset | Category | Description | # Modules | Modules | +|-----------------|------------|--------------------------------------------------------------------------|-------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| baddns-thorough | | Run all baddns modules and submodules. | 4 | baddns, baddns_direct, baddns_zone, httpx | +| cloud-enum | | Enumerate cloud resources such as storage buckets, etc. | 60 | anubisdb, asn, azure_realm, azure_tenant, baddns, baddns_direct, baddns_zone, bevigil, binaryedge, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bufferoverrun, builtwith, c99, censys, certspotter, chaos, columbus, crt, digitorus, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, fullhunt, github_codesearch, github_org, hackertarget, httpx, hunterio, internetdb, ipneighbor, leakix, myssl, oauth, otx, passivetotal, postman, postman_download, rapiddns, securitytrails, securitytxt, shodan_dns, sitedossier, social, sslcert, subdomaincenter, subdomainradar, trickest, urlscan, virustotal, wayback, zoomeye | +| code-enum | | Enumerate Git repositories, Docker images, etc. | 16 | apkpure, code_repository, docker_pull, dockerhub, git, git_clone, github_codesearch, github_org, github_workflows, gitlab, google_playstore, httpx, postman, postman_download, social, trufflehog | +| dirbust-heavy | web | Recursive web directory brute-force (aggressive) | 5 | ffuf, ffuf_shortnames, httpx, iis_shortnames, wayback | +| dirbust-light | web | Basic web directory brute-force (surface-level directories only) | 4 | ffuf, ffuf_shortnames, httpx, iis_shortnames | +| dotnet-audit | web | Comprehensive scan for all IIS/.NET specific modules and module settings | 8 | ajaxpro, badsecrets, dotnetnuke, ffuf, ffuf_shortnames, httpx, iis_shortnames, telerik | +| email-enum | | Enumerate email addresses from APIs, web crawling, etc. | 8 | dehashed, dnscaa, dnstlsrpt, emailformat, hunterio, pgp, skymem, sslcert | +| fast | | Scan only the provided targets as fast as possible - no extra discovery | 0 | | +| iis-shortnames | web | Recursively enumerate IIS shortnames | 3 | ffuf_shortnames, httpx, iis_shortnames | +| kitchen-sink | | Everything everywhere all at once | 87 | anubisdb, apkpure, asn, azure_realm, azure_tenant, baddns, baddns_direct, baddns_zone, badsecrets, bevigil, binaryedge, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_file_enum, bucket_firebase, bucket_google, bufferoverrun, builtwith, c99, censys, certspotter, chaos, code_repository, columbus, crt, dehashed, digitorus, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, docker_pull, dockerhub, emailformat, ffuf, ffuf_shortnames, filedownload, fullhunt, git, git_clone, github_codesearch, github_org, github_workflows, gitlab, google_playstore, gowitness, hackertarget, httpx, hunterio, iis_shortnames, internetdb, ipneighbor, leakix, myssl, ntlm, oauth, otx, paramminer_cookies, paramminer_getparams, paramminer_headers, passivetotal, pgp, postman, postman_download, rapiddns, robots, secretsdb, securitytrails, securitytxt, shodan_dns, sitedossier, skymem, social, sslcert, subdomaincenter, subdomainradar, trickest, trufflehog, urlscan, virustotal, wappalyzer, wayback, zoomeye | +| paramminer | web | Discover new web parameters via brute-force | 4 | httpx, paramminer_cookies, paramminer_getparams, paramminer_headers | +| spider | | Recursive web spider | 1 | httpx | +| subdomain-enum | | Enumerate subdomains via APIs, brute-force | 53 | anubisdb, asn, azure_realm, azure_tenant, baddns_direct, baddns_zone, bevigil, binaryedge, bufferoverrun, builtwith, c99, censys, certspotter, chaos, columbus, crt, digitorus, dnsbimi, dnsbrute, dnsbrute_mutations, dnscaa, dnscommonsrv, dnsdumpster, dnstlsrpt, fullhunt, github_codesearch, github_org, hackertarget, httpx, hunterio, internetdb, ipneighbor, leakix, myssl, oauth, otx, passivetotal, postman, postman_download, rapiddns, securitytrails, securitytxt, shodan_dns, sitedossier, social, sslcert, subdomaincenter, subdomainradar, trickest, urlscan, virustotal, wayback, zoomeye | +| web-basic | | Quick web scan | 19 | azure_realm, baddns, badsecrets, bucket_amazon, bucket_azure, bucket_firebase, bucket_google, ffuf_shortnames, filedownload, git, httpx, iis_shortnames, ntlm, oauth, robots, secretsdb, securitytxt, sslcert, wappalyzer | +| web-screenshots | | Take screenshots of webpages | 3 | gowitness, httpx, social | +| web-thorough | | Aggressive web scan | 30 | ajaxpro, azure_realm, baddns, badsecrets, bucket_amazon, bucket_azure, bucket_digitalocean, bucket_firebase, bucket_google, bypass403, dastardly, dotnetnuke, ffuf_shortnames, filedownload, generic_ssrf, git, host_header, httpx, hunt, iis_shortnames, ntlm, oauth, robots, secretsdb, securitytxt, smuggler, sslcert, telerik, url_manipulation, wappalyzer | diff --git a/docs/scanning/tips_and_tricks.md b/docs/scanning/tips_and_tricks.md index 32b55448f1..c5073c1d63 100644 --- a/docs/scanning/tips_and_tricks.md +++ b/docs/scanning/tips_and_tricks.md @@ -77,15 +77,46 @@ You can also pair the web spider with subdomain enumeration: bbot -t evilcorp.com -f subdomain-enum -c spider.yml ``` -### Ingesting BBOT Data Into SIEM (Elastic, Splunk) +### Exclude CDNs from Port Scan -If your goal is to feed BBOT data into a SIEM such as Elastic, be sure to enable this option when scanning: +If you want to exclude CDNs (e.g. Cloudflare) from port scanning, you can set the `allowed_cdn_ports` config option in the `portscan` module. For example, to allow only port 80 (HTTP) and 443 (HTTPS), you can do the following: + +```bash +bbot -t evilcorp.com -m portscan -c modules.portscan.allowed_cdn_ports=80,443 +``` + +By default, if you set `allowed_cdn_ports`, it will skip only providers marked as CDNs. If you want to skip cloud providers as well, you can set `cdn_tags`, which is a comma-separated list of tags to skip (matched against the beginning of each tag). + +```bash +bbot -t evilcorp.com -m portscan -c modules.portscan.allowed_cdn_ports=80,443 modules.portscan.cdn_tags=cdn-,cloud- +``` + +...or via a preset: + +```yaml title="skip_cdns.yml" +modules: + - portscan + +config: + modules: + portscan: + allowed_cdn_ports: 80,443 + cdn_tags: cdn-,cloud- +``` + +```bash +bbot -t evilcorp.com -p skip_cdns.yml +``` + +### Ingest BBOT Data Into SIEM (Elastic, Splunk) + +If your goal is to run a BBOT scan and later feed its data into a SIEM such as Elastic, be sure to enable this option when scanning: ```bash bbot -t evilcorp.com -c modules.json.siem_friendly=true ``` -This nests the event's `.data` beneath its event type like so: +This ensures the `.data` event attribute is always the same type (a dictionary), by nesting it like so: ```json { "type": "DNS_NAME", diff --git a/mkdocs.yml b/mkdocs.yml index 1802fc678a..4413fac487 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,7 +7,7 @@ site_description: >- # Repository repo_name: blacklanternsecurity/bbot repo_url: https://github.com/blacklanternsecurity/bbot -watch: +watch: - "mkdocs.yml" - "bbot" - "docs" @@ -29,7 +29,7 @@ nav: - Tips and Tricks: scanning/tips_and_tricks.md - Advanced Usage: scanning/advanced.md - Configuration: scanning/configuration.md - - Modules: + - Modules: - List of Modules: modules/list_of_modules.md - Nuclei: modules/nuclei.md - Custom YARA Rules: modules/custom_yara_rules.md diff --git a/poetry.lock b/poetry.lock index 2d53dd5620..9dc7f17d5e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -27,13 +27,13 @@ ansible-core = ">=2.15.7,<2.16.0" [[package]] name = "ansible-core" -version = "2.15.12" +version = "2.15.13" description = "Radically simple IT automation" optional = false python-versions = ">=3.9" files = [ - {file = "ansible_core-2.15.12-py3-none-any.whl", hash = "sha256:390edd603420122f7cb1c470d8d1f8bdbbd795a1844dd03c1917db21935aecb9"}, - {file = "ansible_core-2.15.12.tar.gz", hash = "sha256:5fde82cd3928d9857ad880782c644f27d3168b0f25321d5a8d6befa524aa1818"}, + {file = "ansible_core-2.15.13-py3-none-any.whl", hash = "sha256:e7f50bbb61beae792f5ecb86eff82149d3948d078361d70aedb01d76bc483c30"}, + {file = "ansible_core-2.15.13.tar.gz", hash = "sha256:f542e702ee31fb049732143aeee6b36311ca48b7d13960a0685afffa0d742d7f"}, ] [package.dependencies] @@ -74,24 +74,24 @@ files = [ [[package]] name = "anyio" -version = "4.6.2.post1" +version = "4.7.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" files = [ - {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, - {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, + {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, + {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -129,52 +129,6 @@ charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] -[[package]] -name = "black" -version = "24.10.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.9" -files = [ - {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, - {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, - {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, - {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, - {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, - {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, - {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, - {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, - {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, - {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, - {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, - {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, - {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, - {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, - {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, - {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, - {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, - {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, - {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, - {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, - {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, - {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "cachetools" version = "5.5.0" @@ -188,13 +142,13 @@ files = [ [[package]] name = "certifi" -version = "2024.8.30" +version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] [[package]] @@ -417,19 +371,19 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "cloudcheck" -version = "5.0.1.595" +version = "7.0.33" description = "Check whether an IP address belongs to a cloud provider" optional = false python-versions = "<4.0,>=3.9" files = [ - {file = "cloudcheck-5.0.1.595-py3-none-any.whl", hash = "sha256:68acec63b09400fa0409ae7f3ffa817cbc891bf8a2ac63f9610a3b049a4bf57d"}, - {file = "cloudcheck-5.0.1.595.tar.gz", hash = "sha256:38456074332ed2ba928e7073e3928a5223a6005a64124b4b342d8b9599ca10e0"}, + {file = "cloudcheck-7.0.33-py3-none-any.whl", hash = "sha256:005d6888b3b4526888f98f9514487e801d521d756b48c7ff55daa9a638fda570"}, + {file = "cloudcheck-7.0.33.tar.gz", hash = "sha256:36699d3868ffcdd3ac36e761e3c074a69d32120c787013d36820f6766ab73543"}, ] [package.dependencies] -httpx = ">=0.26,<0.28" +httpx = ">=0.26,<0.29" pydantic = ">=2.4.2,<3.0.0" -radixtarget = ">=1.0.0.14,<2.0.0.0" +radixtarget = ">=3.0.13,<4.0.0" regex = ">=2024.4.16,<2025.0.0" [[package]] @@ -445,73 +399,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.3" +version = "7.6.9" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6da42bbcec130b188169107ecb6ee7bd7b4c849d24c9370a0c884cf728d8e976"}, - {file = "coverage-7.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c222958f59b0ae091f4535851cbb24eb57fc0baea07ba675af718fb5302dddb2"}, - {file = "coverage-7.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab84a8b698ad5a6c365b08061920138e7a7dd9a04b6feb09ba1bfae68346ce6d"}, - {file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70a6756ce66cd6fe8486c775b30889f0dc4cb20c157aa8c35b45fd7868255c5c"}, - {file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c2e6fa98032fec8282f6b27e3f3986c6e05702828380618776ad794e938f53a"}, - {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:921fbe13492caf6a69528f09d5d7c7d518c8d0e7b9f6701b7719715f29a71e6e"}, - {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6d99198203f0b9cb0b5d1c0393859555bc26b548223a769baf7e321a627ed4fc"}, - {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:87cd2e29067ea397a47e352efb13f976eb1b03e18c999270bb50589323294c6e"}, - {file = "coverage-7.6.3-cp310-cp310-win32.whl", hash = "sha256:a3328c3e64ea4ab12b85999eb0779e6139295bbf5485f69d42cf794309e3d007"}, - {file = "coverage-7.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:bca4c8abc50d38f9773c1ec80d43f3768df2e8576807d1656016b9d3eeaa96fd"}, - {file = "coverage-7.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c51ef82302386d686feea1c44dbeef744585da16fcf97deea2a8d6c1556f519b"}, - {file = "coverage-7.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0ca37993206402c6c35dc717f90d4c8f53568a8b80f0bf1a1b2b334f4d488fba"}, - {file = "coverage-7.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c77326300b839c44c3e5a8fe26c15b7e87b2f32dfd2fc9fee1d13604347c9b38"}, - {file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e484e479860e00da1f005cd19d1c5d4a813324e5951319ac3f3eefb497cc549"}, - {file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c6c0f4d53ef603397fc894a895b960ecd7d44c727df42a8d500031716d4e8d2"}, - {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:37be7b5ea3ff5b7c4a9db16074dc94523b5f10dd1f3b362a827af66a55198175"}, - {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:43b32a06c47539fe275106b376658638b418c7cfdfff0e0259fbf877e845f14b"}, - {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee77c7bef0724165e795b6b7bf9c4c22a9b8468a6bdb9c6b4281293c6b22a90f"}, - {file = "coverage-7.6.3-cp311-cp311-win32.whl", hash = "sha256:43517e1f6b19f610a93d8227e47790722c8bf7422e46b365e0469fc3d3563d97"}, - {file = "coverage-7.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:04f2189716e85ec9192df307f7c255f90e78b6e9863a03223c3b998d24a3c6c6"}, - {file = "coverage-7.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27bd5f18d8f2879e45724b0ce74f61811639a846ff0e5c0395b7818fae87aec6"}, - {file = "coverage-7.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d546cfa78844b8b9c1c0533de1851569a13f87449897bbc95d698d1d3cb2a30f"}, - {file = "coverage-7.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9975442f2e7a5cfcf87299c26b5a45266ab0696348420049b9b94b2ad3d40234"}, - {file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:583049c63106c0555e3ae3931edab5669668bbef84c15861421b94e121878d3f"}, - {file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2341a78ae3a5ed454d524206a3fcb3cec408c2a0c7c2752cd78b606a2ff15af4"}, - {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a4fb91d5f72b7e06a14ff4ae5be625a81cd7e5f869d7a54578fc271d08d58ae3"}, - {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e279f3db904e3b55f520f11f983cc8dc8a4ce9b65f11692d4718ed021ec58b83"}, - {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa23ce39661a3e90eea5f99ec59b763b7d655c2cada10729ed920a38bfc2b167"}, - {file = "coverage-7.6.3-cp312-cp312-win32.whl", hash = "sha256:52ac29cc72ee7e25ace7807249638f94c9b6a862c56b1df015d2b2e388e51dbd"}, - {file = "coverage-7.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:40e8b1983080439d4802d80b951f4a93d991ef3261f69e81095a66f86cf3c3c6"}, - {file = "coverage-7.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9134032f5aa445ae591c2ba6991d10136a1f533b1d2fa8f8c21126468c5025c6"}, - {file = "coverage-7.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99670790f21a96665a35849990b1df447993880bb6463a0a1d757897f30da929"}, - {file = "coverage-7.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc7d6b380ca76f5e817ac9eef0c3686e7834c8346bef30b041a4ad286449990"}, - {file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7b26757b22faf88fcf232f5f0e62f6e0fd9e22a8a5d0d5016888cdfe1f6c1c4"}, - {file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c59d6a4a4633fad297f943c03d0d2569867bd5372eb5684befdff8df8522e39"}, - {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f263b18692f8ed52c8de7f40a0751e79015983dbd77b16906e5b310a39d3ca21"}, - {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79644f68a6ff23b251cae1c82b01a0b51bc40c8468ca9585c6c4b1aeee570e0b"}, - {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71967c35828c9ff94e8c7d405469a1fb68257f686bca7c1ed85ed34e7c2529c4"}, - {file = "coverage-7.6.3-cp313-cp313-win32.whl", hash = "sha256:e266af4da2c1a4cbc6135a570c64577fd3e6eb204607eaff99d8e9b710003c6f"}, - {file = "coverage-7.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:ea52bd218d4ba260399a8ae4bb6b577d82adfc4518b93566ce1fddd4a49d1dce"}, - {file = "coverage-7.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8d4c6ea0f498c7c79111033a290d060c517853a7bcb2f46516f591dab628ddd3"}, - {file = "coverage-7.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:331b200ad03dbaa44151d74daeb7da2cf382db424ab923574f6ecca7d3b30de3"}, - {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54356a76b67cf8a3085818026bb556545ebb8353951923b88292556dfa9f812d"}, - {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebec65f5068e7df2d49466aab9128510c4867e532e07cb6960075b27658dca38"}, - {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d33a785ea8354c480515e781554d3be582a86297e41ccbea627a5c632647f2cd"}, - {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f7ddb920106bbbbcaf2a274d56f46956bf56ecbde210d88061824a95bdd94e92"}, - {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:70d24936ca6c15a3bbc91ee9c7fc661132c6f4c9d42a23b31b6686c05073bde5"}, - {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c30e42ea11badb147f0d2e387115b15e2bd8205a5ad70d6ad79cf37f6ac08c91"}, - {file = "coverage-7.6.3-cp313-cp313t-win32.whl", hash = "sha256:365defc257c687ce3e7d275f39738dcd230777424117a6c76043459db131dd43"}, - {file = "coverage-7.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:23bb63ae3f4c645d2d82fa22697364b0046fbafb6261b258a58587441c5f7bd0"}, - {file = "coverage-7.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da29ceabe3025a1e5a5aeeb331c5b1af686daab4ff0fb4f83df18b1180ea83e2"}, - {file = "coverage-7.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df8c05a0f574d480947cba11b947dc41b1265d721c3777881da2fb8d3a1ddfba"}, - {file = "coverage-7.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1e3b40b82236d100d259854840555469fad4db64f669ab817279eb95cd535c"}, - {file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4adeb878a374126f1e5cf03b87f66279f479e01af0e9a654cf6d1509af46c40"}, - {file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43d6a66e33b1455b98fc7312b124296dad97a2e191c80320587234a77b1b736e"}, - {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1990b1f4e2c402beb317840030bb9f1b6a363f86e14e21b4212e618acdfce7f6"}, - {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:12f9515d875859faedb4144fd38694a761cd2a61ef9603bf887b13956d0bbfbb"}, - {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99ded130555c021d99729fabd4ddb91a6f4cc0707df4b1daf912c7850c373b13"}, - {file = "coverage-7.6.3-cp39-cp39-win32.whl", hash = "sha256:c3a79f56dee9136084cf84a6c7c4341427ef36e05ae6415bf7d787c96ff5eaa3"}, - {file = "coverage-7.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:aac7501ae73d4a02f4b7ac8fcb9dc55342ca98ffb9ed9f2dfb8a25d53eda0e4d"}, - {file = "coverage-7.6.3-pp39.pp310-none-any.whl", hash = "sha256:b9853509b4bf57ba7b1f99b9d866c422c9c5248799ab20e652bbb8a184a38181"}, - {file = "coverage-7.6.3.tar.gz", hash = "sha256:bb7d5fe92bd0dc235f63ebe9f8c6e0884f7360f88f3411bfed1350c872ef2054"}, + {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, + {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, + {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, + {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, + {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, + {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, + {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, + {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, + {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, + {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, + {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, + {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, + {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, + {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, + {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, + {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, ] [package.dependencies] @@ -618,26 +572,15 @@ idna = ["idna (>=3.7)"] trio = ["trio (>=0.23)"] wmi = ["wmi (>=1.5.1)"] -[[package]] -name = "docutils" -version = "0.21.2" -description = "Docutils -- Python Documentation Utilities" -optional = false -python-versions = ">=3.9" -files = [ - {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, - {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, -] - [[package]] name = "dunamai" -version = "1.22.0" +version = "1.23.0" description = "Dynamic version generation" optional = false python-versions = ">=3.5" files = [ - {file = "dunamai-1.22.0-py3-none-any.whl", hash = "sha256:eab3894b31e145bd028a74b13491c57db01986a7510482c9b5fff3b4e53d77b7"}, - {file = "dunamai-1.22.0.tar.gz", hash = "sha256:375a0b21309336f0d8b6bbaea3e038c36f462318c68795166e31f9873fdad676"}, + {file = "dunamai-1.23.0-py3-none-any.whl", hash = "sha256:a0906d876e92441793c6a423e16a4802752e723e9c9a5aabdc5535df02dbe041"}, + {file = "dunamai-1.23.0.tar.gz", hash = "sha256:a163746de7ea5acb6dacdab3a6ad621ebc612ed1e528aaa8beedb8887fccd2c4"}, ] [package.dependencies] @@ -657,6 +600,26 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "fastapi" +version = "0.115.6" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305"}, + {file = "fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.40.0,<0.42.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] + [[package]] name = "filelock" version = "3.16.1" @@ -673,22 +636,6 @@ docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2. testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] typing = ["typing-extensions (>=4.12.2)"] -[[package]] -name = "flake8" -version = "7.1.1" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.8.1" -files = [ - {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, - {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.12.0,<2.13.0" -pyflakes = ">=3.2.0,<3.3.0" - [[package]] name = "ghp-import" version = "2.1.0" @@ -733,13 +680,13 @@ files = [ [[package]] name = "httpcore" -version = "1.0.6" +version = "1.0.7" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f"}, - {file = "httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"}, + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, ] [package.dependencies] @@ -779,13 +726,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "identify" -version = "2.6.1" +version = "2.6.3" description = "File identification library for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, - {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, + {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, + {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, ] [package.extras] @@ -884,13 +831,13 @@ files = [ [[package]] name = "livereload" -version = "2.7.0" +version = "2.7.1" description = "Python LiveReload is an awesome tool for web developers" optional = false python-versions = ">=3.7" files = [ - {file = "livereload-2.7.0-py3-none-any.whl", hash = "sha256:19bee55aff51d5ade6ede0dc709189a0f904d3b906d3ea71641ed548acff3246"}, - {file = "livereload-2.7.0.tar.gz", hash = "sha256:f4ba199ef93248902841e298670eebfe1aa9e148e19b343bc57dbf1b74de0513"}, + {file = "livereload-2.7.1-py3-none-any.whl", hash = "sha256:5201740078c1b9433f4b2ba22cd2729a39b9d0ec0a2cc6b4d3df257df5ad0564"}, + {file = "livereload-2.7.1.tar.gz", hash = "sha256:3d9bf7c05673df06e32bea23b494b8d36ca6d10f7d5c3c8a6989608c09c986a9"}, ] [package.dependencies] @@ -1149,17 +1096,6 @@ files = [ {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - [[package]] name = "mergedeep" version = "1.3.4" @@ -1278,13 +1214,13 @@ pyyaml = ">=5.1" [[package]] name = "mkdocs-material" -version = "9.5.44" +version = "9.5.49" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.44-py3-none-any.whl", hash = "sha256:47015f9c167d58a5ff5e682da37441fc4d66a1c79334bfc08d774763cacf69ca"}, - {file = "mkdocs_material-9.5.44.tar.gz", hash = "sha256:f3a6c968e524166b3f3ed1fb97d3ed3e0091183b0545cedf7156a2a6804c56c0"}, + {file = "mkdocs_material-9.5.49-py3-none-any.whl", hash = "sha256:c3c2d8176b18198435d3a3e119011922f3e11424074645c24019c2dcf08a360e"}, + {file = "mkdocs_material-9.5.49.tar.gz", hash = "sha256:3671bb282b4f53a1c72e08adbe04d2481a98f85fed392530051f80ff94a9621d"}, ] [package.dependencies] @@ -1473,17 +1409,6 @@ plot = ["matplotlib (==3.9.2)", "pandas (==2.2.2)"] test = ["pytest (==8.3.3)", "pytest-sugar (==1.0.0)"] type = ["mypy (==1.11.2)"] -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - [[package]] name = "nodeenv" version = "1.9.1" @@ -1524,15 +1449,99 @@ files = [ [package.extras] dev = ["black", "mypy", "pytest"] +[[package]] +name = "orjson" +version = "3.10.12" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +optional = false +python-versions = ">=3.8" +files = [ + {file = "orjson-3.10.12-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ece01a7ec71d9940cc654c482907a6b65df27251255097629d0dea781f255c6d"}, + {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34ec9aebc04f11f4b978dd6caf697a2df2dd9b47d35aa4cc606cabcb9df69d7"}, + {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd6ec8658da3480939c79b9e9e27e0db31dffcd4ba69c334e98c9976ac29140e"}, + {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17e6baf4cf01534c9de8a16c0c611f3d94925d1701bf5f4aff17003677d8ced"}, + {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6402ebb74a14ef96f94a868569f5dccf70d791de49feb73180eb3c6fda2ade56"}, + {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0000758ae7c7853e0a4a6063f534c61656ebff644391e1f81698c1b2d2fc8cd2"}, + {file = "orjson-3.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:888442dcee99fd1e5bd37a4abb94930915ca6af4db50e23e746cdf4d1e63db13"}, + {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c1f7a3ce79246aa0e92f5458d86c54f257fb5dfdc14a192651ba7ec2c00f8a05"}, + {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:802a3935f45605c66fb4a586488a38af63cb37aaad1c1d94c982c40dcc452e85"}, + {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1da1ef0113a2be19bb6c557fb0ec2d79c92ebd2fed4cfb1b26bab93f021fb885"}, + {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a3273e99f367f137d5b3fecb5e9f45bcdbfac2a8b2f32fbc72129bbd48789c2"}, + {file = "orjson-3.10.12-cp310-none-win32.whl", hash = "sha256:475661bf249fd7907d9b0a2a2421b4e684355a77ceef85b8352439a9163418c3"}, + {file = "orjson-3.10.12-cp310-none-win_amd64.whl", hash = "sha256:87251dc1fb2b9e5ab91ce65d8f4caf21910d99ba8fb24b49fd0c118b2362d509"}, + {file = "orjson-3.10.12-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a734c62efa42e7df94926d70fe7d37621c783dea9f707a98cdea796964d4cf74"}, + {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:750f8b27259d3409eda8350c2919a58b0cfcd2054ddc1bd317a643afc646ef23"}, + {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb52c22bfffe2857e7aa13b4622afd0dd9d16ea7cc65fd2bf318d3223b1b6252"}, + {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:440d9a337ac8c199ff8251e100c62e9488924c92852362cd27af0e67308c16ef"}, + {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e15c06491c69997dfa067369baab3bf094ecb74be9912bdc4339972323f252"}, + {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:362d204ad4b0b8724cf370d0cd917bb2dc913c394030da748a3bb632445ce7c4"}, + {file = "orjson-3.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b57cbb4031153db37b41622eac67329c7810e5f480fda4cfd30542186f006ae"}, + {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:165c89b53ef03ce0d7c59ca5c82fa65fe13ddf52eeb22e859e58c237d4e33b9b"}, + {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5dee91b8dfd54557c1a1596eb90bcd47dbcd26b0baaed919e6861f076583e9da"}, + {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a4e1cfb72de6f905bdff061172adfb3caf7a4578ebf481d8f0530879476c07"}, + {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:038d42c7bc0606443459b8fe2d1f121db474c49067d8d14c6a075bbea8bf14dd"}, + {file = "orjson-3.10.12-cp311-none-win32.whl", hash = "sha256:03b553c02ab39bed249bedd4abe37b2118324d1674e639b33fab3d1dafdf4d79"}, + {file = "orjson-3.10.12-cp311-none-win_amd64.whl", hash = "sha256:8b8713b9e46a45b2af6b96f559bfb13b1e02006f4242c156cbadef27800a55a8"}, + {file = "orjson-3.10.12-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:53206d72eb656ca5ac7d3a7141e83c5bbd3ac30d5eccfe019409177a57634b0d"}, + {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac8010afc2150d417ebda810e8df08dd3f544e0dd2acab5370cfa6bcc0662f8f"}, + {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed459b46012ae950dd2e17150e838ab08215421487371fa79d0eced8d1461d70"}, + {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dcb9673f108a93c1b52bfc51b0af422c2d08d4fc710ce9c839faad25020bb69"}, + {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22a51ae77680c5c4652ebc63a83d5255ac7d65582891d9424b566fb3b5375ee9"}, + {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910fdf2ac0637b9a77d1aad65f803bac414f0b06f720073438a7bd8906298192"}, + {file = "orjson-3.10.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24ce85f7100160936bc2116c09d1a8492639418633119a2224114f67f63a4559"}, + {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a76ba5fc8dd9c913640292df27bff80a685bed3a3c990d59aa6ce24c352f8fc"}, + {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ff70ef093895fd53f4055ca75f93f047e088d1430888ca1229393a7c0521100f"}, + {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f4244b7018b5753ecd10a6d324ec1f347da130c953a9c88432c7fbc8875d13be"}, + {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16135ccca03445f37921fa4b585cff9a58aa8d81ebcb27622e69bfadd220b32c"}, + {file = "orjson-3.10.12-cp312-none-win32.whl", hash = "sha256:2d879c81172d583e34153d524fcba5d4adafbab8349a7b9f16ae511c2cee8708"}, + {file = "orjson-3.10.12-cp312-none-win_amd64.whl", hash = "sha256:fc23f691fa0f5c140576b8c365bc942d577d861a9ee1142e4db468e4e17094fb"}, + {file = "orjson-3.10.12-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:47962841b2a8aa9a258b377f5188db31ba49af47d4003a32f55d6f8b19006543"}, + {file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6334730e2532e77b6054e87ca84f3072bee308a45a452ea0bffbbbc40a67e296"}, + {file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:accfe93f42713c899fdac2747e8d0d5c659592df2792888c6c5f829472e4f85e"}, + {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7974c490c014c48810d1dede6c754c3cc46598da758c25ca3b4001ac45b703f"}, + {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3f250ce7727b0b2682f834a3facff88e310f52f07a5dcfd852d99637d386e79e"}, + {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f31422ff9486ae484f10ffc51b5ab2a60359e92d0716fcce1b3593d7bb8a9af6"}, + {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5f29c5d282bb2d577c2a6bbde88d8fdcc4919c593f806aac50133f01b733846e"}, + {file = "orjson-3.10.12-cp313-none-win32.whl", hash = "sha256:f45653775f38f63dc0e6cd4f14323984c3149c05d6007b58cb154dd080ddc0dc"}, + {file = "orjson-3.10.12-cp313-none-win_amd64.whl", hash = "sha256:229994d0c376d5bdc91d92b3c9e6be2f1fbabd4cc1b59daae1443a46ee5e9825"}, + {file = "orjson-3.10.12-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7d69af5b54617a5fac5c8e5ed0859eb798e2ce8913262eb522590239db6c6763"}, + {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ed119ea7d2953365724a7059231a44830eb6bbb0cfead33fcbc562f5fd8f935"}, + {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5fc1238ef197e7cad5c91415f524aaa51e004be5a9b35a1b8a84ade196f73f"}, + {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43509843990439b05f848539d6f6198d4ac86ff01dd024b2f9a795c0daeeab60"}, + {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f72e27a62041cfb37a3de512247ece9f240a561e6c8662276beaf4d53d406db4"}, + {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a904f9572092bb6742ab7c16c623f0cdccbad9eeb2d14d4aa06284867bddd31"}, + {file = "orjson-3.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:855c0833999ed5dc62f64552db26f9be767434917d8348d77bacaab84f787d7b"}, + {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:897830244e2320f6184699f598df7fb9db9f5087d6f3f03666ae89d607e4f8ed"}, + {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:0b32652eaa4a7539f6f04abc6243619c56f8530c53bf9b023e1269df5f7816dd"}, + {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:36b4aa31e0f6a1aeeb6f8377769ca5d125db000f05c20e54163aef1d3fe8e833"}, + {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5535163054d6cbf2796f93e4f0dbc800f61914c0e3c4ed8499cf6ece22b4a3da"}, + {file = "orjson-3.10.12-cp38-none-win32.whl", hash = "sha256:90a5551f6f5a5fa07010bf3d0b4ca2de21adafbbc0af6cb700b63cd767266cb9"}, + {file = "orjson-3.10.12-cp38-none-win_amd64.whl", hash = "sha256:703a2fb35a06cdd45adf5d733cf613cbc0cb3ae57643472b16bc22d325b5fb6c"}, + {file = "orjson-3.10.12-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f29de3ef71a42a5822765def1febfb36e0859d33abf5c2ad240acad5c6a1b78d"}, + {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de365a42acc65d74953f05e4772c974dad6c51cfc13c3240899f534d611be967"}, + {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a5a0158648a67ff0004cb0df5df7dcc55bfc9ca154d9c01597a23ad54c8d0c"}, + {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c47ce6b8d90fe9646a25b6fb52284a14ff215c9595914af63a5933a49972ce36"}, + {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0eee4c2c5bfb5c1b47a5db80d2ac7aaa7e938956ae88089f098aff2c0f35d5d8"}, + {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d3081bbe8b86587eb5c98a73b97f13d8f9fea685cf91a579beddacc0d10566"}, + {file = "orjson-3.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c23a6e90383884068bc2dba83d5222c9fcc3b99a0ed2411d38150734236755"}, + {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5472be7dc3269b4b52acba1433dac239215366f89dc1d8d0e64029abac4e714e"}, + {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:7319cda750fca96ae5973efb31b17d97a5c5225ae0bc79bf5bf84df9e1ec2ab6"}, + {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:74d5ca5a255bf20b8def6a2b96b1e18ad37b4a122d59b154c458ee9494377f80"}, + {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ff31d22ecc5fb85ef62c7d4afe8301d10c558d00dd24274d4bbe464380d3cd69"}, + {file = "orjson-3.10.12-cp39-none-win32.whl", hash = "sha256:c22c3ea6fba91d84fcb4cda30e64aff548fcf0c44c876e681f47d61d24b12e6b"}, + {file = "orjson-3.10.12-cp39-none-win_amd64.whl", hash = "sha256:be604f60d45ace6b0b33dd990a66b4526f1a7a186ac411c942674625456ca548"}, + {file = "orjson-3.10.12.tar.gz", hash = "sha256:0a78bbda3aea0f9f079057ee1ee8a1ecf790d4f1af88dd67493c6b8ee52506ff"}, +] + [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -1695,17 +1704,6 @@ files = [ {file = "puremagic-1.28.tar.gz", hash = "sha256:195893fc129657f611b86b959aab337207d6df7f25372209269ed9e303c1a8c0"}, ] -[[package]] -name = "pycodestyle" -version = "2.12.1" -description = "Python style guide checker" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, - {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, -] - [[package]] name = "pycparser" version = "2.22" @@ -1760,22 +1758,19 @@ files = [ [[package]] name = "pydantic" -version = "2.9.2" +version = "2.10.4" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, - {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, + {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, + {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.23.4" -typing-extensions = [ - {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, - {version = ">=4.6.1", markers = "python_version < \"3.13\""}, -] +pydantic-core = "2.27.2" +typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] @@ -1783,116 +1778,116 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.23.4" +version = "2.27.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, - {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, - {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, - {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, - {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, - {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, - {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, - {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, - {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, - {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, - {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, - {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, - {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, - {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, - {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, - {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, - {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, - {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, - {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, - {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, - {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, - {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, - {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, - {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, - {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, - {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, - {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, - {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, - {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, - {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, - {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, - {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, - {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, - {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, - {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, - {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, - {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, + {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, ] [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" -[[package]] -name = "pyflakes" -version = "3.2.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, - {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, -] - [[package]] name = "pygments" version = "2.18.0" @@ -1909,13 +1904,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" -version = "2.9.0" +version = "2.10.1" description = "JSON Web Token implementation in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, - {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, + {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, + {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, ] [package.extras] @@ -1926,13 +1921,13 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pymdown-extensions" -version = "10.11.2" +version = "10.12" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.11.2-py3-none-any.whl", hash = "sha256:41cdde0a77290e480cf53892f5c5e50921a7ee3e5cd60ba91bf19837b33badcf"}, - {file = "pymdown_extensions-10.11.2.tar.gz", hash = "sha256:bc8847ecc9e784a098efd35e20cba772bc5a1b529dfcef9dc1972db9021a1049"}, + {file = "pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77"}, + {file = "pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7"}, ] [package.dependencies] @@ -1958,13 +1953,13 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "8.3.3" +version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, - {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] @@ -1980,20 +1975,20 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments [[package]] name = "pytest-asyncio" -version = "0.24.0" +version = "0.25.0" description = "Pytest support for asyncio" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, - {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, + {file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"}, + {file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"}, ] [package.dependencies] pytest = ">=8.2,<9" [package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] @@ -2048,36 +2043,36 @@ Werkzeug = ">=2.0.0" [[package]] name = "pytest-httpx" -version = "0.30.0" +version = "0.34.0" description = "Send responses to httpx." optional = false python-versions = ">=3.9" files = [ - {file = "pytest-httpx-0.30.0.tar.gz", hash = "sha256:755b8edca87c974dd4f3605c374fda11db84631de3d163b99c0df5807023a19a"}, - {file = "pytest_httpx-0.30.0-py3-none-any.whl", hash = "sha256:6d47849691faf11d2532565d0c8e0e02b9f4ee730da31687feae315581d7520c"}, + {file = "pytest_httpx-0.34.0-py3-none-any.whl", hash = "sha256:42cf0a66f7b71b9111db2897e8b38a903abd33a27b11c48aff4a3c7650313af2"}, + {file = "pytest_httpx-0.34.0.tar.gz", hash = "sha256:3ca4b0975c0f93b985f17df19e76430c1086b5b0cce32b1af082d8901296a735"}, ] [package.dependencies] httpx = "==0.27.*" -pytest = ">=7,<9" +pytest = "==8.*" [package.extras] -testing = ["pytest-asyncio (==0.23.*)", "pytest-cov (==4.*)"] +testing = ["pytest-asyncio (==0.24.*)", "pytest-cov (==5.*)"] [[package]] name = "pytest-rerunfailures" -version = "14.0" +version = "15.0" description = "pytest plugin to re-run tests to eliminate flaky failures" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pytest-rerunfailures-14.0.tar.gz", hash = "sha256:4a400bcbcd3c7a4ad151ab8afac123d90eca3abe27f98725dc4d9702887d2e92"}, - {file = "pytest_rerunfailures-14.0-py3-none-any.whl", hash = "sha256:4197bdd2eaeffdbf50b5ea6e7236f47ff0e44d1def8dae08e409f536d84e7b32"}, + {file = "pytest-rerunfailures-15.0.tar.gz", hash = "sha256:2d9ac7baf59f4c13ac730b47f6fa80e755d1ba0581da45ce30b72fb3542b4474"}, + {file = "pytest_rerunfailures-15.0-py3-none-any.whl", hash = "sha256:dd150c4795c229ef44320adc9a0c0532c51b78bb7a6843a8c53556b9a611df1a"}, ] [package.dependencies] packaging = ">=17.1" -pytest = ">=7.2" +pytest = ">=7.4,<8.2.2 || >8.2.2" [[package]] name = "pytest-timeout" @@ -2095,23 +2090,24 @@ pytest = ">=7.0.0" [[package]] name = "python-daemon" -version = "3.0.1" +version = "3.1.2" description = "Library to implement a well-behaved Unix daemon process." optional = false -python-versions = ">=3" +python-versions = ">=3.7" files = [ - {file = "python-daemon-3.0.1.tar.gz", hash = "sha256:6c57452372f7eaff40934a1c03ad1826bf5e793558e87fef49131e6464b4dae5"}, - {file = "python_daemon-3.0.1-py3-none-any.whl", hash = "sha256:42bb848a3260a027fa71ad47ecd959e471327cb34da5965962edd5926229f341"}, + {file = "python_daemon-3.1.2-py3-none-any.whl", hash = "sha256:b906833cef63502994ad48e2eab213259ed9bb18d54fa8774dcba2ff7864cec6"}, + {file = "python_daemon-3.1.2.tar.gz", hash = "sha256:f7b04335adc473de877f5117e26d5f1142f4c9f7cd765408f0877757be5afbf4"}, ] [package.dependencies] -docutils = "*" lockfile = ">=0.10" -setuptools = ">=62.4.0" [package.extras] -devel = ["coverage", "docutils", "isort", "testscenarios (>=0.4)", "testtools", "twine"] -test = ["coverage", "docutils", "testscenarios (>=0.4)", "testtools"] +build = ["build", "changelog-chug", "docutils", "python-daemon[doc]", "wheel"] +devel = ["python-daemon[dist,test]"] +dist = ["python-daemon[build]", "twine"] +static-analysis = ["isort (>=5.13,<6.0)", "pip-check", "pycodestyle (>=2.12,<3.0)", "pydocstyle (>=6.3,<7.0)", "pyupgrade (>=3.17,<4.0)"] +test = ["coverage", "python-daemon[build,static-analysis]", "testscenarios (>=0.4)", "testtools"] [[package]] name = "python-dateutil" @@ -2326,13 +2322,13 @@ cffi = {version = "*", markers = "implementation_name == \"pypy\""} [[package]] name = "radixtarget" -version = "1.1.0.18" +version = "3.0.15" description = "Check whether an IP address belongs to a cloud provider" optional = false python-versions = "<4.0,>=3.9" files = [ - {file = "radixtarget-1.1.0.18-py3-none-any.whl", hash = "sha256:05e95de6afb0ee4dfa31c53bd25a34a193ae5bb46dc7624e0424bbcfed2c4cea"}, - {file = "radixtarget-1.1.0.18.tar.gz", hash = "sha256:1a3306891a22f7ff2c71d6cd42202af8852cdb4fb68e9a1e9a76a3f60aa98ab6"}, + {file = "radixtarget-3.0.15-py3-none-any.whl", hash = "sha256:1e1d0dd3e8742ffcfc42084eb238f31f6785626b876ab63a9f28a29e97bd3bb0"}, + {file = "radixtarget-3.0.15.tar.gz", hash = "sha256:dedfad3aea1e973f261b7bc0d8936423f59ae4d082648fd496c6cdfdfa069fea"}, ] [[package]] @@ -2491,134 +2487,138 @@ release = ["build", "towncrier", "twine"] test = ["commentjson", "packaging", "pytest"] [[package]] -name = "setproctitle" -version = "1.3.3" -description = "A Python module to customize the process title" +name = "ruff" +version = "0.8.3" +description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "setproctitle-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:897a73208da48db41e687225f355ce993167079eda1260ba5e13c4e53be7f754"}, - {file = "setproctitle-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c331e91a14ba4076f88c29c777ad6b58639530ed5b24b5564b5ed2fd7a95452"}, - {file = "setproctitle-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbbd6c7de0771c84b4aa30e70b409565eb1fc13627a723ca6be774ed6b9d9fa3"}, - {file = "setproctitle-1.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c05ac48ef16ee013b8a326c63e4610e2430dbec037ec5c5b58fcced550382b74"}, - {file = "setproctitle-1.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1342f4fdb37f89d3e3c1c0a59d6ddbedbde838fff5c51178a7982993d238fe4f"}, - {file = "setproctitle-1.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc74e84fdfa96821580fb5e9c0b0777c1c4779434ce16d3d62a9c4d8c710df39"}, - {file = "setproctitle-1.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9617b676b95adb412bb69645d5b077d664b6882bb0d37bfdafbbb1b999568d85"}, - {file = "setproctitle-1.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6a249415f5bb88b5e9e8c4db47f609e0bf0e20a75e8d744ea787f3092ba1f2d0"}, - {file = "setproctitle-1.3.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:38da436a0aaace9add67b999eb6abe4b84397edf4a78ec28f264e5b4c9d53cd5"}, - {file = "setproctitle-1.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:da0d57edd4c95bf221b2ebbaa061e65b1788f1544977288bdf95831b6e44e44d"}, - {file = "setproctitle-1.3.3-cp310-cp310-win32.whl", hash = "sha256:a1fcac43918b836ace25f69b1dca8c9395253ad8152b625064415b1d2f9be4fb"}, - {file = "setproctitle-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:200620c3b15388d7f3f97e0ae26599c0c378fdf07ae9ac5a13616e933cbd2086"}, - {file = "setproctitle-1.3.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:334f7ed39895d692f753a443102dd5fed180c571eb6a48b2a5b7f5b3564908c8"}, - {file = "setproctitle-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:950f6476d56ff7817a8fed4ab207727fc5260af83481b2a4b125f32844df513a"}, - {file = "setproctitle-1.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:195c961f54a09eb2acabbfc90c413955cf16c6e2f8caa2adbf2237d1019c7dd8"}, - {file = "setproctitle-1.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f05e66746bf9fe6a3397ec246fe481096664a9c97eb3fea6004735a4daf867fd"}, - {file = "setproctitle-1.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5901a31012a40ec913265b64e48c2a4059278d9f4e6be628441482dd13fb8b5"}, - {file = "setproctitle-1.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64286f8a995f2cd934082b398fc63fca7d5ffe31f0e27e75b3ca6b4efda4e353"}, - {file = "setproctitle-1.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:184239903bbc6b813b1a8fc86394dc6ca7d20e2ebe6f69f716bec301e4b0199d"}, - {file = "setproctitle-1.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:664698ae0013f986118064b6676d7dcd28fefd0d7d5a5ae9497cbc10cba48fa5"}, - {file = "setproctitle-1.3.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e5119a211c2e98ff18b9908ba62a3bd0e3fabb02a29277a7232a6fb4b2560aa0"}, - {file = "setproctitle-1.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:417de6b2e214e837827067048f61841f5d7fc27926f2e43954567094051aff18"}, - {file = "setproctitle-1.3.3-cp311-cp311-win32.whl", hash = "sha256:6a143b31d758296dc2f440175f6c8e0b5301ced3b0f477b84ca43cdcf7f2f476"}, - {file = "setproctitle-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a680d62c399fa4b44899094027ec9a1bdaf6f31c650e44183b50d4c4d0ccc085"}, - {file = "setproctitle-1.3.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d4460795a8a7a391e3567b902ec5bdf6c60a47d791c3b1d27080fc203d11c9dc"}, - {file = "setproctitle-1.3.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bdfd7254745bb737ca1384dee57e6523651892f0ea2a7344490e9caefcc35e64"}, - {file = "setproctitle-1.3.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:477d3da48e216d7fc04bddab67b0dcde633e19f484a146fd2a34bb0e9dbb4a1e"}, - {file = "setproctitle-1.3.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ab2900d111e93aff5df9fddc64cf51ca4ef2c9f98702ce26524f1acc5a786ae7"}, - {file = "setproctitle-1.3.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:088b9efc62d5aa5d6edf6cba1cf0c81f4488b5ce1c0342a8b67ae39d64001120"}, - {file = "setproctitle-1.3.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6d50252377db62d6a0bb82cc898089916457f2db2041e1d03ce7fadd4a07381"}, - {file = "setproctitle-1.3.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:87e668f9561fd3a457ba189edfc9e37709261287b52293c115ae3487a24b92f6"}, - {file = "setproctitle-1.3.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:287490eb90e7a0ddd22e74c89a92cc922389daa95babc833c08cf80c84c4df0a"}, - {file = "setproctitle-1.3.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe1c49486109f72d502f8be569972e27f385fe632bd8895f4730df3c87d5ac8"}, - {file = "setproctitle-1.3.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4a6ba2494a6449b1f477bd3e67935c2b7b0274f2f6dcd0f7c6aceae10c6c6ba3"}, - {file = "setproctitle-1.3.3-cp312-cp312-win32.whl", hash = "sha256:2df2b67e4b1d7498632e18c56722851ba4db5d6a0c91aaf0fd395111e51cdcf4"}, - {file = "setproctitle-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:f38d48abc121263f3b62943f84cbaede05749047e428409c2c199664feb6abc7"}, - {file = "setproctitle-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:816330675e3504ae4d9a2185c46b573105d2310c20b19ea2b4596a9460a4f674"}, - {file = "setproctitle-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f960bc22d8d8e4ac886d1e2e21ccbd283adcf3c43136161c1ba0fa509088e0"}, - {file = "setproctitle-1.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e6e7adff74796ef12753ff399491b8827f84f6c77659d71bd0b35870a17d8f"}, - {file = "setproctitle-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53bc0d2358507596c22b02db079618451f3bd720755d88e3cccd840bafb4c41c"}, - {file = "setproctitle-1.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad6d20f9541f5f6ac63df553b6d7a04f313947f550eab6a61aa758b45f0d5657"}, - {file = "setproctitle-1.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c1c84beab776b0becaa368254801e57692ed749d935469ac10e2b9b825dbdd8e"}, - {file = "setproctitle-1.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:507e8dc2891021350eaea40a44ddd887c9f006e6b599af8d64a505c0f718f170"}, - {file = "setproctitle-1.3.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b1067647ac7aba0b44b591936118a22847bda3c507b0a42d74272256a7a798e9"}, - {file = "setproctitle-1.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2e71f6365744bf53714e8bd2522b3c9c1d83f52ffa6324bd7cbb4da707312cd8"}, - {file = "setproctitle-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:7f1d36a1e15a46e8ede4e953abb104fdbc0845a266ec0e99cc0492a4364f8c44"}, - {file = "setproctitle-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c9a402881ec269d0cc9c354b149fc29f9ec1a1939a777f1c858cdb09c7a261df"}, - {file = "setproctitle-1.3.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ff814dea1e5c492a4980e3e7d094286077054e7ea116cbeda138819db194b2cd"}, - {file = "setproctitle-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:accb66d7b3ccb00d5cd11d8c6e07055a4568a24c95cf86109894dcc0c134cc89"}, - {file = "setproctitle-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554eae5a5b28f02705b83a230e9d163d645c9a08914c0ad921df363a07cf39b1"}, - {file = "setproctitle-1.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a911b26264dbe9e8066c7531c0591cfab27b464459c74385b276fe487ca91c12"}, - {file = "setproctitle-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2982efe7640c4835f7355fdb4da313ad37fb3b40f5c69069912f8048f77b28c8"}, - {file = "setproctitle-1.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df3f4274b80709d8bcab2f9a862973d453b308b97a0b423a501bcd93582852e3"}, - {file = "setproctitle-1.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:af2c67ae4c795d1674a8d3ac1988676fa306bcfa1e23fddb5e0bd5f5635309ca"}, - {file = "setproctitle-1.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:af4061f67fd7ec01624c5e3c21f6b7af2ef0e6bab7fbb43f209e6506c9ce0092"}, - {file = "setproctitle-1.3.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:37a62cbe16d4c6294e84670b59cf7adcc73faafe6af07f8cb9adaf1f0e775b19"}, - {file = "setproctitle-1.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a83ca086fbb017f0d87f240a8f9bbcf0809f3b754ee01cec928fff926542c450"}, - {file = "setproctitle-1.3.3-cp38-cp38-win32.whl", hash = "sha256:059f4ce86f8cc92e5860abfc43a1dceb21137b26a02373618d88f6b4b86ba9b2"}, - {file = "setproctitle-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:ab92e51cd4a218208efee4c6d37db7368fdf182f6e7ff148fb295ecddf264287"}, - {file = "setproctitle-1.3.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c7951820b77abe03d88b114b998867c0f99da03859e5ab2623d94690848d3e45"}, - {file = "setproctitle-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5bc94cf128676e8fac6503b37763adb378e2b6be1249d207630f83fc325d9b11"}, - {file = "setproctitle-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f5d9027eeda64d353cf21a3ceb74bb1760bd534526c9214e19f052424b37e42"}, - {file = "setproctitle-1.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e4a8104db15d3462e29d9946f26bed817a5b1d7a47eabca2d9dc2b995991503"}, - {file = "setproctitle-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c32c41ace41f344d317399efff4cffb133e709cec2ef09c99e7a13e9f3b9483c"}, - {file = "setproctitle-1.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbf16381c7bf7f963b58fb4daaa65684e10966ee14d26f5cc90f07049bfd8c1e"}, - {file = "setproctitle-1.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e18b7bd0898398cc97ce2dfc83bb192a13a087ef6b2d5a8a36460311cb09e775"}, - {file = "setproctitle-1.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69d565d20efe527bd8a9b92e7f299ae5e73b6c0470f3719bd66f3cd821e0d5bd"}, - {file = "setproctitle-1.3.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ddedd300cd690a3b06e7eac90ed4452348b1348635777ce23d460d913b5b63c3"}, - {file = "setproctitle-1.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:415bfcfd01d1fbf5cbd75004599ef167a533395955305f42220a585f64036081"}, - {file = "setproctitle-1.3.3-cp39-cp39-win32.whl", hash = "sha256:21112fcd2195d48f25760f0eafa7a76510871bbb3b750219310cf88b04456ae3"}, - {file = "setproctitle-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:5a740f05d0968a5a17da3d676ce6afefebeeeb5ce137510901bf6306ba8ee002"}, - {file = "setproctitle-1.3.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6b9e62ddb3db4b5205c0321dd69a406d8af9ee1693529d144e86bd43bcb4b6c0"}, - {file = "setproctitle-1.3.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e3b99b338598de0bd6b2643bf8c343cf5ff70db3627af3ca427a5e1a1a90dd9"}, - {file = "setproctitle-1.3.3-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ae9a02766dad331deb06855fb7a6ca15daea333b3967e214de12cfae8f0ef5"}, - {file = "setproctitle-1.3.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:200ede6fd11233085ba9b764eb055a2a191fb4ffb950c68675ac53c874c22e20"}, - {file = "setproctitle-1.3.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0d3a953c50776751e80fe755a380a64cb14d61e8762bd43041ab3f8cc436092f"}, - {file = "setproctitle-1.3.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5e08e232b78ba3ac6bc0d23ce9e2bee8fad2be391b7e2da834fc9a45129eb87"}, - {file = "setproctitle-1.3.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1da82c3e11284da4fcbf54957dafbf0655d2389cd3d54e4eaba636faf6d117a"}, - {file = "setproctitle-1.3.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:aeaa71fb9568ebe9b911ddb490c644fbd2006e8c940f21cb9a1e9425bd709574"}, - {file = "setproctitle-1.3.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:59335d000c6250c35989394661eb6287187854e94ac79ea22315469ee4f4c244"}, - {file = "setproctitle-1.3.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3ba57029c9c50ecaf0c92bb127224cc2ea9fda057b5d99d3f348c9ec2855ad3"}, - {file = "setproctitle-1.3.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d876d355c53d975c2ef9c4f2487c8f83dad6aeaaee1b6571453cb0ee992f55f6"}, - {file = "setproctitle-1.3.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:224602f0939e6fb9d5dd881be1229d485f3257b540f8a900d4271a2c2aa4e5f4"}, - {file = "setproctitle-1.3.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d7f27e0268af2d7503386e0e6be87fb9b6657afd96f5726b733837121146750d"}, - {file = "setproctitle-1.3.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5e7266498cd31a4572378c61920af9f6b4676a73c299fce8ba93afd694f8ae7"}, - {file = "setproctitle-1.3.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33c5609ad51cd99d388e55651b19148ea99727516132fb44680e1f28dd0d1de9"}, - {file = "setproctitle-1.3.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:eae8988e78192fd1a3245a6f4f382390b61bce6cfcc93f3809726e4c885fa68d"}, - {file = "setproctitle-1.3.3.tar.gz", hash = "sha256:c913e151e7ea01567837ff037a23ca8740192880198b7fbb90b16d181607caae"}, + {file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"}, + {file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"}, + {file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"}, + {file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"}, + {file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"}, + {file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"}, + {file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"}, ] -[package.extras] -test = ["pytest"] - [[package]] -name = "setuptools" -version = "75.2.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" +name = "setproctitle" +version = "1.3.4" +description = "A Python module to customize the process title" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-75.2.0-py3-none-any.whl", hash = "sha256:a7fcb66f68b4d9e8e66b42f9876150a3371558f98fa32222ffaa5bced76406f8"}, - {file = "setuptools-75.2.0.tar.gz", hash = "sha256:753bb6ebf1f465a1912e19ed1d41f403a79173a9acf66a42e7e6aec45c3c16ec"}, + {file = "setproctitle-1.3.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0f6661a69c68349172ba7b4d5dd65fec2b0917abc99002425ad78c3e58cf7595"}, + {file = "setproctitle-1.3.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:754bac5e470adac7f7ec2239c485cd0b75f8197ca8a5b86ffb20eb3a3676cc42"}, + {file = "setproctitle-1.3.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7bc7088c15150745baf66db62a4ced4507d44419eb66207b609f91b64a682af"}, + {file = "setproctitle-1.3.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a46ef3ecf61e4840fbc1145fdd38acf158d0da7543eda7b773ed2b30f75c2830"}, + {file = "setproctitle-1.3.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffcb09d5c0ffa043254ec9a734a73f3791fec8bf6333592f906bb2e91ed2af1a"}, + {file = "setproctitle-1.3.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06c16b7a91cdc5d700271899e4383384a61aae83a3d53d0e2e5a266376083342"}, + {file = "setproctitle-1.3.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9f9732e59863eaeedd3feef94b2b216cb86d40dda4fad2d0f0aaec3b31592716"}, + {file = "setproctitle-1.3.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e152f4ab9ea1632b5fecdd87cee354f2b2eb6e2dfc3aceb0eb36a01c1e12f94c"}, + {file = "setproctitle-1.3.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:020ea47a79b2bbd7bd7b94b85ca956ba7cb026e82f41b20d2e1dac4008cead25"}, + {file = "setproctitle-1.3.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c52b12b10e4057fc302bd09cb3e3f28bb382c30c044eb3396e805179a8260e4"}, + {file = "setproctitle-1.3.4-cp310-cp310-win32.whl", hash = "sha256:a65a147f545f3fac86f11acb2d0b316d3e78139a9372317b7eb50561b2817ba0"}, + {file = "setproctitle-1.3.4-cp310-cp310-win_amd64.whl", hash = "sha256:66821fada6426998762a3650a37fba77e814a249a95b1183011070744aff47f6"}, + {file = "setproctitle-1.3.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0f749f07002c2d6fecf37cedc43207a88e6c651926a470a5f229070cf791879"}, + {file = "setproctitle-1.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:90ea8d302a5d30b948451d146e94674a3c5b020cc0ced9a1c28f8ddb0f203a5d"}, + {file = "setproctitle-1.3.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f859c88193ed466bee4eb9d45fbc29d2253e6aa3ccd9119c9a1d8d95f409a60d"}, + {file = "setproctitle-1.3.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3afa5a0ed08a477ded239c05db14c19af585975194a00adf594d48533b23701"}, + {file = "setproctitle-1.3.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a78fce9018cc3e9a772b6537bbe3fe92380acf656c9f86db2f45e685af376e"}, + {file = "setproctitle-1.3.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d758e2eed2643afac5f2881542fbb5aa97640b54be20d0a5ed0691d02f0867d"}, + {file = "setproctitle-1.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ef133a1a2ee378d549048a12d56f4ef0e2b9113b0b25b6b77821e9af94d50634"}, + {file = "setproctitle-1.3.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d2a154b79d5fb42d1eff06e05e22f0e8091261d877dd47b37d31352b74ecc37"}, + {file = "setproctitle-1.3.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:202eae632815571297833876a0f407d0d9c7ad9d843b38adbe687fe68c5192ee"}, + {file = "setproctitle-1.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2b0080819859e80a7776ac47cf6accb4b7ad313baf55fabac89c000480dcd103"}, + {file = "setproctitle-1.3.4-cp311-cp311-win32.whl", hash = "sha256:9c9d7d1267dee8c6627963d9376efa068858cfc8f573c083b1b6a2d297a8710f"}, + {file = "setproctitle-1.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:475986ddf6df65d619acd52188336a20f616589403f5a5ceb3fc70cdc137037a"}, + {file = "setproctitle-1.3.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d06990dcfcd41bb3543c18dd25c8476fbfe1f236757f42fef560f6aa03ac8dfc"}, + {file = "setproctitle-1.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:317218c9d8b17a010ab2d2f0851e8ef584077a38b1ba2b7c55c9e44e79a61e73"}, + {file = "setproctitle-1.3.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb5fefb53b9d9f334a5d9ec518a36b92a10b936011ac8a6b6dffd60135f16459"}, + {file = "setproctitle-1.3.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0855006261635e8669646c7c304b494b6df0a194d2626683520103153ad63cc9"}, + {file = "setproctitle-1.3.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a88e466fcaee659679c1d64dcb2eddbcb4bfadffeb68ba834d9c173a25b6184"}, + {file = "setproctitle-1.3.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f963b6ed8ba33eda374a98d979e8a0eaf21f891b6e334701693a2c9510613c4c"}, + {file = "setproctitle-1.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:122c2e05697fa91f5d23f00bbe98a9da1bd457b32529192e934095fadb0853f1"}, + {file = "setproctitle-1.3.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1bba0a866f5895d5b769d8c36b161271c7fd407e5065862ab80ff91c29fbe554"}, + {file = "setproctitle-1.3.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:97f1f861998e326e640708488c442519ad69046374b2c3fe9bcc9869b387f23c"}, + {file = "setproctitle-1.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:726aee40357d4bdb70115442cb85ccc8e8bc554fc0bbbaa3a57cbe81df42287d"}, + {file = "setproctitle-1.3.4-cp312-cp312-win32.whl", hash = "sha256:04d6ba8b816dbb0bfd62000b0c3e583160893e6e8c4233e1dca1a9ae4d95d924"}, + {file = "setproctitle-1.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:9c76e43cb351ba8887371240b599925cdf3ecececc5dfb7125c71678e7722c55"}, + {file = "setproctitle-1.3.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6e3b177e634aa6bbbfbf66d097b6d1cdb80fc60e912c7d8bace2e45699c07dd"}, + {file = "setproctitle-1.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b17655a5f245b416e127e02087ea6347a48821cc4626bc0fd57101bfcd88afc"}, + {file = "setproctitle-1.3.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa5057a86df920faab8ee83960b724bace01a3231eb8e3f2c93d78283504d598"}, + {file = "setproctitle-1.3.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149fdfb8a26a555780c4ce53c92e6d3c990ef7b30f90a675eca02e83c6d5f76d"}, + {file = "setproctitle-1.3.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ded03546938a987f463c68ab98d683af87a83db7ac8093bbc179e77680be5ba2"}, + {file = "setproctitle-1.3.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ab9f5b7f2bbc1754bc6292d9a7312071058e5a891b0391e6d13b226133f36aa"}, + {file = "setproctitle-1.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0b19813c852566fa031902124336fa1f080c51e262fc90266a8c3d65ca47b74c"}, + {file = "setproctitle-1.3.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:db78b645dc63c0ccffca367a498f3b13492fb106a2243a1e998303ba79c996e2"}, + {file = "setproctitle-1.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b669aaac70bd9f03c070270b953f78d9ee56c4af6f0ff9f9cd3e6d1878c10b40"}, + {file = "setproctitle-1.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6dc3d656702791565994e64035a208be56b065675a5bc87b644c657d6d9e2232"}, + {file = "setproctitle-1.3.4-cp313-cp313-win32.whl", hash = "sha256:091f682809a4d12291cf0205517619d2e7014986b7b00ebecfde3d76f8ae5a8f"}, + {file = "setproctitle-1.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:adcd6ba863a315702184d92d3d3bbff290514f24a14695d310f02ae5e28bd1f7"}, + {file = "setproctitle-1.3.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:acf41cf91bbc5a36d1fa4455a818bb02bf2a4ccfed2f892ba166ba2fcbb0ec8a"}, + {file = "setproctitle-1.3.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ceb3ce3262b0e8e088e4117175591b7a82b3bdc5e52e33b1e74778b5fb53fd38"}, + {file = "setproctitle-1.3.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b2ef636a6a25fe7f3d5a064bea0116b74a4c8c7df9646b17dc7386c439a26cf"}, + {file = "setproctitle-1.3.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28b8614de08679ae95bc4e8d6daaef6b61afdf027fa0d23bf13d619000286b3c"}, + {file = "setproctitle-1.3.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24f3c8be826a7d44181eac2269b15b748b76d98cd9a539d4c69f09321dcb5c12"}, + {file = "setproctitle-1.3.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc9d79b1bf833af63b7c720a6604eb16453ac1ad4e718eb8b59d1f97d986b98c"}, + {file = "setproctitle-1.3.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fb693000b65842c85356b667d057ae0d0bac6519feca7e1c437cc2cfeb0afc59"}, + {file = "setproctitle-1.3.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:a166251b8fbc6f2755e2ce9d3c11e9edb0c0c7d2ed723658ff0161fbce26ac1c"}, + {file = "setproctitle-1.3.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:0361428e6378911a378841509c56ba472d991cbed1a7e3078ec0cacc103da44a"}, + {file = "setproctitle-1.3.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:62d66e0423e3bd520b4c897063506b309843a8d07343fbfad04197e91a4edd28"}, + {file = "setproctitle-1.3.4-cp38-cp38-win32.whl", hash = "sha256:5edd01909348f3b0b2da329836d6b5419cd4869fec2e118e8ff3275b38af6267"}, + {file = "setproctitle-1.3.4-cp38-cp38-win_amd64.whl", hash = "sha256:59e0dda9ad245921af0328035a961767026e1fa94bb65957ab0db0a0491325d6"}, + {file = "setproctitle-1.3.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bdaaa81a6e95a0a19fba0285f10577377f3503ae4e9988b403feba79da3e2f80"}, + {file = "setproctitle-1.3.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ee5b19a2d794463bcc19153dfceede7beec784b4cf7967dec0bc0fc212ab3a3"}, + {file = "setproctitle-1.3.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3058a1bb0c767b3a6ccbb38b27ef870af819923eb732e21e44a3f300370fe159"}, + {file = "setproctitle-1.3.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a97d37ee4fe0d1c6e87d2a97229c27a88787a8f4ebfbdeee95f91b818e52efe"}, + {file = "setproctitle-1.3.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e61dd7d05da11fc69bb86d51f1e0ee08f74dccf3ecf884c94de41135ffdc75d"}, + {file = "setproctitle-1.3.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eb115d53dc2a1299ae72f1119c96a556db36073bacb6da40c47ece5db0d9587"}, + {file = "setproctitle-1.3.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:342570716e2647a51ea859b8a9126da9dc1a96a0153c9c0a3514effd60ab57ad"}, + {file = "setproctitle-1.3.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0ad212ae2b03951367a69584af034579b34e1e4199a75d377ef9f8e08ee299b1"}, + {file = "setproctitle-1.3.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4afcb38e22122465013f4621b7e9ff8d42a7a48ae0ffeb94133a806cb91b4aad"}, + {file = "setproctitle-1.3.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:30bb223e6c3f95ad9e9bb2a113292759e947d1cfd60dbd4adb55851c370006b2"}, + {file = "setproctitle-1.3.4-cp39-cp39-win32.whl", hash = "sha256:5f0521ed3bb9f02e9486573ea95e2062cd6bf036fa44e640bd54a06f22d85f35"}, + {file = "setproctitle-1.3.4-cp39-cp39-win_amd64.whl", hash = "sha256:0baadeb27f9e97e65922b4151f818b19c311d30b9efdb62af0e53b3db4006ce2"}, + {file = "setproctitle-1.3.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:939d364a187b2adfbf6ae488664277e717d56c7951a4ddeb4f23b281bc50bfe5"}, + {file = "setproctitle-1.3.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb8a6a19be0cbf6da6fcbf3698b76c8af03fe83e4bd77c96c3922be3b88bf7da"}, + {file = "setproctitle-1.3.4-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:779006f9e1aade9522a40e8d9635115ab15dd82b7af8e655967162e9c01e2573"}, + {file = "setproctitle-1.3.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5519f2a7b8c535b0f1f77b30441476571373add72008230c81211ee17b423b57"}, + {file = "setproctitle-1.3.4-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:743836d484151334ebba1490d6907ca9e718fe815dcd5756f2a01bc3067d099c"}, + {file = "setproctitle-1.3.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abda20aff8d1751e48d7967fa8945fef38536b82366c49be39b83678d4be3893"}, + {file = "setproctitle-1.3.4-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a2041b5788ce52f218b5be94af458e04470f997ab46fdebd57cf0b8374cc20e"}, + {file = "setproctitle-1.3.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2c3b1ce68746557aa6e6f4547e76883925cdc7f8d7c7a9f518acd203f1265ca5"}, + {file = "setproctitle-1.3.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:0b6a4cbabf024cb263a45bdef425760f14470247ff223f0ec51699ca9046c0fe"}, + {file = "setproctitle-1.3.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e55d7ecc68bdc80de5a553691a3ed260395d5362c19a266cf83cbb4e046551f"}, + {file = "setproctitle-1.3.4-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02ca3802902d91a89957f79da3ec44b25b5804c88026362cb85eea7c1fbdefd1"}, + {file = "setproctitle-1.3.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:47669fc8ed8b27baa2d698104732234b5389f6a59c37c046f6bcbf9150f7a94e"}, + {file = "setproctitle-1.3.4.tar.gz", hash = "sha256:3b40d32a3e1f04e94231ed6dfee0da9e43b4f9c6b5450d53e6dd7754c34e0c50"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] +test = ["pytest"] [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] @@ -2654,6 +2654,24 @@ files = [ {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, ] +[[package]] +name = "starlette" +version = "0.41.3" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7"}, + {file = "starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + [[package]] name = "tabulate" version = "0.8.10" @@ -2691,13 +2709,43 @@ testing = ["mypy", "pytest", "pytest-gitignore", "pytest-mock", "responses", "ru [[package]] name = "tomli" -version = "2.0.2" +version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, - {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] @@ -2713,22 +2761,22 @@ files = [ [[package]] name = "tornado" -version = "6.4.1" +version = "6.4.2" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = false python-versions = ">=3.8" files = [ - {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, - {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, - {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, - {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, - {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, + {file = "tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1"}, + {file = "tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803"}, + {file = "tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec"}, + {file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946"}, + {file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf"}, + {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634"}, + {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73"}, + {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c"}, + {file = "tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482"}, + {file = "tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38"}, + {file = "tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b"}, ] [[package]] @@ -2770,6 +2818,25 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "uvicorn" +version = "0.34.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.9" +files = [ + {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, + {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + [[package]] name = "verspec" version = "0.1.0" @@ -2786,13 +2853,13 @@ test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"] [[package]] name = "virtualenv" -version = "20.27.0" +version = "20.28.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" files = [ - {file = "virtualenv-20.27.0-py3-none-any.whl", hash = "sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655"}, - {file = "virtualenv-20.27.0.tar.gz", hash = "sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2"}, + {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, + {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, ] [package.dependencies] @@ -2806,41 +2873,41 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "watchdog" -version = "5.0.3" +version = "6.0.0" description = "Filesystem events monitoring" optional = false python-versions = ">=3.9" files = [ - {file = "watchdog-5.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:85527b882f3facda0579bce9d743ff7f10c3e1e0db0a0d0e28170a7d0e5ce2ea"}, - {file = "watchdog-5.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:53adf73dcdc0ef04f7735066b4a57a4cd3e49ef135daae41d77395f0b5b692cb"}, - {file = "watchdog-5.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e25adddab85f674acac303cf1f5835951345a56c5f7f582987d266679979c75b"}, - {file = "watchdog-5.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f01f4a3565a387080dc49bdd1fefe4ecc77f894991b88ef927edbfa45eb10818"}, - {file = "watchdog-5.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91b522adc25614cdeaf91f7897800b82c13b4b8ac68a42ca959f992f6990c490"}, - {file = "watchdog-5.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d52db5beb5e476e6853da2e2d24dbbbed6797b449c8bf7ea118a4ee0d2c9040e"}, - {file = "watchdog-5.0.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d11b07c64f63f49876e0ab8042ae034674c8653bfcdaa8c4b32e71cfff87e8"}, - {file = "watchdog-5.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:349c9488e1d85d0a58e8cb14222d2c51cbc801ce11ac3936ab4c3af986536926"}, - {file = "watchdog-5.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:53a3f10b62c2d569e260f96e8d966463dec1a50fa4f1b22aec69e3f91025060e"}, - {file = "watchdog-5.0.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:950f531ec6e03696a2414b6308f5c6ff9dab7821a768c9d5788b1314e9a46ca7"}, - {file = "watchdog-5.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae6deb336cba5d71476caa029ceb6e88047fc1dc74b62b7c4012639c0b563906"}, - {file = "watchdog-5.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1021223c08ba8d2d38d71ec1704496471ffd7be42cfb26b87cd5059323a389a1"}, - {file = "watchdog-5.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:752fb40efc7cc8d88ebc332b8f4bcbe2b5cc7e881bccfeb8e25054c00c994ee3"}, - {file = "watchdog-5.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a2e8f3f955d68471fa37b0e3add18500790d129cc7efe89971b8a4cc6fdeb0b2"}, - {file = "watchdog-5.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b8ca4d854adcf480bdfd80f46fdd6fb49f91dd020ae11c89b3a79e19454ec627"}, - {file = "watchdog-5.0.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:90a67d7857adb1d985aca232cc9905dd5bc4803ed85cfcdcfcf707e52049eda7"}, - {file = "watchdog-5.0.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:720ef9d3a4f9ca575a780af283c8fd3a0674b307651c1976714745090da5a9e8"}, - {file = "watchdog-5.0.3-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:223160bb359281bb8e31c8f1068bf71a6b16a8ad3d9524ca6f523ac666bb6a1e"}, - {file = "watchdog-5.0.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:560135542c91eaa74247a2e8430cf83c4342b29e8ad4f520ae14f0c8a19cfb5b"}, - {file = "watchdog-5.0.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dd021efa85970bd4824acacbb922066159d0f9e546389a4743d56919b6758b91"}, - {file = "watchdog-5.0.3-py3-none-manylinux2014_armv7l.whl", hash = "sha256:78864cc8f23dbee55be34cc1494632a7ba30263951b5b2e8fc8286b95845f82c"}, - {file = "watchdog-5.0.3-py3-none-manylinux2014_i686.whl", hash = "sha256:1e9679245e3ea6498494b3028b90c7b25dbb2abe65c7d07423ecfc2d6218ff7c"}, - {file = "watchdog-5.0.3-py3-none-manylinux2014_ppc64.whl", hash = "sha256:9413384f26b5d050b6978e6fcd0c1e7f0539be7a4f1a885061473c5deaa57221"}, - {file = "watchdog-5.0.3-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:294b7a598974b8e2c6123d19ef15de9abcd282b0fbbdbc4d23dfa812959a9e05"}, - {file = "watchdog-5.0.3-py3-none-manylinux2014_s390x.whl", hash = "sha256:26dd201857d702bdf9d78c273cafcab5871dd29343748524695cecffa44a8d97"}, - {file = "watchdog-5.0.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:0f9332243355643d567697c3e3fa07330a1d1abf981611654a1f2bf2175612b7"}, - {file = "watchdog-5.0.3-py3-none-win32.whl", hash = "sha256:c66f80ee5b602a9c7ab66e3c9f36026590a0902db3aea414d59a2f55188c1f49"}, - {file = "watchdog-5.0.3-py3-none-win_amd64.whl", hash = "sha256:f00b4cf737f568be9665563347a910f8bdc76f88c2970121c86243c8cfdf90e9"}, - {file = "watchdog-5.0.3-py3-none-win_ia64.whl", hash = "sha256:49f4d36cb315c25ea0d946e018c01bb028048023b9e103d3d3943f58e109dd45"}, - {file = "watchdog-5.0.3.tar.gz", hash = "sha256:108f42a7f0345042a854d4d0ad0834b741d421330d5f575b81cb27b883500176"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, ] [package.extras] @@ -3070,13 +3137,13 @@ files = [ [[package]] name = "zipp" -version = "3.20.2" +version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, - {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] [package.extras] @@ -3090,4 +3157,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "03dad7bf2fb115c7b122eea8e1f1694590237d1b929b453f7dd65e2c2e757d36" +content-hash = "0145b8e3c345caf43d941534dbfae68125f0006a29c033857311c392ff73672f" diff --git a/pyproject.toml b/pyproject.toml index 278ac25b41..7b463077d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bbot" -version = "2.2.0" +version = "2.3.0" description = "OSINT automation for hackers." authors = [ "TheTechromancer", @@ -11,7 +11,7 @@ readme = "README.md" repository = "https://github.com/blacklanternsecurity/bbot" homepage = "https://github.com/blacklanternsecurity/bbot" documentation = "https://www.blacklanternsecurity.com/bbot/" -keywords = ["python", "cli", "automation", "osint", "neo4j", "scanner", "python-library", "hacking", "recursion", "pentesting", "recon", "command-line-tool", "bugbounty", "subdomains", "security-tools", "subdomain-scanner", "osint-framework", "attack-surface", "subdomain-enumeration", "osint-tool"] +keywords = ["python", "cli", "automation", "osint", "threat-intel", "intelligence", "neo4j", "scanner", "python-library", "hacking", "recursion", "pentesting", "recon", "command-line-tool", "bugbounty", "subdomains", "security-tools", "subdomain-scanner", "osint-framework", "attack-surface", "subdomain-enumeration", "osint-tool"] classifiers = [ "Operating System :: POSIX :: Linux", "Topic :: Security", @@ -48,30 +48,32 @@ socksio = "^1.0.0" jinja2 = "^3.1.3" regex = "^2024.4.16" unidecode = "^1.3.8" -radixtarget = "^1.0.0.15" -cloudcheck = "^5.0.0.350" mmh3 = ">=4.1,<6.0" setproctitle = "^1.3.3" yara-python = "^4.5.1" pyzmq = "^26.0.3" httpx = "^0.27.0" puremagic = "^1.28" +radixtarget = "^3.0.13" +cloudcheck = "^7.0.12" +orjson = "^3.10.12" [tool.poetry.group.dev.dependencies] -flake8 = ">=6,<8" poetry-dynamic-versioning = ">=0.21.4,<1.5.0" urllib3 = "^2.0.2" werkzeug = ">=2.3.4,<4.0.0" pytest-env = ">=0.8.2,<1.2.0" pre-commit = ">=3.4,<5.0" -black = "^24.1.1" pytest-cov = ">=5,<7" -pytest-rerunfailures = "^14.0" +pytest-rerunfailures = ">=14,<16" pytest-timeout = "^2.3.1" -pytest-httpx = "^0.30.0" pytest-httpserver = "^1.0.11" pytest = "^8.3.1" -pytest-asyncio = "0.24.0" +pytest-asyncio = "0.25.0" +uvicorn = ">=0.32,<0.35" +fastapi = "^0.115.5" +pytest-httpx = ">=0.33,<0.35" +ruff = "^0.8.0" [tool.poetry.group.docs.dependencies] mkdocs = "^1.5.2" @@ -94,14 +96,19 @@ asyncio_default_fixture_loop_scope = "function" requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"] build-backend = "poetry_dynamic_versioning.backend" -[tool.black] +[tool.codespell] +ignore-words-list = "bu,cna,couldn,dialin,nd,ned,thirdparty" +skip = "./docs/javascripts/vega*.js,./bbot/wordlists/*" + +[tool.ruff] line-length = 119 -extend-exclude = "(test_step_1/test_manager_*)" +format.exclude = ["bbot/test/test_step_1/test_manager_*"] +lint.ignore = ["E402", "E711", "E713", "E721", "E741", "F401", "F403", "F405"] [tool.poetry-dynamic-versioning] enable = true metadata = false -format-jinja = 'v2.2.0{% if branch == "dev" %}.{{ distance }}rc{% endif %}' +format-jinja = 'v2.3.0{% if branch == "dev" %}.{{ distance }}rc{% endif %}' [tool.poetry-dynamic-versioning.substitution] files = ["*/__init__.py"]