diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..7b3585981 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +FROM nestybox/ubuntu-noble-systemd-docker@sha256:8b1c4409fe89bc110e1e468767074fe4403ba6bb2d1b34881fec5df8b6c2f9c3 AS fact_base + +ARG FACT_DIR=/opt/fact +COPY src $FACT_DIR +WORKDIR $FACT_DIR + +RUN --mount=type=cache,target=/var/cache/apt \ + --mount=type=cache,target=/var/lib/apt \ + apt-get update && \ + apt-get install -y --no-install-recommends \ + curl \ + python3-venv \ + postgresql-client \ + redis-tools + +RUN python3 -m venv venv +ARG VENV_DIR=$FACT_DIR/venv/bin +ENV PATH=$VENV_DIR:$PATH \ + VIRTUAL_ENV=$VENV_DIR \ + PYTHONPATH=$FACT_DIR \ + FACT_INSTALLER_SKIP_DOCKER=1 + +RUN --mount=type=cache,target=/var/cache/apt \ + --mount=type=cache,target=/var/lib/apt \ + ./install/pre_install.sh -D + +FROM fact_base AS fact_frontend + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + python3 install.py -F -H + +RUN chown -R admin:admin "$FACT_DIR" + +COPY --chown=admin docker/entrypoint_frontend.sh . + +ENTRYPOINT ["./entrypoint_frontend.sh"] + +FROM fact_base AS fact_backend + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + python3 install.py -B + +RUN chown -R admin:admin "$FACT_DIR" + +COPY --chown=admin docker/entrypoint_backend.sh . + +# This file serves as a flag to indicate that the backend installation of the docker containers is completed +RUN touch DOCKER_INSTALL_INCOMPLETE +# We must still install the docker images, so we need to overwrite the flag now: +ENV FACT_INSTALLER_SKIP_DOCKER=0 + +ENTRYPOINT ["./entrypoint_backend.sh"] diff --git a/README.md b/README.md index 7c08103e9..978aa9486 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,30 @@ our [tutorial](https://github.com/fkie-cad/FACT_core/blob/master/INSTALL.vagrant ### Docker -There is also a dockerized version, but it is currently unmaintained. -(see the [FACT_docker](https://github.com/fkie-cad/FACT_docker) repo for more information). +>[!IMPORTANT] +> The docker image requires Sysbox as Docker runtime. +> Sysbox installation is described [here](https://github.com/nestybox/sysbox/tree/master?tab=readme-ov-file#installation) +> Please make sure that it works before trying to run FACT with docker by running the hello-world image: +> `docker run --rm --runtime sysbox-runc hello-world` + +If you have untracked files in the directory or also installed FACT locally, create a `.dockerignore`, so you don't +copy the files into the Docker image when building it: + +```shell +# exclude untracked files: +git ls-files . --exclude-standard --others --directory > .dockerignore +# exclude ignored files: +git ls-files . --ignored --exclude-standard --others --directory >> .dockerignore +# also exclude the .git folder: +echo ".git/" >> .dockerignore +``` + +Running FACT with docker compose: + +```shell +docker compose build +docker compose up +``` ## Usage diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..93f7cc6e1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,119 @@ +services: + fact-frontend: + runtime: sysbox-runc + build: + context: . + target: fact_frontend + environment: + PGPASSWORD: password + HASURA_HOST: hasura + HASURA_PORT: 8080 + ports: + - "5000:5000" + networks: + fact: + aliases: + - frontend + depends_on: + db: + condition: service_healthy + restart: true + redis: + condition: service_started + fact-backend: + runtime: sysbox-runc + build: + context: . + target: fact_backend + environment: + PGPASSWORD: password + networks: + fact: + aliases: + - backend + volumes: + - fact_files:/media/data + - fact_docker_images:/var/lib/docker + depends_on: + db: + condition: service_healthy + restart: true + redis: + condition: service_started + db: + container_name: postgres + image: postgres:17 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + PGDATA: /data/postgres + volumes: + - fact_db:/data/postgres + networks: + fact: + aliases: + - db + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d postgres"] + interval: 5s + retries: 5 + start_period: 5s + timeout: 10s + redis: + container_name: redis + image: redis:alpine + restart: always + networks: + fact: + aliases: + - redis + hasura: + image: hasura/graphql-engine:v2.38.0 + ports: + - "18080:8080" + restart: always + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + HASURA_GRAPHQL_METADATA_DATABASE_URL: postgresql://postgres:password@db:5432/postgres + PG_DATABASE_URL: postgresql://postgres:password@db:5432/postgres + HASURA_GRAPHQL_ENABLE_CONSOLE: "true" + HASURA_GRAPHQL_CONSOLE_ASSETS_DIR: /srv/console-assets + FACT_DB_URL: "postgresql://postgres:password@db:5432/fact_db" + HASURA_GRAPHQL_ADMIN_SECRET: "${HASURA_ADMIN_SECRET:-4dM1n_S3cR3T_changemeplz}" + HASURA_GRAPHQL_UNAUTHORIZED_ROLE: "ro_user" + depends_on: + hasura-connector: + condition: service_healthy + networks: + fact: + aliases: + - hasura + hasura-connector: + image: hasura/graphql-data-connector:v2.38.0 + restart: always + ports: + - "18081: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 + networks: + fact: + aliases: + - hasura-connector + +networks: + fact: + driver: bridge + +volumes: + fact_db: + fact_files: + fact_docker_images: diff --git a/docker/entrypoint_backend.sh b/docker/entrypoint_backend.sh new file mode 100755 index 000000000..8ba07152a --- /dev/null +++ b/docker/entrypoint_backend.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -eux + +# start docker service +dockerd & + +# redis runs in a different container => replace "localhost" with "redis" +sed -i 's/host = "localhost"/host = "redis"/g' /opt/fact/config/fact-core-config.toml +# postgres also runs in a different container => replace "localhost" with "db" +sed -i 's/server = "localhost"/server = "db"/g' /opt/fact/config/fact-core-config.toml + +if [ -e DOCKER_INSTALL_INCOMPLETE ]; then + echo "Installing FACT docker images..." + python3 install.py --backend-docker-images + rm DOCKER_INSTALL_INCOMPLETE + echo "FACT docker image installation completed" +fi + +# We can't use rest/status here, because it needs the list of available plugins (which is available after the backend +# was started). +until curl -s -X GET 'http://frontend:5000/rest/statistics/general'; do + echo "Waiting for FACT frontend to start..." + sleep 2 +done +echo "FACT frontend is ready" + +python3 start_fact_backend.py diff --git a/docker/entrypoint_frontend.sh b/docker/entrypoint_frontend.sh new file mode 100755 index 000000000..20330dd6c --- /dev/null +++ b/docker/entrypoint_frontend.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -eux + +# start docker service +dockerd & + +# redis runs in a different container => replace "localhost" with "redis" +sed -i '/^\[common\.redis\]/,/^\[/{ /host = "localhost"/s//host = "redis"/ }' /opt/fact/config/fact-core-config.toml +# postgres also runs in a different container => replace "localhost" with "db" +sed -i 's/server = "localhost"/server = "db"/g' /opt/fact/config/fact-core-config.toml +# switch to correct hasura host/port +sed -i '/^\[frontend\.hasura\]/,/^\[/{ /host = "localhost"/s//host = "hasura"/ }' /opt/fact/config/fact-core-config.toml +sed -i '/^\[frontend\.hasura\]/,/^\[/{ /port = 33333/s//port = 8080/ }' /opt/fact/config/fact-core-config.toml +# replace localhost with 0.0.0.0 in uWSGI config so that the frontend can be reached from outside +sed -i 's/127.0.0.1/0.0.0.0/g' /opt/fact/config/uwsgi_config.ini + +# init the DB +python3 init_postgres.py + +# init hasura +python3 storage/graphql/hasura/init_hasura.py + +python3 start_fact_frontend.py --no-radare diff --git a/src/config.py b/src/config.py index 9f3e0d41d..8ae3bb314 100644 --- a/src/config.py +++ b/src/config.py @@ -113,6 +113,7 @@ class Authentication(BaseModel): class Hasura(BaseModel): model_config = ConfigDict(extra='forbid') admin_secret: str + host: str = 'localhost' port: int = 33_333 diff --git a/src/config/fact-core-config.toml b/src/config/fact-core-config.toml index fe5d3f4e6..cb316ed52 100644 --- a/src/config/fact-core-config.toml +++ b/src/config/fact-core-config.toml @@ -183,4 +183,6 @@ password-salt = "5up3r5tr0n6_p455w0rd_5417" [frontend.hasura] +host = "localhost" +port = 33333 admin-secret = "4dM1n_S3cR3T_changemeplz" diff --git a/src/init_postgres.py b/src/init_postgres.py index 7f8b6bf2c..337dc3a0f 100755 --- a/src/init_postgres.py +++ b/src/init_postgres.py @@ -43,9 +43,7 @@ def user_exists(user_name: str, host: str, port: str | int) -> bool: def create_admin_user(user_name: str, password: str, host: str, port: int | str): execute_psql_command( - # fmt: off - (f"CREATE USER {user_name} WITH PASSWORD '{password}' " 'LOGIN SUPERUSER INHERIT CREATEDB CREATEROLE;'), - # fmt: on + f"CREATE USER {user_name} WITH PASSWORD '{password}' LOGIN SUPERUSER INHERIT CREATEDB CREATEROLE;", host=host, port=port, ) diff --git a/src/install.py b/src/install.py index 2cc90543a..c3c588faf 100755 --- a/src/install.py +++ b/src/install.py @@ -43,7 +43,7 @@ PROGRAM_VERSION = '1.2' PROGRAM_DESCRIPTION = 'Firmware Analysis and Comparison Tool (FACT) installation script' -FACT_INSTALLER_SKIP_DOCKER = os.getenv('FACT_INSTALLER_SKIP_DOCKER') +FACT_INSTALLER_SKIP_DOCKER = bool(int(os.getenv('FACT_INSTALLER_SKIP_DOCKER', '0'))) def _setup_argparser(): @@ -169,7 +169,7 @@ def install(): welcome() none_chosen = not (args.frontend or args.db or args.backend or args.common) # TODO maybe replace this with an cli argument - skip_docker = FACT_INSTALLER_SKIP_DOCKER is not None + skip_docker = FACT_INSTALLER_SKIP_DOCKER # Note that the skip_docker environment variable overrides the cli argument only_docker = not skip_docker and none_chosen and (args.backend_docker_images or args.frontend_docker_images) diff --git a/src/storage/graphql/hasura/init_hasura.py b/src/storage/graphql/hasura/init_hasura.py index 2bf7c9082..e7a32252b 100644 --- a/src/storage/graphql/hasura/init_hasura.py +++ b/src/storage/graphql/hasura/init_hasura.py @@ -48,7 +48,9 @@ class HasuraInitError(Exception): 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' + host = config.frontend.hasura.host + port = config.frontend.hasura.port + self.url = f'http://{host}:{port}/v1/metadata' self.headers = { 'Content-Type': 'application/json', 'X-Hasura-Role': 'admin', diff --git a/src/storage/graphql/interface.py b/src/storage/graphql/interface.py index be879572c..f549d2bf5 100644 --- a/src/storage/graphql/interface.py +++ b/src/storage/graphql/interface.py @@ -84,7 +84,7 @@ class GraphQlInterface: def __init__(self): if config.frontend is None: config.load() - url = f'http://localhost:{config.frontend.hasura.port}/v1/graphql' + url = f'http://{config.frontend.hasura.host}:{config.frontend.hasura.port}/v1/graphql' headers = { 'Content-Type': 'application/json', 'X-Hasura-Role': 'admin', diff --git a/src/web_interface/components/database_routes.py b/src/web_interface/components/database_routes.py index bb4d00901..46bd9e2fe 100644 --- a/src/web_interface/components/database_routes.py +++ b/src/web_interface/components/database_routes.py @@ -404,7 +404,7 @@ def proxy_graphql(self): 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', + url=f'http://{config.frontend.hasura.host}:{config.frontend.hasura.port}/v1/graphql', headers={**req_headers, 'X-Hasura-Role': 'ro_user'}, data=request.get_data(), cookies=request.cookies,