From b1b822daeedf22a72324a56789c4aff4ab9dc605 Mon Sep 17 00:00:00 2001 From: Paolo Scattolin Date: Thu, 24 Sep 2026 13:40:13 +0200 Subject: [PATCH 1/5] feat: add HTTPS support for GUI deployments Allow Vite and Nginx to serve the GUI over HTTPS while keeping backend certificate verification configurable for local self-signed certificates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .env.example | 18 ++++++++-- .gitignore | 1 + README.md | 40 +++++++++++++++------ docker-compose.dev.yml | 6 ++++ docker-compose.yml | 16 ++++++--- nginx.conf | 15 ++++++-- vite.config.ts | 79 +++++++++++++++++++++++++++++++----------- 7 files changed, 136 insertions(+), 39 deletions(-) diff --git a/.env.example b/.env.example index a0ab8b0..ba0c8cf 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,11 @@ # copy these lines into `.env.development` or `.env` (see Vite docs at https://vite.dev/guide/env-and-mode) and adjust as needed for local development VITE_BACKEND_URL=http://localhost:8080 # backend API URL used by the frontend (dev server proxy target) VITE_SERVER_PORT=5173 # dev server port +VITE_ENABLE_HTTPS=false # set true to serve the GUI over HTTPS +VITE_HTTPS_CERT_FILE= # required when VITE_ENABLE_HTTPS=true +VITE_HTTPS_KEY_FILE= # required when VITE_ENABLE_HTTPS=true +VITE_VERIFY_SSL=false # set true to verify the backend TLS certificate +VITE_USE_POLLING=false # set true when the filesystem does not emit file events reliably ########## Docker Compose (dev) ########## # Values used by `docker-compose.dev.yml`. Compose will substitute ${VAR} from the @@ -18,10 +23,19 @@ VITE_SERVER_PORT=5173 # dev server port VITE_APP_VERSION=development VITE_BACKEND_URL=http://host.docker.internal:8080 # backend API URL used by the frontend inside the container (resolved via Compose host mapping) VITE_SERVER_PORT=5173 # dev server port inside the container +VITE_ENABLE_HTTPS=false # set true to serve the GUI over HTTPS +VITE_HTTPS_CERT_FILE=/app/certs/server.crt # certificate mounted from ./certs +VITE_HTTPS_KEY_FILE=/app/certs/server.key # certificate key mounted from ./certs +VITE_VERIFY_SSL=false # set true to verify the backend TLS certificate DOCKER_HOST_PORT=5173 # port on the host machine mapped to the container's VITE_SERVER_PORT (for browser access) ########## Production (nginx) ########## -# Values used at container runtime by nginx to proxy /api/ -NGINX_BACKEND_URL=http://host.docker.internal:8080/ # backend API URL used by nginx inside the container (resolved via Compose host mapping) +# Values used at container runtime by nginx to serve HTTPS and proxy /api/. +SOARCA_GUI_VERSION=latest # Docker Hub image tag +SOARCA_CERTS_VOLUME=soarca_certs_data_container # external volume created by SOARCA +NGINX_BACKEND_URL=https://host.docker.internal:8080/ # backend API URL used by nginx inside the container NGINX_SERVER_PORT=8081 # port nginx listens on inside the container +NGINX_CERT_FILE=/app/certs/server.crt # TLS certificate mounted from SOARCA_CERTS_VOLUME +NGINX_CERT_KEY_FILE=/app/certs/server.key # TLS certificate key mounted from SOARCA_CERTS_VOLUME +NGINX_PROXY_SSL_VERIFY=off # set to on when the backend certificate is trusted DOCKER_HOST_PORT=8081 # port on the host machine mapped to the container's NGINX_SERVER_PORT (for browser access) \ No newline at end of file diff --git a/.gitignore b/.gitignore index b58fd44..6c53f85 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ lerna-debug.log* node_modules dist +certs dist-ssr *.local *.tsbuildinfo diff --git a/README.md b/README.md index 3229e2d..f43e637 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,10 @@ flowchart TB - `VITE_BACKEND_URL` - Backend API URL (proxied by Vite dev server) - `VITE_SERVER_PORT` - Port where Vite dev server listens +- `VITE_ENABLE_HTTPS` - Set to `true` to serve the development or preview build over HTTPS +- `VITE_HTTPS_CERT_FILE` - Path to the TLS certificate used by Vite +- `VITE_HTTPS_KEY_FILE` - Path to the TLS certificate key used by Vite +- `VITE_VERIFY_SSL` - Set to `true` to verify the backend certificate when Vite proxies API requests - `DOCKER_HOST_PORT` - The port of the host machine that is mapped to the `VITE_SERVER_PORT` of the Vite dev server running in the container. This is only relevant when running the Development Container. - `HOST_PORT` - is the same of the `VITE_SERVER_PORT` if run locally or `DOCKER_HOST_PORT` if running the Development Container. @@ -125,7 +129,12 @@ flowchart TB If no enviroment variables are provided, the defaults are: - `VITE_BACKEND_URL`: `http://localhost:8080` - (SOARCA default) - - `VITE_SERVER_PORT`: `5713` - (Vite default) + - `VITE_SERVER_PORT`: `5173` - (Vite default) + - `VITE_ENABLE_HTTPS`: `false` + + To enable HTTPS, set `VITE_ENABLE_HTTPS=true` and provide + `VITE_HTTPS_CERT_FILE` and `VITE_HTTPS_KEY_FILE`. Set + `VITE_BACKEND_URL=https://localhost:8080` when SOARCA also uses HTTPS. 3. **Start the dev server:** @@ -145,6 +154,10 @@ flowchart TB - `VITE_SERVER_PORT`: `5173` - `DOCKER_HOST_PORT` — port on the host mapped to the `VITE_SERVER_PORT` (default: `5173`) + For HTTPS, place `server.crt` and `server.key` in `./certs`, then set + `VITE_ENABLE_HTTPS=true`. The development Compose file mounts that directory + read-only at `/app/certs`. + 2. **Start the container:** ```bash docker compose -f docker-compose.dev.yml up @@ -257,10 +270,14 @@ flowchart TB **Environment variables involved:** -- `VITE_BACKEND_URL` - Backend API URL embedded into the client bundle at build time (must be set before running `npm run build`) - `VITE_APP_VERSION` - Application version embedded into the client bundle at build time (optional, falls back to git describe or "development") - `NGINX_BACKEND_URL` - Backend API URL used by Nginx at runtime to proxy `/api/*` requests. This is only relevant when running in Docker Container Mode. - `NGINX_SERVER_PORT` - Port where Nginx listens inside the container. This is only relevant when running in Docker Container Mode. +- `NGINX_CERT_FILE` - TLS certificate path inside the container +- `NGINX_CERT_KEY_FILE` - TLS certificate key path inside the container +- `NGINX_PROXY_SSL_VERIFY` - Controls verification of the backend TLS certificate +- `SOARCA_CERTS_VOLUME` - External Docker volume containing the SOARCA TLS certificate and key +- `SOARCA_GUI_VERSION` - Docker Hub image tag to run, defaulting to `latest` - `DOCKER_HOST_PORT` - The port of the host machine that is mapped to the `NGINX_SERVER_PORT` of the Nginx server running in the container. This is only relevant when running in Docker Container Mode. - `HOST_PORT` - Port 4173 (Vite default) for local preview mode (`npm run preview`) or `DOCKER_HOST_PORT` when running in Docker Container Mode. @@ -276,7 +293,7 @@ flowchart TB Optionally create a `.env` file (explanation and defaults can be found in `.env.example`). Note that `VITE_` variables must be set at _build time_ to be embedded into the static bundle. If no environment variables are provided, the defaults are: - - `VITE_BACKEND_URL`: `http://localhost:8080` - (SOARCA default) + - `VITE_ENABLE_HTTPS`: `false` 3. **Build and preview:** @@ -293,21 +310,24 @@ flowchart TB #### Running in Docker Container (recommended for production) 1. **Configure environment variables:** - Optionally provide values via the shell, a project `.env`, or `--env-file` when starting Compose (explanation and defaults can be found in `.env.example`). Note that `VITE_` variables are build-time only (embedded at image build), while `NGINX_` variables are runtime (used by the running container). + Optionally provide values via the shell, a project `.env`, or `--env-file` when starting Compose (explanation and defaults can be found in `.env.example`). The production container serves HTTPS and expects the certificate volume created by the SOARCA deployment. If no environment variables are provided, the defaults are: - - `VITE_BACKEND_URL`: `http://localhost:8080` - embedded at build time - - `NGINX_BACKEND_URL`: `http://host.docker.internal:8080/` - used at runtime + - `SOARCA_GUI_VERSION`: `latest` + - `SOARCA_CERTS_VOLUME`: `soarca_certs_data_container` + - `NGINX_BACKEND_URL`: `https://host.docker.internal:8080/` - `NGINX_SERVER_PORT`: `8081` - - `DOCKER_HOST_PORT`: `8081` — port on the host mapped to the `NGINX_SERVER_PORT` + - `NGINX_PROXY_SSL_VERIFY`: `off`, to support SOARCA's generated self-signed certificate + - `DOCKER_HOST_PORT`: `8081` - port on the host mapped to the `NGINX_SERVER_PORT` -2. **Build and start the container:** +2. **Pull and start the container:** ```bash - docker compose up --build + docker compose pull + docker compose up -d ``` - The app will be available at `http://localhost:DOCKER_HOST_PORT`. + The app will be available at `https://localhost:DOCKER_HOST_PORT`. ### NPM Scripts for local run diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 6254a2c..a4ad789 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -16,8 +16,14 @@ services: - NODE_ENV=development - VITE_BACKEND_URL=${VITE_BACKEND_URL:-http://host.docker.internal:8080} - VITE_SERVER_PORT=${VITE_SERVER_PORT:-5173} + - VITE_ENABLE_HTTPS=${VITE_ENABLE_HTTPS:-false} + - VITE_HTTPS_CERT_FILE=${VITE_HTTPS_CERT_FILE:-/app/certs/server.crt} + - VITE_HTTPS_KEY_FILE=${VITE_HTTPS_KEY_FILE:-/app/certs/server.key} + - VITE_VERIFY_SSL=${VITE_VERIFY_SSL:-false} + - VITE_IS_DOCKER=true volumes: - .:/app + - ./certs:/app/certs:ro - node_modules_volume:/app/node_modules command: sh -c "npm install && npm run dev" diff --git a/docker-compose.yml b/docker-compose.yml index ee604c9..5352e6f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: soarca-gui: container_name: soarca-gui - image: soarca-gui:latest + image: cossas/soarca-gui:${SOARCA_GUI_VERSION:-latest} extra_hosts: - "host.docker.internal:host-gateway" build: @@ -9,12 +9,20 @@ services: dockerfile: Dockerfile args: VITE_APP_VERSION: ${VITE_APP_VERSION:-development} - NGINX_BACKEND_URL: ${NGINX_BACKEND_URL:-http://host.docker.internal:8080/} - NGINX_SERVER_PORT: ${NGINX_SERVER_PORT:-8081} ports: - "${DOCKER_HOST_PORT:-8081}:${NGINX_SERVER_PORT:-8081}" + volumes: + - soarca-certs:/app/certs:ro environment: - NODE_ENV=production - - NGINX_BACKEND_URL=${NGINX_BACKEND_URL:-http://host.docker.internal:8080/} + - NGINX_BACKEND_URL=${NGINX_BACKEND_URL:-https://host.docker.internal:8080/} - NGINX_SERVER_PORT=${NGINX_SERVER_PORT:-8081} + - NGINX_CERT_FILE=${NGINX_CERT_FILE:-/app/certs/server.crt} + - NGINX_CERT_KEY_FILE=${NGINX_CERT_KEY_FILE:-/app/certs/server.key} + - NGINX_PROXY_SSL_VERIFY=${NGINX_PROXY_SSL_VERIFY:-off} restart: unless-stopped + +volumes: + soarca-certs: + external: true + name: ${SOARCA_CERTS_VOLUME:-soarca_certs_data_container} diff --git a/nginx.conf b/nginx.conf index 372c1f2..3a9bb1e 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,11 +1,20 @@ # Nginx template: variables are substituted at container start (envsubst) # Variables expected: # - ${NGINX_SERVER_PORT} (default: 8081): port nginx will listen on inside the container -# - ${NGINX_BACKEND_URL} (default: http://host.docker.internal:8080/): backend to proxy /api/ to (include trailing slash) +# - ${NGINX_BACKEND_URL} (default: https://host.docker.internal:8080/): backend to proxy /api/ to (include trailing slash) +# - ${NGINX_CERT_FILE} (default: /app/certs/server.crt): TLS certificate file +# - ${NGINX_CERT_KEY_FILE} (default: /app/certs/server.key): TLS certificate key file +# - ${NGINX_PROXY_SSL_VERIFY} (default: off): verify the backend TLS certificate server { - listen ${NGINX_SERVER_PORT}; + listen ${NGINX_SERVER_PORT} ssl; server_name localhost; + ssl_certificate ${NGINX_CERT_FILE}; + ssl_certificate_key ${NGINX_CERT_KEY_FILE}; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + location / { root /usr/share/nginx/html; index index.html; @@ -14,6 +23,8 @@ server { location /api/ { proxy_pass ${NGINX_BACKEND_URL}; + proxy_ssl_verify ${NGINX_PROXY_SSL_VERIFY}; + proxy_ssl_server_name on; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/vite.config.ts b/vite.config.ts index 73a10ee..7397178 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,10 @@ import react from "@vitejs/plugin-react-swc"; import { execSync } from "child_process"; +import fs from "node:fs"; +import type { ServerOptions as HttpsServerOptions } from "node:https"; +import path from "node:path"; import sbom from "rollup-plugin-sbom"; -import { defineConfig } from "vite"; +import { defineConfig, loadEnv } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; /** @@ -27,27 +30,61 @@ const getAppVersion = (): string => { return "development"; }; -export default defineConfig({ - plugins: [react(), tsconfigPaths(), sbom()], - define: { - __APP_VERSION__: JSON.stringify(getAppVersion()), - }, - server: { - proxy: { - "/api": { - target: process.env.VITE_BACKEND_URL || "http://localhost:8080", - rewrite: (path) => path.replace(/^\/api/, ""), +const getHttpsOptions = ( + enabled: boolean, + certFile?: string, + keyFile?: string, +): HttpsServerOptions | undefined => { + if (!enabled) { + return undefined; + } + + if (!certFile || !keyFile) { + throw new Error( + "VITE_HTTPS_CERT_FILE and VITE_HTTPS_KEY_FILE are required when VITE_ENABLE_HTTPS=true.", + ); + } + + return { + cert: fs.readFileSync(path.resolve(certFile)), + key: fs.readFileSync(path.resolve(keyFile)), + }; +}; + +export default defineConfig(({ mode }) => { + const env = { ...loadEnv(mode, process.cwd(), ""), ...process.env }; + const https = getHttpsOptions( + env.VITE_ENABLE_HTTPS === "true", + env.VITE_HTTPS_CERT_FILE, + env.VITE_HTTPS_KEY_FILE, + ); + + return { + plugins: [react(), tsconfigPaths(), sbom()], + define: { + __APP_VERSION__: JSON.stringify(getAppVersion()), + }, + server: { + proxy: { + "/api": { + target: env.VITE_BACKEND_URL || "http://localhost:8080", + secure: env.VITE_VERIFY_SSL === "true", + rewrite: (requestPath) => requestPath.replace(/^\/api/, ""), + }, + }, + host: true, + port: Number(env.VITE_SERVER_PORT) || 5173, + strictPort: true, + https, + watch: { + usePolling: + env.VITE_USE_POLLING === "true" || env.VITE_IS_DOCKER === "true", }, }, - host: true, - port: Number(process.env.VITE_SERVER_PORT) || 5173, - strictPort: true, // Fail if port is already in use, as the docker container won't be able to use a different one - watch: { - usePolling: true, + preview: { + host: true, + port: 4173, + https, }, - }, - preview: { - host: true, - port: 4173, - }, + }; }); From 864f454201c4c5e7ce008987b53fb672391f07d2 Mon Sep 17 00:00:00 2001 From: Paolo Scattolin Date: Thu, 24 Sep 2026 13:40:21 +0200 Subject: [PATCH 2/5] ci: publish React release images to Docker Hub --- .github/workflows/ci.yml | 2 +- .github/workflows/development-release.yml | 3 +- .github/workflows/release.yml | 139 ++++++++-------------- Dockerfile | 6 +- 4 files changed, 57 insertions(+), 93 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d94a1d1..bf380b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,5 +92,5 @@ jobs: target: production tags: cossas/soarca-gui:pr-${{ env.describe_version }} build-args: | - VERSION=${{ env.describe_version }} + VITE_APP_VERSION=${{ env.describe_version }} builder: default diff --git a/.github/workflows/development-release.yml b/.github/workflows/development-release.yml index 32b2fe2..c63ace4 100644 --- a/.github/workflows/development-release.yml +++ b/.github/workflows/development-release.yml @@ -62,7 +62,6 @@ jobs: - name: Get version run: | - export VERSION=$(git describe --tags --dirty) echo "describe_version=$(git describe --tags --dirty)" >> "$GITHUB_ENV" - name: Build and push @@ -70,7 +69,7 @@ jobs: with: context: . build-args: | - VERSION=${{ env.describe_version }} + VITE_APP_VERSION=${{ env.describe_version }} push: true target: production tags: cossas/soarca-gui:${{ env.describe_version }},cossas/soarca-gui:development diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e51142f..b725735 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,9 @@ on: tags: - '[0-9]+.[0-9]+.[0-9]+\-?*' +permissions: + contents: read + jobs: build: name: Build with npm @@ -14,8 +17,7 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 - - name: Make repo safe - run: git config --global --add safe.directory /__w/SOARCA/SOARCA-GUI + - name: Use Node.js 24 uses: actions/setup-node@v6 with: @@ -37,123 +39,84 @@ jobs: - name: Build with npm run: npm run build - - name: "Upload Artifact" + - name: Prepare release SBOM + run: | + cp dist/cyclonedx/bom.json "soarca-gui-${GITHUB_REF_NAME}.cdx.json" + zip sbom.zip "soarca-gui-${GITHUB_REF_NAME}.cdx.json" + + - name: Upload SBOM artifact uses: actions/upload-artifact@v6 with: - name: ${{ github.sha }} - path: dist/* + name: release-sbom + path: sbom.zip + if-no-files-found: error retention-days: 1 docker-build: needs: build - name: Build docker image and push it to docker hub + name: Build Docker image and push it to Docker Hub runs-on: ubuntu-latest steps: - - name: Checkout Code + - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - - name: Make repo safe - run: git config --global --add safe.directory /__w/SOARCA-GUI/SOARCA-GUI - name: Set up QEMU uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Download dist - uses: actions/download-artifact@v7 - with: - pattern: ${{ github.sha }} - - - name: Move files to bin folder and make executable - run: | - mkdir -p bin - mv ${{ github.sha }}/* ./bin/ - chmod +x bin/soarca-gui-* - - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USER }} password: ${{ secrets.DOCKER_HUB_TOKEN }} - - name: Get version - run: | - export VERSION=$(git describe --tags --dirty) - echo "describe_version=$(git describe --tags --dirty)" >> "$GITHUB_ENV" - - name: Build and push uses: docker/build-push-action@v6 with: context: . build-args: | - VERSION=${{ env.describe_version }} - push: false - tags: cossas/soarca-gui:${{ env.describe_version }},cossas/soarca-gui:latest - - release-artifacts: + VITE_APP_VERSION=${{ github.ref_name }} + push: true + target: production + tags: | + cossas/soarca-gui:${{ github.ref_name }} + cossas/soarca-gui:latest + + publish-sbom: needs: build - name: Create release artifacts + name: Publish SBOM to the GitHub release runs-on: ubuntu-latest + permissions: + contents: write steps: - - name: Setup Go - uses: actions/setup-go@v4 - with: - go-version: "1.23.x" - - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v6 - with: - gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - passphrase: ${{ secrets.GPG_PASSPHRASE }} - - name: Checkout Code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Make repo safe - run: git config --global --add safe.directory /__w/SOARCA-GUI/SOARCA-GUI - - - name: Setup node - uses: actions/setup-node@v6 + - name: Download SBOM artifact + uses: actions/download-artifact@v7 with: - node-version: 24 - - run: npm ci - - - name: Build and sbom - run: | - go install github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@latest - go install github.com/a-h/templ/cmd/templ@v0.3.819 - make sbom - zip -r bin/sbom.zip bin + name: release-sbom - - name: Release soarca gui binary - uses: goreleaser/goreleaser-action@v5 - with: - distribution: goreleaser - version: latest - args: release --clean - workdir: server + - name: Create GitHub release and publish SBOM env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} - - - name: Upload release sbom - uses: actions/github-script@v4 - with: - script: | - const fs = require('fs'); - const tag = context.ref.replace("refs/tags/", ""); - // Get release for this tag - const release = await github.repos.getReleaseByTag({ - owner: context.repo.owner, - repo: context.repo.repo, - tag - }); - // Upload the release asset - await github.repos.uploadReleaseAsset({ - owner: context.repo.owner, - repo: context.repo.repo, - release_id: release.data.id, - name: "sbom.zip", - data: await fs.readFileSync("bin/sbom.zip") - }); + GH_TOKEN: ${{ github.token }} + run: | + if ! gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then + if [[ "$GITHUB_REF_NAME" == *-* ]]; then + gh release create "$GITHUB_REF_NAME" \ + --verify-tag \ + --generate-notes \ + --prerelease \ + --title "$GITHUB_REF_NAME" + else + gh release create "$GITHUB_REF_NAME" \ + --verify-tag \ + --generate-notes \ + --title "$GITHUB_REF_NAME" + fi + fi + + gh release upload "$GITHUB_REF_NAME" \ + sbom.zip \ + --clobber diff --git a/Dockerfile b/Dockerfile index 6afb121..050a7c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,10 +2,12 @@ FROM node:24-alpine AS builder RUN apk add --no-cache git WORKDIR /app +ARG VITE_APP_VERSION=development +ENV VITE_APP_VERSION=${VITE_APP_VERSION} COPY package*.json ./ -RUN npm install +RUN npm ci COPY . . -RUN npm run build +RUN npm run build && rm -rf dist/cyclonedx # Serve with nginx FROM nginx:alpine AS production From b639cff4cff7576ec254c014fdcb41fa783ae57a Mon Sep 17 00:00:00 2001 From: Paolo Scattolin Date: Thu, 24 Sep 2026 13:40:40 +0200 Subject: [PATCH 3/5] fix: correct manual action value handling --- .../external-vars-incident-response.json | 125 ++++++++++++++++++ .../external-vars-ip-investigation.json | 95 +++++++++++++ .../timeline-tab-view/ManualActionModal.tsx | 71 +++------- .../timeline-tab-view/TimelineTabView.tsx | 4 +- 4 files changed, 237 insertions(+), 58 deletions(-) create mode 100644 mock-playbooks/external-vars-incident-response.json create mode 100644 mock-playbooks/external-vars-ip-investigation.json diff --git a/mock-playbooks/external-vars-incident-response.json b/mock-playbooks/external-vars-incident-response.json new file mode 100644 index 0000000..9c1cca3 --- /dev/null +++ b/mock-playbooks/external-vars-incident-response.json @@ -0,0 +1,125 @@ +{ + "type": "playbook", + "spec_version": "cacao-2.0", + "id": "playbook--f4a5b6c7-d8e9-4f0a-b1c2-d3e4f5a6b7c8", + "name": "Incident response with host isolation", + "description": "Two-step manual incident response. External variables (incident UUID, affected host, alert count) are supplied at trigger time. Step 1: analyst decides on host isolation. Step 2: analyst documents remediation.", + "playbook_types": ["remediation"], + "created_by": "identity--ac3c0258-7a81-46e7-a2ae-d34b6d06cc54", + "created": "2026-05-28T10:00:00.000Z", + "modified": "2026-05-28T10:00:00.000Z", + "revoked": false, + "valid_from": "2026-05-28T10:00:00.000Z", + "valid_until": "2126-05-28T10:00:00.000Z", + "priority": 1, + "severity": 2, + "impact": 2, + "labels": ["soarca", "manual", "remediation"], + "external_references": [ + { + "name": "COSSAS", + "description": "TNO COSSAS" + } + ], + "playbook_variables": { + "__incident_id__": { + "type": "uuid", + "description": "UUID of the incident in the ticketing system", + "constant": false, + "external": true + }, + "__affected_host__": { + "type": "string", + "description": "Hostname or IP address of the affected system", + "constant": false, + "external": true + }, + "__alert_count__": { + "type": "integer", + "description": "Number of alerts triggered by this incident", + "constant": false, + "external": true + } + }, + "workflow_start": "start--a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c50", + "workflow_exception": "end--b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d60", + "workflow": { + "start--a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c50": { + "type": "start", + "name": "Start incident response", + "on_completion": "action--c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e70" + }, + "action--c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e70": { + "type": "action", + "name": "Confirm host isolation", + "description": "Review incident context and decide whether to isolate the affected host from the network.", + "step_variables": { + "__isolate__": { + "type": "bool", + "description": "Set to true to isolate the host, false to monitor only", + "constant": false, + "external": false + } + }, + "commands": [ + { + "type": "manual", + "command": "Review the incident details and decide whether to isolate the affected host. Consider the alert count and potential business impact before proceeding." + } + ], + "agent": "soarca-manual-capability--7b0e98db-fa93-42aa-8511-e871c65131b1", + "targets": ["individual--9d1f6217-34d5-435c-b29a-6a1af6b664d9"], + "out_args": ["__isolate__"], + "on_completion": "action--d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f80", + "timeout": 300000 + }, + "action--d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f80": { + "type": "action", + "name": "Document remediation", + "description": "Record all remediation actions taken and close the incident.", + "step_variables": { + "__remediation_notes__": { + "type": "string", + "description": "Summary of remediation actions taken", + "constant": false, + "external": false + }, + "__incident_closed__": { + "type": "bool", + "description": "Set to true when the incident is fully resolved", + "constant": false, + "external": false + } + }, + "commands": [ + { + "type": "manual", + "command": "Document all remediation steps taken and confirm whether the incident is fully resolved." + } + ], + "agent": "soarca-manual-capability--7b0e98db-fa93-42aa-8511-e871c65131b1", + "targets": ["individual--9d1f6217-34d5-435c-b29a-6a1af6b664d9"], + "out_args": ["__remediation_notes__", "__incident_closed__"], + "on_completion": "end--b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d60", + "timeout": 300000 + }, + "end--b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d60": { + "type": "end", + "name": "End incident response" + } + }, + "agent_definitions": { + "soarca-manual-capability--7b0e98db-fa93-42aa-8511-e871c65131b1": { + "type": "soarca-manual", + "name": "soarca-manual", + "description": "SOARCAs manual command handler" + } + }, + "target_definitions": { + "individual--9d1f6217-34d5-435c-b29a-6a1af6b664d9": { + "type": "individual", + "name": "SOC Analyst", + "description": "Security Operations Center analyst" + } + } +} diff --git a/mock-playbooks/external-vars-ip-investigation.json b/mock-playbooks/external-vars-ip-investigation.json new file mode 100644 index 0000000..a9dccd5 --- /dev/null +++ b/mock-playbooks/external-vars-ip-investigation.json @@ -0,0 +1,95 @@ +{ + "type": "playbook", + "spec_version": "cacao-2.0", + "id": "playbook--3a7c9b2e-4f1d-4c8a-b5e6-9d0a2c4e7f81", + "name": "Suspicious IP investigation", + "description": "Investigate a suspicious IP address. External variables (IP and priority) are supplied at trigger time. An analyst manually triages and records the result.", + "playbook_types": ["investigation"], + "created_by": "identity--ac3c0258-7a81-46e7-a2ae-d34b6d06cc54", + "created": "2026-05-28T10:00:00.000Z", + "modified": "2026-05-28T10:00:00.000Z", + "revoked": false, + "valid_from": "2026-05-28T10:00:00.000Z", + "valid_until": "2126-05-28T10:00:00.000Z", + "priority": 1, + "severity": 1, + "impact": 1, + "labels": ["soarca", "manual", "investigation"], + "external_references": [ + { + "name": "COSSAS", + "description": "TNO COSSAS" + } + ], + "playbook_variables": { + "__target_ip__": { + "type": "ipv4-addr", + "description": "Suspicious IPv4 address to investigate", + "constant": false, + "external": true + }, + "__priority__": { + "type": "integer", + "description": "Investigation priority level (1 = low, 10 = critical)", + "constant": false, + "external": true + } + }, + "workflow_start": "start--c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", + "workflow_exception": "end--d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", + "workflow": { + "start--c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d": { + "type": "start", + "name": "Start IP investigation", + "on_completion": "action--e3c4d5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f" + }, + "action--e3c4d5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f": { + "type": "action", + "name": "Analyst IP triage", + "description": "Review the suspicious IP address and decide whether to escalate. Check threat intelligence feeds and firewall logs.", + "step_variables": { + "__escalate__": { + "type": "bool", + "description": "Set to true to escalate, false to close the investigation", + "constant": false, + "external": false + }, + "__analyst_notes__": { + "type": "string", + "description": "Analyst notes about the IP address", + "constant": false, + "external": false + } + }, + "commands": [ + { + "type": "manual", + "command": "Review the suspicious IP and determine if escalation is needed. Check threat intelligence feeds and firewall logs for activity from this address." + } + ], + "agent": "soarca-manual-capability--7b0e98db-fa93-42aa-8511-e871c65131b1", + "targets": ["individual--9d1f6217-34d5-435c-b29a-6a1af6b664d9"], + "out_args": ["__escalate__", "__analyst_notes__"], + "on_completion": "end--d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", + "timeout": 300000 + }, + "end--d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e": { + "type": "end", + "name": "End IP investigation" + } + }, + "agent_definitions": { + "soarca-manual-capability--7b0e98db-fa93-42aa-8511-e871c65131b1": { + "type": "soarca-manual", + "name": "soarca-manual", + "description": "SOARCAs manual command handler" + } + }, + "target_definitions": { + "individual--9d1f6217-34d5-435c-b29a-6a1af6b664d9": { + "type": "individual", + "name": "SOC Analyst", + "description": "Security Operations Center analyst" + } + } +} diff --git a/src/pages/main-page/monitoring-page/timeline-tab-view/ManualActionModal.tsx b/src/pages/main-page/monitoring-page/timeline-tab-view/ManualActionModal.tsx index bec1a15..806c878 100644 --- a/src/pages/main-page/monitoring-page/timeline-tab-view/ManualActionModal.tsx +++ b/src/pages/main-page/monitoring-page/timeline-tab-view/ManualActionModal.tsx @@ -9,7 +9,7 @@ import toast from "react-hot-toast"; import { getStepManualData, postStepActionResult } from "@/api/manual"; import { formatErrorForToast } from "@/api/utils"; -import { Button, Modal, RadioGroup, Spinner, ThemeVariant } from "@/components"; +import { Button, Modal, Spinner, ThemeVariant } from "@/components"; import { Execution, ManualOutArgsUpdatePayload, @@ -69,9 +69,6 @@ export const ManualActionModal: React.FC = ({ const [variableValues, setVariableValues] = useState>( {}, ); - const [variableChoices, setVariableChoices] = useState< - Record - >({}); const { data: interactionData, isLoading: isLoadingOutArgs } = useQuery({ queryKey: ["manual", executionId, activeStep?.step_id], @@ -96,10 +93,6 @@ export const ManualActionModal: React.FC = ({ }, }); - const handleChoiceChange = (key: string, value: ActionType) => { - setVariableChoices((prev) => ({ ...prev, [key]: value })); - }; - const handleValueChange = (key: string, value: string) => { setVariableValues((prev) => ({ ...prev, [key]: value })); }; @@ -111,23 +104,12 @@ export const ManualActionModal: React.FC = ({ const outArgs = interactionData?.out_args ?? {}; Object.entries(outArgs).forEach(([key, variable]) => { - const hasExistingValue = variable?.value && variable.value.trim() !== ""; - - if (hasExistingValue) { - response_out_args[key] = { - ...variable, - name: variable?.name || key, - type: variable?.type || "string", - value: variable.value, - }; - } else { - response_out_args[key] = { - ...variable, - name: variable?.name || key, - type: variable?.type || "string", - value: variableValues[key] || "", - }; - } + response_out_args[key] = { + ...variable, + name: variable?.name || key, + type: variable?.type || "string", + value: variableValues[key] ?? variable?.value ?? "", + }; }); const responseStatus: ManualResponseStatus = @@ -161,8 +143,6 @@ export const ManualActionModal: React.FC = ({ isLoadingOutArgs={isLoadingOutArgs} mutation={mutation} variableValues={variableValues} - variableChoices={variableChoices} - onChoiceChange={handleChoiceChange} onValueChange={handleValueChange} /> @@ -208,8 +188,6 @@ interface ModalContentProps { unknown >; variableValues: Record; - variableChoices: Record; - onChoiceChange: (key: string, value: ActionType) => void; onValueChange: (key: string, value: string) => void; } @@ -219,8 +197,6 @@ const ModalContent: React.FC = ({ isLoadingOutArgs, mutation, variableValues, - variableChoices, - onChoiceChange, onValueChange, }) => { const outArgEntries = Object.entries(outArgs); @@ -254,8 +230,6 @@ const ModalContent: React.FC = ({ Variables {outArgEntries.map(([key, variable]) => { - const hasExistingValue = - variable?.value && variable.value.trim() !== ""; const varType = variable?.type || "string"; return ( @@ -269,30 +243,15 @@ const ModalContent: React.FC = ({ )} - {hasExistingValue ? ( - - onChoiceChange(key, value as ActionType) - } - $disabled={mutation.isPending} + + - ) : ( - - - - )} + ); })} diff --git a/src/pages/main-page/monitoring-page/timeline-tab-view/TimelineTabView.tsx b/src/pages/main-page/monitoring-page/timeline-tab-view/TimelineTabView.tsx index 52f441c..3e28e36 100644 --- a/src/pages/main-page/monitoring-page/timeline-tab-view/TimelineTabView.tsx +++ b/src/pages/main-page/monitoring-page/timeline-tab-view/TimelineTabView.tsx @@ -102,12 +102,12 @@ export const TimelineTabView: React.FC = ({ const isStepTerminated = status !== StepExecutionStatus.Running; const isStepManualAndOngoing = - !isStepTerminated && !automated_execution; + !isStepTerminated && automated_execution === false; // Determine if we should display the user action cards showing user responses. // We only display it if the step is not automated, it has been completed successfully. const shouldDisplayUserActionResponseCard = - isStepTerminated && !automated_execution; + isStepTerminated && automated_execution === false; const userResponses = shouldDisplayUserActionResponseCard ? getUserResponses(step) From 9c87f2868a1a460b756aaedce585648aee6ee678 Mon Sep 17 00:00:00 2001 From: Paolo Scattolin Date: Thu, 24 Sep 2026 13:54:16 +0200 Subject: [PATCH 4/5] fix: remove example playbooks --- .../external-vars-incident-response.json | 125 ------------------ .../external-vars-ip-investigation.json | 95 ------------- 2 files changed, 220 deletions(-) delete mode 100644 mock-playbooks/external-vars-incident-response.json delete mode 100644 mock-playbooks/external-vars-ip-investigation.json diff --git a/mock-playbooks/external-vars-incident-response.json b/mock-playbooks/external-vars-incident-response.json deleted file mode 100644 index 9c1cca3..0000000 --- a/mock-playbooks/external-vars-incident-response.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "type": "playbook", - "spec_version": "cacao-2.0", - "id": "playbook--f4a5b6c7-d8e9-4f0a-b1c2-d3e4f5a6b7c8", - "name": "Incident response with host isolation", - "description": "Two-step manual incident response. External variables (incident UUID, affected host, alert count) are supplied at trigger time. Step 1: analyst decides on host isolation. Step 2: analyst documents remediation.", - "playbook_types": ["remediation"], - "created_by": "identity--ac3c0258-7a81-46e7-a2ae-d34b6d06cc54", - "created": "2026-05-28T10:00:00.000Z", - "modified": "2026-05-28T10:00:00.000Z", - "revoked": false, - "valid_from": "2026-05-28T10:00:00.000Z", - "valid_until": "2126-05-28T10:00:00.000Z", - "priority": 1, - "severity": 2, - "impact": 2, - "labels": ["soarca", "manual", "remediation"], - "external_references": [ - { - "name": "COSSAS", - "description": "TNO COSSAS" - } - ], - "playbook_variables": { - "__incident_id__": { - "type": "uuid", - "description": "UUID of the incident in the ticketing system", - "constant": false, - "external": true - }, - "__affected_host__": { - "type": "string", - "description": "Hostname or IP address of the affected system", - "constant": false, - "external": true - }, - "__alert_count__": { - "type": "integer", - "description": "Number of alerts triggered by this incident", - "constant": false, - "external": true - } - }, - "workflow_start": "start--a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c50", - "workflow_exception": "end--b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d60", - "workflow": { - "start--a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c50": { - "type": "start", - "name": "Start incident response", - "on_completion": "action--c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e70" - }, - "action--c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e70": { - "type": "action", - "name": "Confirm host isolation", - "description": "Review incident context and decide whether to isolate the affected host from the network.", - "step_variables": { - "__isolate__": { - "type": "bool", - "description": "Set to true to isolate the host, false to monitor only", - "constant": false, - "external": false - } - }, - "commands": [ - { - "type": "manual", - "command": "Review the incident details and decide whether to isolate the affected host. Consider the alert count and potential business impact before proceeding." - } - ], - "agent": "soarca-manual-capability--7b0e98db-fa93-42aa-8511-e871c65131b1", - "targets": ["individual--9d1f6217-34d5-435c-b29a-6a1af6b664d9"], - "out_args": ["__isolate__"], - "on_completion": "action--d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f80", - "timeout": 300000 - }, - "action--d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f80": { - "type": "action", - "name": "Document remediation", - "description": "Record all remediation actions taken and close the incident.", - "step_variables": { - "__remediation_notes__": { - "type": "string", - "description": "Summary of remediation actions taken", - "constant": false, - "external": false - }, - "__incident_closed__": { - "type": "bool", - "description": "Set to true when the incident is fully resolved", - "constant": false, - "external": false - } - }, - "commands": [ - { - "type": "manual", - "command": "Document all remediation steps taken and confirm whether the incident is fully resolved." - } - ], - "agent": "soarca-manual-capability--7b0e98db-fa93-42aa-8511-e871c65131b1", - "targets": ["individual--9d1f6217-34d5-435c-b29a-6a1af6b664d9"], - "out_args": ["__remediation_notes__", "__incident_closed__"], - "on_completion": "end--b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d60", - "timeout": 300000 - }, - "end--b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d60": { - "type": "end", - "name": "End incident response" - } - }, - "agent_definitions": { - "soarca-manual-capability--7b0e98db-fa93-42aa-8511-e871c65131b1": { - "type": "soarca-manual", - "name": "soarca-manual", - "description": "SOARCAs manual command handler" - } - }, - "target_definitions": { - "individual--9d1f6217-34d5-435c-b29a-6a1af6b664d9": { - "type": "individual", - "name": "SOC Analyst", - "description": "Security Operations Center analyst" - } - } -} diff --git a/mock-playbooks/external-vars-ip-investigation.json b/mock-playbooks/external-vars-ip-investigation.json deleted file mode 100644 index a9dccd5..0000000 --- a/mock-playbooks/external-vars-ip-investigation.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "type": "playbook", - "spec_version": "cacao-2.0", - "id": "playbook--3a7c9b2e-4f1d-4c8a-b5e6-9d0a2c4e7f81", - "name": "Suspicious IP investigation", - "description": "Investigate a suspicious IP address. External variables (IP and priority) are supplied at trigger time. An analyst manually triages and records the result.", - "playbook_types": ["investigation"], - "created_by": "identity--ac3c0258-7a81-46e7-a2ae-d34b6d06cc54", - "created": "2026-05-28T10:00:00.000Z", - "modified": "2026-05-28T10:00:00.000Z", - "revoked": false, - "valid_from": "2026-05-28T10:00:00.000Z", - "valid_until": "2126-05-28T10:00:00.000Z", - "priority": 1, - "severity": 1, - "impact": 1, - "labels": ["soarca", "manual", "investigation"], - "external_references": [ - { - "name": "COSSAS", - "description": "TNO COSSAS" - } - ], - "playbook_variables": { - "__target_ip__": { - "type": "ipv4-addr", - "description": "Suspicious IPv4 address to investigate", - "constant": false, - "external": true - }, - "__priority__": { - "type": "integer", - "description": "Investigation priority level (1 = low, 10 = critical)", - "constant": false, - "external": true - } - }, - "workflow_start": "start--c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", - "workflow_exception": "end--d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", - "workflow": { - "start--c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d": { - "type": "start", - "name": "Start IP investigation", - "on_completion": "action--e3c4d5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f" - }, - "action--e3c4d5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f": { - "type": "action", - "name": "Analyst IP triage", - "description": "Review the suspicious IP address and decide whether to escalate. Check threat intelligence feeds and firewall logs.", - "step_variables": { - "__escalate__": { - "type": "bool", - "description": "Set to true to escalate, false to close the investigation", - "constant": false, - "external": false - }, - "__analyst_notes__": { - "type": "string", - "description": "Analyst notes about the IP address", - "constant": false, - "external": false - } - }, - "commands": [ - { - "type": "manual", - "command": "Review the suspicious IP and determine if escalation is needed. Check threat intelligence feeds and firewall logs for activity from this address." - } - ], - "agent": "soarca-manual-capability--7b0e98db-fa93-42aa-8511-e871c65131b1", - "targets": ["individual--9d1f6217-34d5-435c-b29a-6a1af6b664d9"], - "out_args": ["__escalate__", "__analyst_notes__"], - "on_completion": "end--d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", - "timeout": 300000 - }, - "end--d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e": { - "type": "end", - "name": "End IP investigation" - } - }, - "agent_definitions": { - "soarca-manual-capability--7b0e98db-fa93-42aa-8511-e871c65131b1": { - "type": "soarca-manual", - "name": "soarca-manual", - "description": "SOARCAs manual command handler" - } - }, - "target_definitions": { - "individual--9d1f6217-34d5-435c-b29a-6a1af6b664d9": { - "type": "individual", - "name": "SOC Analyst", - "description": "Security Operations Center analyst" - } - } -} From 4807317c73a2b5595aae237cd9bf476c56b047d4 Mon Sep 17 00:00:00 2001 From: Paolo Scattolin Date: Thu, 24 Sep 2026 15:00:13 +0200 Subject: [PATCH 5/5] fix: drop tls1.2 and use describe --- .github/workflows/release.yml | 8 ++++++-- nginx.conf | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b725735..15385c7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,16 +74,20 @@ jobs: username: ${{ secrets.DOCKER_HUB_USER }} password: ${{ secrets.DOCKER_HUB_TOKEN }} + - name: Get version + run: | + echo "describe_version=$(git describe --tags --dirty)" >> "$GITHUB_ENV" + - name: Build and push uses: docker/build-push-action@v6 with: context: . build-args: | - VITE_APP_VERSION=${{ github.ref_name }} + VITE_APP_VERSION=${{ env.describe_version }} push: true target: production tags: | - cossas/soarca-gui:${{ github.ref_name }} + cossas/soarca-gui:${{ env.describe_version }} cossas/soarca-gui:latest publish-sbom: diff --git a/nginx.conf b/nginx.conf index 3a9bb1e..e6dbeda 100644 --- a/nginx.conf +++ b/nginx.conf @@ -11,7 +11,7 @@ server { ssl_certificate ${NGINX_CERT_FILE}; ssl_certificate_key ${NGINX_CERT_KEY_FILE}; - ssl_protocols TLSv1.2 TLSv1.3; + ssl_protocols TLSv1.3; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m;