Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/ci-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ jobs:
- name: Start the stack (gated on health)
run: docker compose up -d --wait --wait-timeout 600

# Debug: images and versions actually run, and the state of each service, also when the start fails
- name: Show running images and state
if: always()
run: |
docker compose images
echo
docker compose ps -a --format 'table {{.Service}}\t{{.Image}}\t{{.State}}\t{{.Status}}'

# TheHive Flow has no container healthcheck (distroless image): poll its readiness endpoint
- name: TheHive Flow readiness
run: |
Expand All @@ -48,6 +56,9 @@ jobs:
echo "TheHive Flow is not ready"
exit 1

- name: TheHive Flow connection test (connector, JWT authentication)
run: bash ./tests/flow.sh

- name: Smoke test (org / user / case / alert workflow)
run: bash ./tests/smoke.sh

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# Ignore custom configuration files for TheHive
/*/thehive/config/index.conf
/*/thehive/config/secret.conf
/*/thehive/config/flow.conf

# Ignore custom configuration files for Cortex
/*/cortex/config/index.conf
Expand Down
6 changes: 3 additions & 3 deletions prod1-thehive/scripts/check_permissions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ fi
NON_COMPLIANT_DIRS=$(find ./cassandra ./certificates ./elasticsearch ./nginx ./scripts ./thehive -type d ! -perm 750)

## List non-executable files with unexpected permissions (should be 644)
NON_COMPLIANT_FILES=$(find ./docker-compose.yml ./dot.env.template ./cassandra ./certificates ./elasticsearch ./nginx ./thehive -type f ! -name '*.key' ! -perm 644)
NON_COMPLIANT_FILES=$(find ./docker-compose.yml ./dot.env.template ./cassandra ./certificates ./elasticsearch ./nginx ./thehive -type f ! -name '*.key' ! -path ./thehive/config/flow.conf ! -perm 644)

## List executable files with unexpected permissions (should be 755)
NON_COMPLIANT_EXECUTABLE_FILES=$(find ./scripts -type f ! -perm 755)

## List private keys with unexpected permissions (should be 600)
NON_COMPLIANT_KEY_FILES=$(find ./certificates ./nginx -type f -name '*.key' ! -perm 600)
## List private keys and TheHive Flow connection settings (JWT signing key) with unexpected permissions (should be 600)
NON_COMPLIANT_KEY_FILES=$(find ./certificates ./nginx ./thehive/config -type f \( -name '*.key' -o -path ./thehive/config/flow.conf \) ! -perm 600)

