diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c45242922..2d42216e9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,6 +27,7 @@ repos: - id: pretty-format-json args: [--autofix] + exclude: ^src/web_interface/static/package-lock.json - repo: https://github.com/jumanjihouse/pre-commit-hooks rev: 2.1.5 diff --git a/docsrc/conf.py b/docsrc/conf.py index e8acf6f98..9e4f899bd 100644 --- a/docsrc/conf.py +++ b/docsrc/conf.py @@ -77,7 +77,10 @@ 'flask_restx', 'flask_security', 'flask_sqlalchemy', + 'gql', + 'graphql', 'itsdangerous', + 'json5', 'lief', 'magic', 'markupsafe', diff --git a/src/config.py b/src/config.py index 599bf6dda..3f8b67d5d 100644 --- a/src/config.py +++ b/src/config.py @@ -108,6 +108,13 @@ class Authentication(BaseModel): radare2_url: str + hasura: Frontend.Hasura + + class Hasura(BaseModel): + model_config = ConfigDict(extra='forbid') + admin_secret: str + port: int = 33_333 + class Backend(Common): model_config = ConfigDict(extra='forbid') diff --git a/src/config/fact-core-config.toml b/src/config/fact-core-config.toml index 3f45d96fc..e3f72df37 100644 --- a/src/config/fact-core-config.toml +++ b/src/config/fact-core-config.toml @@ -169,3 +169,7 @@ radare2-url = "http://localhost:8000" enabled = false user-database = "sqlite:////media/data/fact_auth_data/fact_users.db" password-salt = "5up3r5tr0n6_p455w0rd_5417" + + +[frontend.hasura] +admin-secret = "4dM1n_S3cR3T_changemeplz" diff --git a/src/conftest.py b/src/conftest.py index 1a49ac526..99ca47897 100644 --- a/src/conftest.py +++ b/src/conftest.py @@ -144,6 +144,8 @@ def frontend_config(request, common_config) -> config.Frontend: 'user_database': 'sqlite:////media/data/fact_auth_data/fact_users.db', 'password_salt': '5up3r5tr0n6_p455w0rd_5417', }, + # we need the actual secret to set up the test configuration + 'hasura': {'admin_secret': config.frontend.hasura.admin_secret}, } test_config.update(common_config.model_dump()) diff --git a/src/install.py b/src/install.py index 1229dbfec..cf542ddf7 100755 --- a/src/install.py +++ b/src/install.py @@ -73,6 +73,9 @@ def _setup_argparser(): install_options.add_argument( '-R', '--no_radare', action='store_true', default=False, help='do not install radare view container' ) + install_options.add_argument( + '-H', '--no-hasura', action='store_true', default=False, help='do not set up hasura for GraphQL' + ) install_options.add_argument( '-U', '--statistic_cronjob', @@ -192,10 +195,10 @@ def install(): def install_fact_components(args, distribution, none_chosen, skip_docker): if (args.common or args.frontend or args.backend or none_chosen) and not args.no_common: common(distribution) - if args.frontend or none_chosen: - frontend(skip_docker, not args.no_radare, args.nginx, distribution) if args.db or none_chosen: db() + if args.frontend or none_chosen: + frontend(skip_docker, not args.no_radare, args.nginx, distribution, args.no_hasura) if args.backend or none_chosen: backend(skip_docker, distribution) diff --git a/src/install/db.py b/src/install/db.py index 60fc749f9..03aece931 100644 --- a/src/install/db.py +++ b/src/install/db.py @@ -33,9 +33,15 @@ def install_postgres(version: int = 14): if process.returncode != 0: raise InstallationError(f'Failed to set up PostgreSQL: {process.stderr}') - # increase the maximum number of concurrent connections (and restart for the change to take effect) + +def configure_postgres(version: int = 14): config_path = f'/etc/postgresql/{version}/main/postgresql.conf' + # increase the maximum number of concurrent connections run(f'sudo sed -i -E "s/max_connections = [0-9]+/max_connections = 999/g" {config_path}', shell=True, check=True) + hba_config_path = f'/etc/postgresql/{version}/main/pg_hba.conf' + # change UNIX domain socket auth mode from peer to user/pw + run(f'sudo sed -i -E "s/(local +all +all +)peer/\\1scram-sha-256/g" {hba_config_path}', shell=True, check=True) + # restart for the changes to take effect run('sudo service postgresql restart', shell=True, check=True) @@ -53,6 +59,7 @@ def main(): else: logging.info('Setting up PostgreSQL database') install_postgres() + configure_postgres() # initializing DB logging.info('Initializing PostgreSQL database') diff --git a/src/install/frontend.py b/src/install/frontend.py index 4bf13e10b..27adc4c70 100644 --- a/src/install/frontend.py +++ b/src/install/frontend.py @@ -16,6 +16,7 @@ read_package_list_from_file, run_cmd_with_logging, ) +from storage.graphql.util import get_env DEFAULT_CERT = '.\n.\n.\n.\n.\nexample.com\n.\n\n\n' INSTALL_DIR = Path(__file__).parent @@ -102,7 +103,8 @@ def _configure_nginx(): # copy is better on redhat to respect selinux context '(cd ../config && sudo install -m 644 $PWD/nginx.conf /etc/nginx/nginx.conf)', '(sudo mkdir /etc/nginx/error || true)', - '(cd ../web_interface/templates/ && sudo ln -s $PWD/maintenance.html /etc/nginx/error/maintenance.html) || true', # noqa: E501 + '(cd ../web_interface/templates/ ' + '&& sudo ln -s $PWD/maintenance.html /etc/nginx/error/maintenance.html) || true', ], error='configuring nginx', ) @@ -141,7 +143,13 @@ def _copy_mime_icons(): run_cmd_with_logging(f'cp -rL {ICON_THEME_INSTALL_PATH / source} {MIME_ICON_DIR / target}') -def main(skip_docker, radare, nginx, distribution): +def _init_hasura(): + with OperateInDirectory(INSTALL_DIR.parent / 'storage' / 'graphql' / 'hasura'): + run_cmd_with_logging('docker compose up -d', env=get_env()) + run_cmd_with_logging('python3 init_hasura.py') + + +def main(skip_docker, radare, nginx, distribution, skip_hasura): if distribution != 'fedora': pkgs = read_package_list_from_file(INSTALL_DIR / 'apt-pkgs-frontend.txt') apt_install_packages(*pkgs) @@ -170,6 +178,9 @@ def main(skip_docker, radare, nginx, distribution): if not skip_docker: _install_docker_images(radare) + if not skip_hasura: + _init_hasura() + if not MIME_ICON_DIR.is_dir(): MIME_ICON_DIR.mkdir() _copy_mime_icons() diff --git a/src/install/requirements_backend.txt b/src/install/requirements_backend.txt index d42978714..db579fff8 100644 --- a/src/install/requirements_backend.txt +++ b/src/install/requirements_backend.txt @@ -1,5 +1,4 @@ cryptography==42.0.4 -docker~=7.1.0 MarkupSafe==2.1.1 networkx==2.6.3 Pillow==10.3.0 diff --git a/src/install/requirements_common.txt b/src/install/requirements_common.txt index f60a6cf98..e2edaa521 100644 --- a/src/install/requirements_common.txt +++ b/src/install/requirements_common.txt @@ -3,6 +3,7 @@ testresources==2.0.1 # General python dependencies appdirs==1.4.4 +docker~=7.1.0 flaky==3.7.0 lief==0.12.3 psutil==5.9.4 diff --git a/src/install/requirements_frontend.txt b/src/install/requirements_frontend.txt index fc47c47ba..7a4ac4370 100644 --- a/src/install/requirements_frontend.txt +++ b/src/install/requirements_frontend.txt @@ -7,6 +7,7 @@ flask-wtf~=1.2.1 flask~=3.0.3 flask-restx~=1.3.0 flask-sqlalchemy~=3.1.1 +gql~=3.5.0 itsdangerous~=2.2.0 matplotlib~=3.7.5 more-itertools~=10.2.0 diff --git a/src/storage/db_connection.py b/src/storage/db_connection.py index b97f3dcf4..3169b4944 100644 --- a/src/storage/db_connection.py +++ b/src/storage/db_connection.py @@ -17,6 +17,9 @@ def __init__( self.base = Base address = config.common.postgres.server + if address in ('localhost', '127.0.0.1', '::1'): + # local postgres => connect through UNIX domain socket (faster than TCP) + address = '/var/run/postgresql' port = config.common.postgres.port user = getattr(config.common.postgres, user) password = getattr(config.common.postgres, password) diff --git a/src/storage/graphql/__init__.py b/src/storage/graphql/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/storage/graphql/hasura/__init__.py b/src/storage/graphql/hasura/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/storage/graphql/hasura/docker-compose.base.yml b/src/storage/graphql/hasura/docker-compose.base.yml new file mode 100644 index 000000000..0d4cff993 --- /dev/null +++ b/src/storage/graphql/hasura/docker-compose.base.yml @@ -0,0 +1,5 @@ + services: + postgres-local: + volumes: + - /var/run/postgresql:/var/run/postgresql + postgres-remote: diff --git a/src/storage/graphql/hasura/docker-compose.yaml b/src/storage/graphql/hasura/docker-compose.yaml new file mode 100644 index 000000000..c37851979 --- /dev/null +++ b/src/storage/graphql/hasura/docker-compose.yaml @@ -0,0 +1,50 @@ +services: + # the postgres instance is only for Hasura to store its metadata and should not be available from outside + postgres: + image: postgres:15 + restart: always + volumes: + - db_data:/var/lib/postgresql/data + environment: + POSTGRES_PASSWORD: postgrespassword + graphql-engine: + image: hasura/graphql-engine:v2.38.0 + ports: + - "${HASURA_PORT}:8080" + restart: always + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + HASURA_GRAPHQL_METADATA_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres + PG_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres + HASURA_GRAPHQL_ENABLE_CONSOLE: "true" + HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log + HASURA_GRAPHQL_CONSOLE_ASSETS_DIR: /srv/console-assets + HASURA_GRAPHQL_METADATA_DEFAULTS: '{"backend_configs":{"dataconnector":{"athena":{"uri":"http://data-connector-agent:8081/api/v1/athena"},"mariadb":{"uri":"http://data-connector-agent:8081/api/v1/mariadb"},"mysql8":{"uri":"http://data-connector-agent:8081/api/v1/mysql"},"oracle":{"uri":"http://data-connector-agent:8081/api/v1/oracle"},"snowflake":{"uri":"http://data-connector-agent:8081/api/v1/snowflake"}}}}' + # should be set during init + FACT_DB_URL: "${FACT_DB_URL}" + HASURA_GRAPHQL_ADMIN_SECRET: "${HASURA_ADMIN_SECRET}" + HASURA_GRAPHQL_UNAUTHORIZED_ROLE: "ro_user" + depends_on: + data-connector-agent: + condition: service_healthy + extends: + # this is kind of a hack to make mounting a volume optional, see https://github.com/docker/compose/issues/3979 + file: docker-compose.base.yml + service: "postgres-${DB_LOCALITY:-local}" + data-connector-agent: + image: hasura/graphql-data-connector:v2.38.0 + restart: always + ports: + - "8081:8081" + environment: + QUARKUS_LOG_LEVEL: ERROR + QUARKUS_OPENTELEMETRY_ENABLED: "false" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8081/api/v1/athena/health"] + interval: 5s + timeout: 10s + retries: 5 + start_period: 5s +volumes: + db_data: diff --git a/src/storage/graphql/hasura/init_hasura.py b/src/storage/graphql/hasura/init_hasura.py new file mode 100644 index 000000000..ab79cbb08 --- /dev/null +++ b/src/storage/graphql/hasura/init_hasura.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +import logging +import sys +from pathlib import Path + +import requests +from requests import Response +from requests.adapters import HTTPAdapter, Retry + +try: + import config +except ImportError: + SRC_DIR = Path(__file__).absolute().parent.parent.parent.parent + sys.path.append(str(SRC_DIR)) + import config + +HTML_OK = 200 +HTML_BAD_REQUEST = 400 +TRACKED_TABLES = ('analysis', 'file_object', 'firmware', 'fw_files', 'included_files', 'virtual_file_path') +RELATIONSHIPS = { + 'pg_create_object_relationship': [ + # table, name, constraint + ('analysis', 'file_object', 'uid'), + ('firmware', 'file_object', 'uid'), + ('fw_files', 'firmware', 'root_uid'), + ('fw_files', 'file_object', 'file_uid'), + ('included_files', 'parent', 'parent_uid'), + ('included_files', 'child', 'child_uid'), + ('file_object', 'firmware', 'uid', 'firmware'), + ], + 'pg_create_array_relationship': [ + ('file_object', 'analysis', 'uid', 'analysis'), + ('file_object', 'includedFiles', 'root_uid', 'fw_files'), + ('file_object', 'parentFirmwares', 'file_uid', 'fw_files'), + ('file_object', 'children', 'parent_uid', 'included_files'), + ('file_object', 'parents', 'child_uid', 'included_files'), + ('file_object', 'childrenFilePaths', 'parent_uid', 'virtual_file_path'), + ('file_object', 'filePaths', 'file_uid', 'virtual_file_path'), + ], +} + + +class HasuraInitError(Exception): + pass + + +class HasuraSetup: + def __init__(self, db_name: str | None = None, testing: bool = False): + self.db_name = db_name or config.common.postgres.database + self.url = f'http://localhost:{config.frontend.hasura.port}/v1/metadata' + self.headers = { + 'Content-Type': 'application/json', + 'X-Hasura-Role': 'admin', + 'X-Hasura-Admin-Secret': config.frontend.hasura.admin_secret, + } + self.testing = testing + + def init_hasura(self, db_args: dict | None = None): + logging.info('Waiting for Hasura...') + self._wait_for_hasura() + logging.info('Initializing Hasura...') + if not self._db_was_already_added(): + self._add_database(db_args) + self._track_tables() + self._add_relationships() + self._add_ro_user_role_to_tables() + logging.info('Hasura initialization successful') + + def _wait_for_hasura(self): + # Hasura is not ready for connections directly after starting the container, so we may need to wait a bit + # hasura will return code 200 OK or 500 ERROR on this health check endpoint + healthcheck_url = self.url.replace('/v1/metadata', '/healthz') + session = requests.Session() + # retry 5 times with a total of 30 seconds + retries = Retry(total=5, backoff_factor=1, status_forcelist=[500]) + session.mount('http://', HTTPAdapter(max_retries=retries)) + try: + session.get(healthcheck_url) + except (ConnectionRefusedError, ConnectionResetError) as _error: + raise HasuraInitError('Could not reach Hasura') from _error + + def _add_database(self, additional_args: dict | None = None): + query = { + 'type': 'pg_add_source', + 'args': { + 'name': self.db_name, + 'configuration': { + 'connection_info': { + 'database_url': self._get_db_url(), + 'pool_settings': {'retries': 1, 'idle_timeout': 180, 'max_connections': 50}, + }, + }, + **(additional_args or {}), + }, + } + response = requests.post(self.url, headers=self.headers, json=query) + if response.status_code != HTML_OK: + raise HasuraInitError(f'Failed to add database: {response.json().get("error")}') + + def _get_db_url(self): + if not self.testing: + return {'from_env': 'FACT_DB_URL'} + user = config.common.postgres.ro_user + pw = config.common.postgres.ro_pw + return f'postgresql://{user}:{pw}@/fact_test?host=/var/run/postgresql' + + def drop_database(self): + query = { + 'type': 'pg_drop_source', + 'args': { + 'name': self.db_name, + 'cascade': True, + }, + } + response = requests.post(self.url, headers=self.headers, json=query) + if response.status_code != HTML_OK: + raise HasuraInitError(f'Failed to drop database: {response.json().get("error")}') + + def _track_tables(self): + for table in TRACKED_TABLES: + query = { + 'type': 'pg_track_table', + 'args': { + 'source': self.db_name, + 'table': table, + }, + } + if self.testing: + # we need to use another name during testing to avoid naming conflicts + query['args']['configuration'] = {'custom_name': self._prefix(table)} + response = requests.post(self.url, headers=self.headers, json=query) + if response.status_code != HTML_OK: + if _was_already_added(response): + continue + raise HasuraInitError(f'Failed to track table {table}: {response.json().get("error")}') + + def _add_relationships(self): + for action, relation_list in RELATIONSHIPS.items(): + for table, name, *constraints in relation_list: + if len(constraints) == 1: + constraint = constraints[0] + else: + column, other_table = constraints + constraint = { + 'column': column, + 'table': {'name': other_table, 'schema': 'public'}, + } + query = { + 'type': action, + 'args': { + 'table': table, + 'name': self._prefix(name), + 'source': self.db_name, + 'using': {'foreign_key_constraint_on': constraint}, + }, + } + response = requests.post(self.url, headers=self.headers, json=query) + if response.status_code != HTML_OK: + if _was_already_added(response): + continue + raise HasuraInitError( + f'Failed to add constraint {name} on table {table}: {response.json().get("error")}' + ) + + def _db_was_already_added(self) -> bool: + query = {'type': 'pg_get_source_tables', 'args': {'source': self.db_name}} + response = requests.post(self.url, headers=self.headers, json=query) + if response.status_code not in {HTML_OK, HTML_BAD_REQUEST}: + raise HasuraInitError('No connection to Hasura API. Is it running?') + data = response.json() + if isinstance(data, dict) and 'error' in data: + return False + return True + + def _add_ro_user_role_to_tables(self): + for table in TRACKED_TABLES: + query = { + 'type': 'pg_create_select_permission', + 'args': { + 'source': self.db_name, + 'table': table, + 'role': 'ro_user', + 'permission': {'columns': '*', 'filter': {}}, + }, + } + response = requests.post(self.url, headers=self.headers, json=query) + if response.status_code != HTML_OK: + if _was_already_added(response): + continue + raise HasuraInitError(f'Failed to role to table {table}: {response.json().get("error")}') + + def _prefix(self, attribute: str) -> str: + return f'test_{attribute}' if self.testing else attribute + + +def _was_already_added(response: Response) -> bool: + data = response.json() + return isinstance(data, dict) and 'error' in data and data.get('code') in ('already-exists', 'already-tracked') + + +if __name__ == '__main__': + config.load() + try: + HasuraSetup().init_hasura() + except HasuraInitError as error: + logging.exception(f'Error during Hasura init: {error}') + sys.exit(1) + sys.exit(0) diff --git a/src/storage/graphql/hasura/restart.py b/src/storage/graphql/hasura/restart.py new file mode 100644 index 000000000..b4b6b6009 --- /dev/null +++ b/src/storage/graphql/hasura/restart.py @@ -0,0 +1,43 @@ +import sys +from pathlib import Path +from shlex import split +from subprocess import PIPE, STDOUT, Popen + +from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn + +try: + import config + from helperFunctions.install import OperateInDirectory + from storage.graphql.util import get_env +except ImportError: + SRC_DIR = Path(__file__).parent.parent.parent.parent + sys.path.append(str(SRC_DIR)) + import config + from helperFunctions.install import OperateInDirectory + from storage.graphql.util import get_env + +progress = Progress( + BarColumn(), + TaskProgressColumn(), + SpinnerColumn(), +) + + +def restart(): + with OperateInDirectory(Path(__file__).parent), progress: + progress.console.print('Restarting Hasura ⏳') + for command in progress.track(('docker compose down', 'docker compose up -d')): + progress.console.print(f'Running {command} ...') + process = Popen(split(command), stdout=PIPE, stderr=STDOUT, text=True, env=get_env()) + while not process.poll(): + if output := process.stdout.readline(): + progress.console.print(f'\t{output.strip()}') + else: + break + process.wait() + progress.console.print('Finished restarting Hasura ✨') + + +if __name__ == '__main__': + config.load() + restart() diff --git a/src/storage/graphql/interface.py b/src/storage/graphql/interface.py new file mode 100644 index 000000000..5e491a974 --- /dev/null +++ b/src/storage/graphql/interface.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import logging + +from gql import Client, gql +from gql.transport.aiohttp import AIOHTTPTransport, log + +import config + +log.setLevel(logging.WARNING) # disable noisy gql logs + +FO_QUERY = """ +query file_object($where: file_object_bool_exp, $limit: Int, $offset: Int) { + file_object_aggregate(where: $where) { + aggregate { + totalCount: count + } + } + file_object(where: $where, limit: $limit, offset: $offset) { + uid + } +} +""" +FW_QUERY = """ +query firmware($where: firmware_bool_exp, $limit: Int, $offset: Int) { + firmware_aggregate(where: $where) { + aggregate { + totalCount: count + } + } + firmware(where: $where, limit: $limit, offset: $offset, order_by: {vendor: asc}) { + uid + } +} +""" +ANALYSIS_QUERY = """ +query analysis($where: analysis_bool_exp, $limit: Int, $offset: Int) { + analysis_aggregate(where: $where, distinct_on: uid) { + aggregate { + totalCount: count + } + } + analysis(where: $where, limit: $limit, offset: $offset, distinct_on: uid) { + uid + } +} +""" +TABLE_TO_QUERY = { + 'file_object': FO_QUERY, + 'firmware': FW_QUERY, + 'analysis': ANALYSIS_QUERY, +} +# these queries are simplified versions of the ones above that are displayed in the web interface +TEMPLATE_QUERIES = { + 'file_object': ( + 'query file_object_query($where: file_object_bool_exp) {\n' + ' file_object(where: $where) {\n' + ' uid\n' + ' }\n' + '}' + ), + 'firmware': ( + 'query firmware_query($where: firmware_bool_exp) {\n' + ' firmware(where: $where, order_by: {vendor: asc}) {\n' + ' uid\n' + ' }\n' + '}' + ), + 'analysis': ( + 'query analysis_query($where: analysis_bool_exp) {\n' + ' analysis(where: $where, distinct_on: uid) {\n' + ' uid\n' + ' }\n' + '}' + ), +} + + +class GraphQLSearchError(Exception): + pass + + +class GraphQlInterface: + def __init__(self): + if config.frontend is None: + config.load() + url = f'http://localhost:{config.frontend.hasura.port}/v1/graphql' + headers = { + 'Content-Type': 'application/json', + 'X-Hasura-Role': 'admin', + 'X-Hasura-Admin-Secret': config.frontend.hasura.admin_secret, + } + transport = AIOHTTPTransport(url=url, headers=headers) + self.client = Client(transport=transport) + + def search_gql( + self, + where: dict, + table: str, + offset: int | None = None, + limit: int | None = None, + ) -> tuple[list[str], int]: + """ + Search the database using a GraphQL query. + + :param where: $where part of the query as dict. + :param table: name of the table we are searching. Must be one of "file_object", "firmware", "analysis". + :param offset: number of items to skip. + :param limit: number of items to return. + :return: Tuple with a list of matching uids and the total number of matches. + """ + variables = {'where': where} + if offset is not None: + variables['offset'] = offset + if limit is not None: + variables['limit'] = limit + + if not (query := TABLE_TO_QUERY[table]): + raise GraphQLSearchError(f'Unknown table {table}') + + response = self.client.execute(gql(query), variable_values=variables) + total = response.get(f'{table}_aggregate', {}).get('aggregate', {}).get('totalCount') + if total == 0: + raise GraphQLSearchError('No results found.') + if total is None: + raise GraphQLSearchError('Could not determine total result count.') + return [e['uid'] for e in response.get(table, {})], total diff --git a/src/storage/graphql/util.py b/src/storage/graphql/util.py new file mode 100644 index 000000000..abb14f38b --- /dev/null +++ b/src/storage/graphql/util.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import os + +import config + + +def get_env() -> dict[str, str]: + user = config.common.postgres.rw_user + pw = config.common.postgres.rw_pw + port = config.common.postgres.port + server = config.common.postgres.server + if server in ('localhost', '127.0.0.1', '::1', '/var/run/postgresql'): + # local postgres => connect through UNIX domain socket (faster than TCP) + db_url = f'postgresql://{user}:{pw}@/fact_db?host=/var/run/postgresql' + locality = 'local' + else: + db_url = f'postgresql://{user}:{pw}@{server}:{port}/fact_db' + locality = 'remote' + return { + **os.environ, + 'HASURA_ADMIN_SECRET': config.frontend.hasura.admin_secret, + 'FACT_DB_URL': db_url, + 'HASURA_PORT': str(config.frontend.hasura.port), + 'DB_LOCALITY': locality, + } diff --git a/src/test/data/fact-core-config.toml b/src/test/data/fact-core-config.toml index f48535888..146c41b73 100644 --- a/src/test/data/fact-core-config.toml +++ b/src/test/data/fact-core-config.toml @@ -113,3 +113,7 @@ communication-timeout = 60 enabled = false user-database = "sqlite:////media/data/fact_auth_data/fact_users.db" password-salt = "5up3r5tr0n6_p455w0rd_5417" + + +[frontend.hasura] +admin-secret = "4dM1n_S3cR3T_changemeplz" diff --git a/src/test/data/fact-core-config.toml-missing-entrys b/src/test/data/fact-core-config.toml-missing-entrys index ff2c0ae0f..60bb59e36 100644 --- a/src/test/data/fact-core-config.toml-missing-entrys +++ b/src/test/data/fact-core-config.toml-missing-entrys @@ -113,3 +113,7 @@ communication-timeout = 60 enabled = false user-database = "sqlite:////media/data/fact_auth_data/fact_users.db" password-salt = "5up3r5tr0n6_p455w0rd_5417" + + +[frontend.hasura] +admin-secret = "4dM1n_S3cR3T_changemeplz" diff --git a/src/test/integration/storage/test_graphql_interface.py b/src/test/integration/storage/test_graphql_interface.py new file mode 100644 index 000000000..cc1d4488f --- /dev/null +++ b/src/test/integration/storage/test_graphql_interface.py @@ -0,0 +1,58 @@ +import pytest + +from storage.graphql.hasura.init_hasura import HasuraSetup +from storage.graphql.interface import TABLE_TO_QUERY, GraphQlInterface +from test.common_helper import generate_analysis_entry +from test.integration.storage.helper import create_fw_with_child_fo, insert_test_fw + + +@pytest.fixture +def _init_hasura(_database_interfaces): + setup = HasuraSetup(testing=True) + try: + setup.init_hasura() + yield + finally: + setup.drop_database() + + +@pytest.fixture +def graphql_interface(): + TABLE_TO_QUERY.update({f'test_{key}': value.replace(key, f'test_{key}') for key, value in TABLE_TO_QUERY.items()}) + return GraphQlInterface() + + +@pytest.mark.usefixtures('_init_hasura') +def test_graphql_search(backend_db, graphql_interface): + fo, fw = create_fw_with_child_fo() + fw.file_name = 'fw.bin' + fo.file_name = 'some.file' + fo.processed_analysis = {'plugin_name': generate_analysis_entry(analysis_result={'foo': 'bar'})} + backend_db.insert_multiple_objects(fw, fo) + + # insert some unrelated objects to assure non-matching objects are not found + insert_test_fw(backend_db, 'some_other_fw', vendor='other_vendor') + + # the queries in the DB fact_test are prefixed with "test_" to avoid name conflicts + result, count = graphql_interface.search_gql({'vendor': {'_eq': fw.vendor}}, 'test_firmware') + assert count == 1 + assert result == [fw.uid] + + result, count = graphql_interface.search_gql({'vendor': {'_in': [fw.vendor, 'other_vendor']}}, 'test_firmware') + assert count == 2 # noqa: PLR2004 + + result, count = graphql_interface.search_gql( + {'result': {'_contains': {'foo': 'bar'}}, 'plugin': {'_eq': 'plugin_name'}}, 'test_analysis' + ) + assert count == 1 + assert result == [fo.uid] + + result, count = graphql_interface.search_gql( + { + 'is_firmware': {'_eq': True}, + 'test_firmwareFilesByFirmware': {'test_file_object': {'file_name': {'_eq': fo.file_name}}}, + }, + 'test_file_object', + ) + assert count == 1 + assert result == [fw.uid] diff --git a/src/web_interface/components/database_routes.py b/src/web_interface/components/database_routes.py index a0d32f232..7a86bce4e 100644 --- a/src/web_interface/components/database_routes.py +++ b/src/web_interface/components/database_routes.py @@ -1,17 +1,27 @@ +from __future__ import annotations + import json import logging +from dataclasses import dataclass from datetime import datetime +from enum import Enum from itertools import chain +from json import JSONDecodeError -from flask import redirect, render_template, request, url_for +import requests +from flask import Response, flash, redirect, render_template, request, url_for +from gql.transport.exceptions import TransportQueryError +from graphql import GraphQLSyntaxError from sqlalchemy.exc import SQLAlchemyError +import config from helperFunctions.data_conversion import make_unicode_string from helperFunctions.database import get_shared_session from helperFunctions.task_conversion import get_file_name_and_binary_from_request from helperFunctions.uid import is_uid from helperFunctions.web_interface import apply_filters_to_query, filter_out_illegal_characters from helperFunctions.yara_binary_search import get_yara_error, is_valid_yara_rule_file +from storage.graphql.interface import TEMPLATE_QUERIES, GraphQlInterface, GraphQLSearchError from storage.query_conversion import QueryConversionException from web_interface.components.component_base import GET, POST, AppRoute, ComponentBase from web_interface.pagination import extract_pagination_from_request, get_pagination @@ -19,7 +29,27 @@ from web_interface.security.privileges import PRIVILEGES +@dataclass +class SearchParameters: + class TargetType(str, Enum): + yara = 'YARA' + graphql = 'GraphQL' + file = 'File' + firmware = 'Firmware' + inverted = 'Inverse Firmware' + + query: dict | str + only_firmware: bool + inverted: bool + search_target: TargetType + query_title: str + + class DatabaseRoutes(ComponentBase): + def __init__(self, *args, **kwargs): + self.gql_interface = GraphQlInterface() + super().__init__(*args, **kwargs) + @staticmethod def _add_date_to_query(query, date): try: @@ -35,20 +65,37 @@ def _add_date_to_query(query, date): @roles_accepted(*PRIVILEGES['basic_search']) @AppRoute('/database/browse', GET) - def browse_database(self, query: str = '{}', only_firmwares=False, inverted=False): + def browse_database(self, query: str = '{}'): page, per_page = extract_pagination_from_request(request)[0:2] - search_parameters = self._get_search_parameters(query, only_firmwares, inverted) + offset, limit = per_page * (page - 1), per_page + total = None + parameters = self._get_search_parameters(query) + + if parameters.search_target == SearchParameters.TargetType.graphql: + where = parameters.query.get('where', {}) + table = parameters.query.get('table') + try: + matches, total = self.gql_interface.search_gql(where, table, offset=offset, limit=limit) + except (GraphQLSearchError, GraphQLSyntaxError, TransportQueryError) as error: + if hasattr(error, 'errors') and error.errors: + error = ', '.join(err.get('message') for err in error.errors if err) + message = f'Error during GraphQL search: {error}' + logging.exception(message) + flash(message) + return redirect(url_for(self.get_graphql.__name__, last_query=json.dumps(where))) + parameters.query = {'uid': {'$in': matches}} # update query for "generic search" + offset, limit = None, None # should only be applied once with get_shared_session(self.db.frontend) as frontend_db: try: firmware_list = self._search_database( - search_parameters['query'], - skip=per_page * (page - 1), - limit=per_page, - only_firmwares=search_parameters['only_firmware'], - inverted=search_parameters['inverted'], + parameters.query, + skip=offset, + limit=limit, + only_firmwares=parameters.only_firmware, + inverted=parameters.inverted, ) - if self._query_has_only_one_result(firmware_list, search_parameters['query']): + if self._query_has_only_one_result(firmware_list, parameters.query): return redirect(url_for('show_analysis', uid=firmware_list[0][0])) except QueryConversionException as exception: error_message = exception.get_message() @@ -58,9 +105,10 @@ def browse_database(self, query: str = '{}', only_firmwares=False, inverted=Fals logging.error(error_message + f' due to exception: {err}', exc_info=True) return render_template('error.html', message=error_message) - total = frontend_db.get_number_of_total_matches( - search_parameters['query'], search_parameters['only_firmware'], inverted=search_parameters['inverted'] - ) + if not total: + total = frontend_db.get_number_of_total_matches( + parameters.query, parameters.only_firmware, parameters.inverted + ) device_classes = frontend_db.get_device_class_list() vendors = frontend_db.get_vendor_list() @@ -75,7 +123,7 @@ def browse_database(self, query: str = '{}', only_firmwares=False, inverted=Fals vendors=vendors, current_class=str(request.args.get('device_class')), current_vendor=str(request.args.get('vendor')), - search_parameters=search_parameters, + search_parameters=parameters, ) @roles_accepted(*PRIVILEGES['pattern_search']) @@ -100,31 +148,45 @@ def browse_searches(self): pagination=pagination, ) - def _get_search_parameters(self, query, only_firmware, inverted): + def _get_search_parameters(self, query_str: str) -> SearchParameters: """ This function prepares the requested search by parsing all necessary parameters. In case of a binary search, indicated by the query being an uid instead of a dict, the cached search result is retrieved. """ - search_parameters = {} - if request.args.get('query'): - query = request.args.get('query') - if is_uid(query): - cached_query = self.db.frontend.get_query_from_cache(query) - query = cached_query.query - search_parameters['query_title'] = cached_query.yara_rule - search_parameters['only_firmware'] = ( - request.args.get('only_firmwares') == 'True' if request.args.get('only_firmwares') else only_firmware + query_str = request.args.get('query', query_str) + graphql = request.args.get('graphql') == 'True' + only_firmware = request.args.get('only_firmwares') == 'True' + inverted = request.args.get('inverted') == 'True' + query_title = None + if graphql: + query = json.loads(query_str) + elif is_uid(query_str): # cached binary search + cached_query = self.db.frontend.get_query_from_cache(query_str) + query = json.loads(cached_query.query) + query_title = cached_query.yara_rule + else: # regular / advanced search + query = apply_filters_to_query(request, query_str) + if request.args.get('date'): + query = self._add_date_to_query(query, request.args.get('date')) + search_target = ( + SearchParameters.TargetType.yara + if query_title + else SearchParameters.TargetType.graphql + if graphql + else SearchParameters.TargetType.file + if not only_firmware + else SearchParameters.TargetType.firmware + if not inverted + else SearchParameters.TargetType.inverted ) - search_parameters['inverted'] = ( - request.args.get('inverted') == 'True' if request.args.get('inverted') else inverted + return SearchParameters( + query=query, + inverted=inverted, + only_firmware=only_firmware, + search_target=search_target, + query_title=query_title or query, ) - search_parameters['query'] = apply_filters_to_query(request, query) - if 'query_title' not in search_parameters: - search_parameters['query_title'] = search_parameters['query'] - if request.args.get('date'): - search_parameters['query'] = self._add_date_to_query(search_parameters['query'], request.args.get('date')) - return search_parameters @staticmethod def _query_has_only_one_result(result_list, query): @@ -154,7 +216,8 @@ def _build_search_query(self): query['firmware_tags'] = {'$overlap': tags} return json.dumps(query) - def _add_hash_query_to_query(self, query, value): + @staticmethod + def _add_hash_query_to_query(query, value): # FIXME: The frontend should not need to know how the plugin is configured hash_types = ['md5', 'sha1', 'sha256', 'sha512', 'ripemd160', 'whirlpool'] hash_query = {f'processed_analysis.file_hashes.{hash_type}': value for hash_type in hash_types} @@ -216,7 +279,8 @@ def start_binary_search(self): error = 'please select a file or enter rules in the text area' return render_template('database/database_binary_search.html', error=error) - def _get_items_from_binary_search_request(self, req): + @staticmethod + def _get_items_from_binary_search_request(req): yara_rule_file = None if req.files.get('file'): _, yara_rule_file = get_file_name_and_binary_from_request(req) @@ -279,4 +343,72 @@ def start_quick_search(self): 'firmware_tags': search_term, } } - return redirect(url_for('browse_database', query=json.dumps(query))) + return redirect(url_for(self.browse_database.__name__, query=json.dumps(query))) + + @roles_accepted(*PRIVILEGES['advanced_search']) + @AppRoute('/database/graphql', GET) + def get_graphql(self): + return render_template( + 'database/database_graphql.html', + secret=config.frontend.hasura.admin_secret, + tables=TEMPLATE_QUERIES, + last_query=request.args.get('last_query'), + ) + + @roles_accepted(*PRIVILEGES['advanced_search']) + @AppRoute('/database/graphql', POST) + def post_graphql(self): + where_str = request.form.get('textarea') + try: + where = json.loads(where_str) + except JSONDecodeError as error: + if where_str == '': + flash('Error: Query is empty') + else: + flash(f'JSON decoding error: {error}') + return redirect(url_for(self.get_graphql.__name__, last_query=where_str)) + + table = request.form.get('tableSelect') + if not (where_str or table): + flash('Error: GraphQL query or table not found in request') + return redirect(url_for(self.get_graphql.__name__)) + + return redirect( + url_for( + self.browse_database.__name__, + query=json.dumps({'where': where, 'table': table}), + graphql=True, + ) + ) + + @roles_accepted(*PRIVILEGES['advanced_search']) + @AppRoute('/graphql', GET, POST) + def proxy_graphql(self): + """ + Creates a proxy for GraphQL so that we have auth and don't need to expose more ports. Since we assume that the + user is authenticated to use this endpoint, we add the Hasura headers if they are missing. + """ + excluded_proxy_headers = {'Host', 'Authorization'} + req_headers = {k: v for (k, v) in request.headers if k not in excluded_proxy_headers} + response = requests.request( + method=request.method, + url=f'http://localhost:{config.frontend.hasura.port}/v1/graphql', + headers={**req_headers, 'X-Hasura-Role': 'ro_user'}, + data=request.get_data(), + cookies=request.cookies, + allow_redirects=False, + ) + excluded_headers = { + 'connection', + 'content-encoding', + 'content-length', + 'keep-alive', + 'proxy-authenticate', + 'proxy-authorization', + 'te', + 'trailers', + 'transfer-encoding', + 'upgrade', + } + headers = {k: v for (k, v) in response.raw.headers.items() if k.lower() not in excluded_headers} + return Response(response.content, response.status_code, headers=headers) diff --git a/src/web_interface/static/package-lock.json b/src/web_interface/static/package-lock.json index 5a1bc5867..33b9c607f 100644 --- a/src/web_interface/static/package-lock.json +++ b/src/web_interface/static/package-lock.json @@ -9,61 +9,1078 @@ "version": "0.0.0", "license": "GPL-3.0-only", "dependencies": { - "@fortawesome/fontawesome-free": "^5.15.4", + "@fortawesome/fontawesome-free": "^6.5.2", + "@graphiql/plugin-explorer": "^3.0.2", + "@graphiql/toolkit": "^0.9.1", "@highlightjs/cdn-assets": "^11.8.0", "bootstrap": "^4.6.2", "bootstrap-datepicker": "^1.9.0", "bootstrap-select": "^1.13.18", "chart.js": "^2.3.0", "diff2html": "^3.4.18", + "graphiql": "^3.2.3", "jquery": "^3.5.0", + "json5": "^2.2.3", "jstree": "^3.3.12", "jstree-bootstrap-theme": "^1.0.1", "moment": "^2.29.4", "popper.js": "^1.16.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", "vis-network": "^9.1.6" } }, + "node_modules/@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.0.0.tgz", + "integrity": "sha512-rtjk5ifyMzOna1c7PBu7J1VCt0PvA5wy3o8eMVnxMKb7z8KA7JFecvD04dSn14vj/bBaAbqRsGed5OjtofEnLA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==", + "license": "MIT", + "peer": true + }, + "node_modules/@codemirror/view": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.28.0.tgz", + "integrity": "sha512-fo7CelaUDKWIyemw4b+J57cWuRkOu4SWCCPfNDkPvfWkGjM9D5racHQXr4EQeYCD6zEBIBxGCeaKkQo+ysl0gA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@egjs/hammerjs": { "version": "2.0.17", "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", "peer": true, "dependencies": { - "@types/hammerjs": "^2.0.36" + "@types/hammerjs": "^2.0.36" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "license": "MIT", + "optional": true + }, + "node_modules/@floating-ui/core": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", + "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", + "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.0.tgz", + "integrity": "sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", + "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==", + "license": "MIT" + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.2.tgz", + "integrity": "sha512-hRILoInAx8GNT5IMkrtIt9blOdrqHOnPBH+k70aWUAqPZPgopb9G5EQJFpaBx/S8zp2fC+mPW349Bziuk1o28Q==", + "hasInstallScript": true, + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", + "engines": { + "node": ">=6" + } + }, + "node_modules/@graphiql/plugin-explorer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@graphiql/plugin-explorer/-/plugin-explorer-3.0.2.tgz", + "integrity": "sha512-bxbXzZ89qfpPJ+ObJ9Be3EJy6WTlvICQx4eRheLfETW0e3ujkBgtzhfWzp7lEFKq0IoTsng3cDr80RxfuEV7aA==", + "license": "MIT", + "dependencies": { + "graphiql-explorer": "^0.9.0" + }, + "peerDependencies": { + "@graphiql/react": "^0.22.2", + "graphql": "^15.5.0 || ^16.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/@graphiql/plugin-explorer/node_modules/graphiql-explorer": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/graphiql-explorer/-/graphiql-explorer-0.9.0.tgz", + "integrity": "sha512-fZC/wsuatqiQDO2otchxriFO0LaWIo/ovF/CQJ1yOudmY0P7pzDiP+l9CEHUiWbizk3e99x6DQG4XG1VxA+d6A==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.6.0 || ^0.7.0 || ^0.8.0-b || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0", + "react": "^15.6.0 || ^16.0.0", + "react-dom": "^15.6.0 || ^16.0.0" + } + }, + "node_modules/@graphiql/react": { + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/@graphiql/react/-/react-0.22.2.tgz", + "integrity": "sha512-46UV7CBQdZ0iU537uOkOU6HOOs7P1o7vQpFSUezB4VRem0Y3I4TDaYQADCOo7gFlwBs5Vb9YOup8r7cmXGIr7A==", + "license": "MIT", + "dependencies": { + "@graphiql/toolkit": "^0.9.1", + "@headlessui/react": "^1.7.15", + "@radix-ui/react-dialog": "^1.0.4", + "@radix-ui/react-dropdown-menu": "^2.0.5", + "@radix-ui/react-tooltip": "^1.0.6", + "@radix-ui/react-visually-hidden": "^1.0.3", + "@types/codemirror": "^5.60.8", + "clsx": "^1.2.1", + "codemirror": "^5.65.3", + "codemirror-graphql": "^2.0.12", + "copy-to-clipboard": "^3.2.0", + "framer-motion": "^6.5.1", + "graphql-language-service": "^5.2.1", + "markdown-it": "^14.1.0", + "set-value": "^4.1.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/@graphiql/toolkit": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.9.1.tgz", + "integrity": "sha512-LVt9pdk0830so50ZnU2Znb2rclcoWznG8r8asqAENzV0U1FM1kuY0sdPpc/rBc9MmmNgnB6A+WZzDhq6dbhTHA==", + "license": "MIT", + "dependencies": { + "@n1ru4l/push-pull-async-iterable-iterator": "^3.1.0", + "meros": "^1.1.4" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0", + "graphql-ws": ">= 4.5.0" + }, + "peerDependenciesMeta": { + "graphql-ws": { + "optional": true + } + } + }, + "node_modules/@headlessui/react": { + "version": "1.7.19", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz", + "integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==", + "license": "MIT", + "dependencies": { + "@tanstack/react-virtual": "^3.0.0-beta.60", + "client-only": "^0.0.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, + "node_modules/@highlightjs/cdn-assets": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/@highlightjs/cdn-assets/-/cdn-assets-11.8.0.tgz", + "integrity": "sha512-gkfCH4xGBGY9xPaW+t26WpgnfpDhNhB5RtVUDLx3MHkC7ZrmKeIxXsfjzOiuOnEgRk+vydlY6XeOeglh+eVhyg==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@lezer/common": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", + "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@lezer/highlight": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz", + "integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.1.tgz", + "integrity": "sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@motionone/animation": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.18.0.tgz", + "integrity": "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==", + "license": "MIT", + "dependencies": { + "@motionone/easing": "^10.18.0", + "@motionone/types": "^10.17.1", + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/dom": { + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.12.0.tgz", + "integrity": "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==", + "license": "MIT", + "dependencies": { + "@motionone/animation": "^10.12.0", + "@motionone/generators": "^10.12.0", + "@motionone/types": "^10.12.0", + "@motionone/utils": "^10.12.0", + "hey-listen": "^1.0.8", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/easing": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.18.0.tgz", + "integrity": "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==", + "license": "MIT", + "dependencies": { + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/generators": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.18.0.tgz", + "integrity": "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==", + "license": "MIT", + "dependencies": { + "@motionone/types": "^10.17.1", + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/types": { + "version": "10.17.1", + "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.17.1.tgz", + "integrity": "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==", + "license": "MIT" + }, + "node_modules/@motionone/utils": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.18.0.tgz", + "integrity": "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==", + "license": "MIT", + "dependencies": { + "@motionone/types": "^10.17.1", + "hey-listen": "^1.0.8", + "tslib": "^2.3.1" + } + }, + "node_modules/@n1ru4l/push-pull-async-iterable-iterator": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.2.0.tgz", + "integrity": "sha512-3fkKj25kEjsfObL6IlKPAlHYPq/oYwUkkQ03zsTTiDjD7vg/RxjdiLeCydqtxHZP0JgsXL3D/X5oAkMGzuUp/Q==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", + "integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", + "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", + "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", + "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz", + "integrity": "sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-menu": "2.0.6", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz", + "integrity": "sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz", + "integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-rect": "1.0.1", + "@radix-ui/react-use-size": "1.0.1", + "@radix-ui/rect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", + "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", + "integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", + "integrity": "sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", + "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" }, - "engines": { - "node": ">=0.8.0" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@fortawesome/fontawesome-free": { - "version": "5.15.4", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz", - "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==", - "hasInstallScript": true, - "engines": { - "node": ">=6" + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@highlightjs/cdn-assets": { - "version": "11.8.0", - "resolved": "https://registry.npmjs.org/@highlightjs/cdn-assets/-/cdn-assets-11.8.0.tgz", - "integrity": "sha512-gkfCH4xGBGY9xPaW+t26WpgnfpDhNhB5RtVUDLx3MHkC7ZrmKeIxXsfjzOiuOnEgRk+vydlY6XeOeglh+eVhyg==", - "engines": { - "node": ">=12.0.0" + "node_modules/@radix-ui/react-use-rect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", + "integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/rect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", + "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", + "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", + "integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.5.1.tgz", + "integrity": "sha512-jIsuhfgy8GqA67PdWqg73ZB2LFE+HD9hjWL1L6ifEIZVyZVAKpYmgUG4WsKQ005aEyImJmbuimPiEvc57IY0Aw==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.5.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@tanstack/virtual-core": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.5.1.tgz", + "integrity": "sha512-046+AUSiDru/V9pajE1du8WayvBKeCvJ2NmKPy/mR8/SbKKrqmSbj7LJBfXE+nSq4f5TBXvnCzu0kcYebI9WdQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@types/codemirror": { + "version": "5.60.15", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz", + "integrity": "sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA==", + "license": "MIT", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "license": "MIT" + }, "node_modules/@types/hammerjs": { "version": "2.0.41", "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz", "integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA==", "peer": true }, + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/bootstrap": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", @@ -126,6 +1143,51 @@ "color-name": "^1.0.0" } }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/codemirror": { + "version": "5.65.16", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.16.tgz", + "integrity": "sha512-br21LjYmSlVL0vFCPWPfhzUCT34FM/pAdK7rRIZwa0rrtrIdotvP4Oh4GUHsu2E3IrQMCfRkL/fN3ytMNxVQvg==", + "license": "MIT" + }, + "node_modules/codemirror-graphql": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-2.0.12.tgz", + "integrity": "sha512-5UCqhWzck1jClCmRewFb8aSiabnAqiaRfsvIPfmbf6WJvOb8oiefJeHilclPPiZBzY8v/Et6EBMtOeKnWCoyng==", + "license": "MIT", + "dependencies": { + "@types/codemirror": "^0.0.90", + "graphql-language-service": "5.2.1" + }, + "peerDependencies": { + "@codemirror/language": "6.0.0", + "codemirror": "^5.65.3", + "graphql": "^15.5.0 || ^16.0.0" + } + }, + "node_modules/codemirror-graphql/node_modules/@types/codemirror": { + "version": "0.0.90", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.90.tgz", + "integrity": "sha512-8Z9+tSg27NPRGubbUPUCrt5DDG/OWzLph5BvcDykwR5D7RyZh5mhHG0uS1ePKV1YFCA+/cwc4Ey2AJAEFfV3IA==", + "license": "MIT", + "dependencies": { + "@types/tern": "*" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -150,6 +1212,21 @@ "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", "peer": true }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/diff": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", @@ -173,6 +1250,106 @@ "highlight.js": "11.6.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/framer-motion": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz", + "integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==", + "license": "MIT", + "dependencies": { + "@motionone/dom": "10.12.0", + "framesync": "6.0.1", + "hey-listen": "^1.0.8", + "popmotion": "11.0.3", + "style-value-types": "5.0.0", + "tslib": "^2.1.0" + }, + "optionalDependencies": { + "@emotion/is-prop-valid": "^0.8.2" + }, + "peerDependencies": { + "react": ">=16.8 || ^17.0.0 || ^18.0.0", + "react-dom": ">=16.8 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/framesync": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz", + "integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/graphiql": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-3.2.3.tgz", + "integrity": "sha512-b5XuFyTWkORhQkUZULPOPmUXocg+x7HFB53cYEjV7LcH4taB4ViGwmXqHILhfPtv+JcTN80Aw8HELVWSa16iiA==", + "license": "MIT", + "dependencies": { + "@graphiql/react": "^0.22.2", + "@graphiql/toolkit": "^0.9.1", + "graphql-language-service": "^5.2.1", + "markdown-it": "^14.1.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/graphql": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", + "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/graphql-language-service": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-5.2.1.tgz", + "integrity": "sha512-8ewD6otGO43vg2TiEGjoLz3CweTwfaf4ZnqfNREqZXS2JSJGXtsRBOMMknCxMfFVh4x14ql3jyDrXcyAAtbmkQ==", + "license": "MIT", + "dependencies": { + "nullthrows": "^1.0.0", + "vscode-languageserver-types": "^3.17.1" + }, + "bin": { + "graphql": "dist/temp-bin.js" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0" + } + }, + "node_modules/hey-listen": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", + "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==", + "license": "MIT" + }, "node_modules/highlight.js": { "version": "11.6.0", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.6.0.tgz", @@ -194,11 +1371,68 @@ "hulk": "bin/hulk" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-primitive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz", + "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/jquery": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz", "integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==" }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jstree": { "version": "3.3.15", "resolved": "https://registry.npmjs.org/jstree/-/jstree-3.3.15.tgz", @@ -221,6 +1455,67 @@ "integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==", "peer": true }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, + "node_modules/meros": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/meros/-/meros-1.3.0.tgz", + "integrity": "sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w==", + "license": "MIT", + "engines": { + "node": ">=13" + }, + "peerDependencies": { + "@types/node": ">=13" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/mkdirp": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", @@ -252,6 +1547,24 @@ "node": "*" } }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", + "license": "MIT" + }, + "node_modules/popmotion": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz", + "integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==", + "license": "MIT", + "dependencies": { + "framesync": "6.0.1", + "hey-listen": "^1.0.8", + "style-value-types": "5.0.0", + "tslib": "^2.1.0" + } + }, "node_modules/popper.js": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", @@ -262,12 +1575,227 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", + "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/set-value": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz", + "integrity": "sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==", + "funding": [ + "https://github.com/sponsors/jonschlinkert", + "https://paypal.me/jonathanschlinkert", + "https://jonschlinkert.dev/sponsor" + ], + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "is-primitive": "^3.0.1" + }, + "engines": { + "node": ">=11.0" + } + }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "license": "MIT", + "peer": true + }, + "node_modules/style-value-types": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz", + "integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==", + "license": "MIT", + "dependencies": { + "hey-listen": "^1.0.8", + "tslib": "^2.1.0" + } + }, "node_modules/timsort": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==", "peer": true }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "license": "0BSD" + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/use-callback-ref": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", + "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/uuid": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", @@ -327,6 +1855,19 @@ "@egjs/hammerjs": "^2.0.0", "component-emitter": "^1.3.0" } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT", + "peer": true } } } diff --git a/src/web_interface/static/package.json b/src/web_interface/static/package.json index fd80b324d..3390b232e 100644 --- a/src/web_interface/static/package.json +++ b/src/web_interface/static/package.json @@ -1,17 +1,23 @@ { "dependencies": { - "@fortawesome/fontawesome-free": "^5.15.4", + "@fortawesome/fontawesome-free": "^6.5.2", + "@graphiql/plugin-explorer": "^3.0.2", + "@graphiql/toolkit": "^0.9.1", "@highlightjs/cdn-assets": "^11.8.0", "bootstrap": "^4.6.2", "bootstrap-datepicker": "^1.9.0", "bootstrap-select": "^1.13.18", "chart.js": "^2.3.0", "diff2html": "^3.4.18", + "graphiql": "^3.2.3", "jquery": "^3.5.0", + "json5": "^2.2.3", "jstree": "^3.3.12", "jstree-bootstrap-theme": "^1.0.1", "moment": "^2.29.4", "popper.js": "^1.16.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", "vis-network": "^9.1.6" }, "license": "GPL-3.0-only", diff --git a/src/web_interface/templates/base.html b/src/web_interface/templates/base.html index 7b898f662..b0aa54433 100644 --- a/src/web_interface/templates/base.html +++ b/src/web_interface/templates/base.html @@ -49,62 +49,69 @@ {% endblock %} {% block styles %}{% endblock %} @@ -138,6 +145,8 @@ class="fas fa-search"> Basic Search Advanced Search + GraphQL Search Binary Pattern Search {% endblock %} -{% block styles %} - -{% endblock %} - {% block body %} diff --git a/src/web_interface/templates/database/database_browse.html b/src/web_interface/templates/database/database_browse.html index a7c980c94..e1ac2043a 100644 --- a/src/web_interface/templates/database/database_browse.html +++ b/src/web_interface/templates/database/database_browse.html @@ -21,27 +21,29 @@