if [ -z "${NON_COMPLIANT_DIRS}" ] &&\
[ -z "${NON_COMPLIANT_FILES}" ] &&\
Expand Down
19 changes: 17 additions & 2 deletions prod1-thehive/thehive/config/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,20 @@ stream {
scalligraph.modules += org.thp.thehive.connector.cortex.CortexModule
# Enable MISP connector
scalligraph.modules += org.thp.thehive.connector.misp.MispModule
# Enable TheHive Flow connector (requires TheHive 6, the TheHive Flow URL and the JWT signing key)
#scalligraph.modules += org.thp.thehive.connector.flow.FlowModule
# Enable TheHive Flow connector (TheHive 6):
# 1. Create thehive/config/flow.conf, readable by its owner only (chmod 600) and never committed:
# it holds the JWT signing key. Put the following lines in it:
# flow.url = "https://<flow_host>/"
# flow.jwt_signing_key = "<jwt_signing_key>"
# features.Orchestrator.enabled = true
# scalligraph.modules += org.thp.thehive.connector.flow.FlowModule
# flow.url: nginx address of TheHive Flow host, matching its TLS certificate. The address TheHive
# calls from must be in nginx_thehive_addresses of TheHive Flow .env file, otherwise nginx returns 403.
# flow.jwt_signing_key: jwt_signing_key value of TheHive Flow .env file.
# 2. With the self-signed certificate of TheHive Flow, also add to flow.conf:
# flow.wsConfig.ssl.trustManager.stores = [{ type: "PEM", path: "/etc/thehive/thehive-flow-nginx.crt" }]
# and copy nginx/certs/server.crt of TheHive Flow host to thehive/config/thehive-flow-nginx.crt.
# INSECURE alternative, for test environments only: flow.wsConfig.ssl.loose.acceptAnyCertificate = true
# disables certificate verification of TheHive Flow.
# 3. Uncomment the line below, then restart TheHive:
#include required("/etc/thehive/flow.conf")
6 changes: 3 additions & 3 deletions prod2-thehive/scripts/check_permissions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ fi
NON_COMPLIANT_DIRS=$(find ./cassandra ./certificates ./elasticsearch ./nginx ./scripts ./thehive -type d ! -perm 750)

## List non-executable files with unexpected permissions (should be 644)
NON_COMPLIANT_FILES=$(find ./docker-compose.yml ./dot.env.template ./cassandra ./certificates ./elasticsearch ./nginx ./thehive -type f ! -name '*.key' ! -perm 644)
NON_COMPLIANT_FILES=$(find ./docker-compose.yml ./dot.env.template ./cassandra ./certificates ./elasticsearch ./nginx ./thehive -type f ! -name '*.key' ! -path ./thehive/config/flow.conf ! -perm 644)

## List executable files with unexpected permissions (should be 755)
NON_COMPLIANT_EXECUTABLE_FILES=$(find ./scripts -type f ! -perm 755)

## List private keys with unexpected permissions (should be 600)
NON_COMPLIANT_KEY_FILES=$(find ./certificates ./nginx -type f -name '*.key' ! -perm 600)
## List private keys and TheHive Flow connection settings (JWT signing key) with unexpected permissions (should be 600)
NON_COMPLIANT_KEY_FILES=$(find ./certificates ./nginx ./thehive/config -type f \( -name '*.key' -o -path ./thehive/config/flow.conf \) ! -perm 600)

if [ -z "${NON_COMPLIANT_DIRS}" ] &&\
[ -z "${NON_COMPLIANT_FILES}" ] &&\
Expand Down
19 changes: 17 additions & 2 deletions prod2-thehive/thehive/config/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,20 @@ stream {
scalligraph.modules += org.thp.thehive.connector.cortex.CortexModule
# Enable MISP connector
scalligraph.modules += org.thp.thehive.connector.misp.MispModule
# Enable TheHive Flow connector (requires TheHive 6, the TheHive Flow URL and the JWT signing key)
#scalligraph.modules += org.thp.thehive.connector.flow.FlowModule
# Enable TheHive Flow connector (TheHive 6):
# 1. Create thehive/config/flow.conf, readable by its owner only (chmod 600) and never committed:
# it holds the JWT signing key. Put the following lines in it:
# flow.url = "https://<flow_host>/"
# flow.jwt_signing_key = "<jwt_signing_key>"
# features.Orchestrator.enabled = true
# scalligraph.modules += org.thp.thehive.connector.flow.FlowModule
# flow.url: nginx address of TheHive Flow host, matching its TLS certificate. The address TheHive
# calls from must be in nginx_thehive_addresses of TheHive Flow .env file, otherwise nginx returns 403.
# flow.jwt_signing_key: jwt_signing_key value of TheHive Flow .env file.
# 2. With the self-signed certificate of TheHive Flow, also add to flow.conf:
# flow.wsConfig.ssl.trustManager.stores = [{ type: "PEM", path: "/etc/thehive/thehive-flow-nginx.crt" }]
# and copy nginx/certs/server.crt of TheHive Flow host to thehive/config/thehive-flow-nginx.crt.
# INSECURE alternative, for test environments only: flow.wsConfig.ssl.loose.acceptAnyCertificate = true
# disables certificate verification of TheHive Flow.
# 3. Uncomment the line below, then restart TheHive:
#include required("/etc/thehive/flow.conf")
2 changes: 1 addition & 1 deletion testing/scripts/check_permissions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ NON_COMPLIANT_CORTEX_DIRS=$(find ./cortex -type d ! -perm 755)

## List non-executable files with unexpected permissions (should be 644)
NON_COMPLIANT_FILES=$(find ./docker-compose.yml ./dot.env.template ./cassandra ./certificates ./cortex ./elasticsearch ./nginx ./thehive ./postgresql ./s3-store ./temporal ./thehive-flow \
\( "${FLOW_DATA_DIRS[@]}" \) -prune -o -type f ! -path ./thehive-flow/secret/thehive-api-key ! -path ./temporal/config/temporal-server.yaml ! -name '*.key' ! -perm 644 -print)
\( "${FLOW_DATA_DIRS[@]}" \) -prune -o -type f ! -path ./thehive-flow/secret/thehive-api-key ! -path ./temporal/config/temporal-server.yaml ! -path ./thehive/config/flow.conf ! -name '*.key' ! -perm 644 -print)

## List executable files with unexpected permissions (should be 755)
NON_COMPLIANT_EXECUTABLE_FILES=$(find ./scripts -type f ! -perm 755)
Expand Down
14 changes: 14 additions & 0 deletions testing/scripts/init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,20 @@ _EOF_

## INIT THEHIVE FLOW
render_temporal_config
## TheHive connection to TheHive Flow, included by thehive/config/application.conf
THEHIVEFLOWFILE="./thehive/config/flow.conf"
if [[ ${#JWT_SIGNING_KEY} -lt 32 || "${JWT_SIGNING_KEY}" == *[\"\\]* ]]; then
error "Invalid jwt_signing_key in ${ENVFILE}: at least 32 characters, without quotes or backslashes (is openssl installed?)"
exit 1
fi
## Created readable by its owner only: the file holds the JWT signing key
( umask 077 && cat > "${THEHIVEFLOWFILE}" ) << _EOF_ || { error "Cannot write ${THEHIVEFLOWFILE}"; exit 1; }
flow.url = "http://thehive-flow:8081"
flow.jwt_signing_key = "${JWT_SIGNING_KEY}"
features.Orchestrator.enabled = true
scalligraph.modules += org.thp.thehive.connector.flow.FlowModule
_EOF_
chmod 600 "${THEHIVEFLOWFILE}" || { error "Cannot set permissions on ${THEHIVEFLOWFILE}"; exit 1; }
## TheHive Flow does not start without a TheHive API key: use a placeholder until one is created in TheHive
THEHIVE_API_KEY_FILE="./thehive-flow/secret/thehive-api-key"
if [[ ! -s "${THEHIVE_API_KEY_FILE}" ]]
Expand Down
2 changes: 1 addition & 1 deletion testing/scripts/reset.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ then
rm ./thehive/config/secret.conf
rm ./cortex/config/index.conf
rm ./cortex/config/secret.conf
rm -f ./temporal/config/temporal-server.yaml ./thehive-flow/secret/thehive-api-key
rm -f ./temporal/config/temporal-server.yaml ./thehive-flow/secret/thehive-api-key ./thehive/config/flow.conf
success "index and secret files deleted."

## DELETE cert files
Expand Down
111 changes: 111 additions & 0 deletions testing/tests/flow.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env bash

## TheHive <-> TheHive Flow connection test, run from the testing directory.
## Checks that TheHive loads the TheHive Flow connector, that TheHive Flow rejects
## unsigned requests, and that TheHive authenticates to TheHive Flow with the
## shared JWT signing key (thehive/config/flow.conf and .env, written by init.sh).

set -uo pipefail

BASE_URL="${BASE_URL:-https://127.0.0.1/thehive}"
CURL_OPTS="${CURL_OPTS:--sk --connect-timeout 5 --max-time 15}"
ADMIN_USER="${ADMIN_USER:-admin@thehive.local}"
ADMIN_PASSWORD="${ADMIN_PASSWORD:-secret}"
TIMEOUT="${TIMEOUT:-180}"
CONFIG_PATH="/api/v1/config/llm-providers"

info() { echo "[INFO] $1" >&2; }
success() { echo "[SUCCESS] $1" >&2; }
error() { echo "[ERROR] $1" >&2; }

# shellcheck disable=SC2086
curl_api() { curl $CURL_OPTS "$@"; }

check_prerequisites() {
for cmd in curl jq docker; do
command -v "$cmd" >/dev/null 2>&1 || { error "$cmd not found"; exit 1; }
done
docker compose ps --status running -q thehive-flow 2>/dev/null | grep -q . \
|| { error "thehive-flow is not running (run from the testing directory, after docker compose up)"; exit 1; }
}

## features.Orchestrator.enabled from flow.conf is applied
check_feature() {
info "Checking the Orchestrator feature in TheHive status..."
local body code
body=$(curl_api -w '\n%{http_code}' -u "${ADMIN_USER}:${ADMIN_PASSWORD}" "${BASE_URL}/api/v1/status" 2>/dev/null) || true
code=${body##*$'\n'}
body=${body%$'\n'*}
if [ "${code}" != "200" ]; then
error "TheHive status returned HTTP ${code:-000} (expected 200)"
return 1
fi
if echo "${body}" | jq -e '.features | type == "array" and index("Orchestrator") != null' >/dev/null 2>&1; then
success "TheHive reports the Orchestrator feature"
else
error "Orchestrator absent from TheHive features $(echo "${body}" | jq -c '.features' 2>/dev/null): check thehive/config/flow.conf"
return 1
fi
}

## Without a JWT, TheHive Flow refuses its configuration endpoint.
## The probe uses GET, so it is never mistaken for TheHive configuration push (PUT).
check_unsigned_rejected() {
info "Checking that TheHive Flow rejects requests without a JWT..."
local code
code=$(docker compose exec -T nginx curl -s --connect-timeout 5 --max-time 10 -o /dev/null -w '%{http_code}' \
"http://thehive-flow:8081${CONFIG_PATH}" 2>/dev/null) || true
code=${code:-000}
if [ "${code}" = "401" ]; then
success "TheHive Flow rejects unsigned requests (HTTP 401)"
else
error "Unsigned request to TheHive Flow returned HTTP ${code} (expected 401)"
return 1
fi
}

## TheHive signs its requests to TheHive Flow with the shared key: its health probe every minute
## (GET /api/v1/version) and its configuration push (PUT). Only requests since TheHive's last start
## count, so a stack reused after a key change is judged on its current key.
check_thehive_authenticated() {
info "Waiting for TheHive to authenticate to TheHive Flow (up to ${TIMEOUT}s)..."
local deadline=$(( $(date +%s) + TIMEOUT )) since requests
since=$(docker inspect --format '{{.State.StartedAt}}' "$(docker compose ps -q thehive)" 2>/dev/null)
[ -n "${since}" ] || { error "Cannot read TheHive container start time"; return 1; }
while [ "$(date +%s)" -lt "${deadline}" ]; do
requests=$(docker compose logs --no-log-prefix --since "${since}" thehive-flow 2>/dev/null \
| jq -Rrc --arg path "${CONFIG_PATH}" 'fromjson? | select(.msg == "HTTP request handled")
| select((.http_method == "GET" and .url_path == "/api/v1/version") or (.http_method == "PUT" and .url_path == $path))
| "\(.http_method) \(.url_path) \(.http_status_code)"' 2>/dev/null)
if grep -qE ' (401|403)$' <<< "${requests}"; then
error "TheHive signed request rejected by TheHive Flow ($(grep -E ' (401|403)$' <<< "${requests}" | tail -n 1)): flow.jwt_signing_key in thehive/config/flow.conf does not match jwt_signing_key in .env"
return 1
fi
if grep -q "^PUT ${CONFIG_PATH} 204$" <<< "${requests}"; then
success "TheHive configuration accepted by TheHive Flow (signed request, HTTP 204)"
return 0
fi
sleep 5
done
error "No signed request from TheHive accepted by TheHive Flow since TheHive start (seen: ${requests:-none}): check flow.url and the connector"
return 1
}

main() {
info "=== TheHive Flow connection test ==="
check_prerequisites
local failed=0
check_feature || failed=1
check_unsigned_rejected || failed=1
check_thehive_authenticated || failed=1
echo "" >&2
if [ "${failed}" -eq 0 ]; then
success "=== TheHive Flow connection test PASSED ==="
exit 0
else
error "=== TheHive Flow connection test FAILED ==="
exit 1
fi
}

main
4 changes: 4 additions & 0 deletions testing/thehive/config/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@ stream {
scalligraph.modules += org.thp.thehive.connector.cortex.CortexModule
# Enable MISP connector
scalligraph.modules += org.thp.thehive.connector.misp.MispModule
# Enable TheHive Flow connector: flow.url, flow.jwt_signing_key, features.Orchestrator.enabled and
# the FlowModule connector, generated by scripts/init.sh with the JWT signing key of .env.
# Required: TheHive does not start without it (run scripts/init.sh)
include required("/etc/thehive/flow.conf")