Browse Firmware Database

-
- - -
+ {% if search_parameters.search_target not in [search_parameters.TargetType.yara, search_parameters.TargetType.graphql] %} +
+ + +
-
- - -
+
+ + +
+ {% endif %}
@@ -65,16 +67,15 @@

Browse Firmware Database

-{% if search_parameters['query_title'] %} +{% if search_parameters.query_title %}
-
{{ search_parameters['query_title'] | render_query_title }}
+
{{ search_parameters.query_title | render_query_title }}
diff --git a/src/web_interface/templates/database/database_graphql.html b/src/web_interface/templates/database/database_graphql.html new file mode 100644 index 000000000..4579daa11 --- /dev/null +++ b/src/web_interface/templates/database/database_graphql.html @@ -0,0 +1,258 @@ +{% extends "base.html" %} + +{% set active_page = "Database" %} + + +{% block head %} + + + + + + + + +{% endblock %} + +{% block styles %} + + + + + +{% endblock %} + + +{% block body %} + +
+ +
+

Database Search with GraphQL

+ + +
+
+ +
+
+
+ +
+ +
+ + + +
+
+
+ + + {% for table, query in tables.items() %} + {# help modal #} +
+ {% endfor %} + +

Example queries:

+ + {% set examples = { + "firmware": [ + ( + '{"device_class": {"_eq": "router"}}', + 'Find firmware with device class "router"', + ), + ( + '{"vendor": {"_ilike": "%link%"}}', + 'Find firmware whose vendor name contains the string "link" (case-insensitive substring)', + ), + ], + "analysis": [ + ( + '{"plugin": {"_eq": "file_type"}, "result": {"_contains": {"mime": "text/plain"}}}', + 'Find files with MIME type "text/plain"', + ), + ( + '{"plugin": {"_eq": "software_components"}, "summary": {"_contains": ["OpenSSL 1.0.1e"]}}', + 'Find files where software OpenSSL matched in version 1.0.1e', + ), + ], + "file_object": [ + ( + '{"file_name": {"_eq": "busybox"}}', + 'Find files with MIME type "text/plain"', + ), + ( + '{"FilePathsByFile": {"file_path": {"_like": "/etc/init.d/%"}}}', + 'Find files whose path starts with "/etc/init.d/"', + ), + ( + '{"is_firmware": {"_eq": true}, "includedFiles": {"file_object": {"file_name": {"_eq": "busybox"}}}}', + 'Find firmware that contains a file with the name "busybox"', + ), + ], + } %} + {% for table, example_list in examples.items() %} +
On table "{{ table }}":
+ {% for code, description in example_list %} +

+ {% set _id = "example-" + table + "-" + loop.index | string %} + {{ code | safe }} + +
+ {{ description }} +

+ {% endfor %} + {% endfor %} + +
+
+

GraphiQL query editor:

+
Loading...
+ + +
+
+ +{% endblock %}