diff --git a/docker/_tests/README.md b/docker/_tests/README.md index b1940c2e..e3cc526e 100644 --- a/docker/_tests/README.md +++ b/docker/_tests/README.md @@ -12,6 +12,7 @@ not touch production `data/` or `data_backup/` paths. ./docker/_tests/run.sh ./docker/_tests/run.sh adbhoney ./docker/_tests/run.sh ciscoasa +./docker/_tests/run.sh citrixhoneypot ``` Common options: @@ -27,6 +28,7 @@ Individual tests can also be run directly: ./docker/_tests/tests/adbhoney.sh ./docker/_tests/tests/adbhoney.sh --image dtagdevsec/adbhoney:24.04 --host-port 15555 ./docker/_tests/tests/ciscoasa.sh --https-port 18443 --ike-port 15000 +./docker/_tests/tests/citrixhoneypot.sh --https-port 1443 ``` ## Conventions diff --git a/docker/_tests/tests/citrixhoneypot.sh b/docker/_tests/tests/citrixhoneypot.sh new file mode 100755 index 00000000..1b76323b --- /dev/null +++ b/docker/_tests/tests/citrixhoneypot.sh @@ -0,0 +1,305 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../lib/common.sh +source "${SCRIPT_DIR}/../lib/common.sh" + +TEST_NAME="citrixhoneypot" +DEFAULT_IMAGE="dtagdevsec/citrixhoneypot:24.04" +IMAGE="" +HTTPS_PORT="443" +LOG_DIR="" +MAPPED_HTTPS_PORT="" +REQUEST_PATH="" + +usage() { + cat < "${TEST_HARNESS_COMPOSE}" <&1)"; then + printf '%s\n' "${output}" + return 0 + fi + sleep 1 + done + + printf '%s\n' "${output}" >&2 + return 1 +} + +wait_for_log_event() { + local token="$1" + local path="$2" + + python3 - "${LOG_DIR}" "${token}" "${path}" "${TEST_TIMEOUT}" <<'PY' +import json +import sys +import time +from pathlib import Path + +log_dir = Path(sys.argv[1]) +token = sys.argv[2] +path = sys.argv[3] +timeout = int(sys.argv[4]) +deadline = time.monotonic() + timeout +last_error = None + + +def contains_probe(value): + if isinstance(value, str): + return token in value or path in value + if isinstance(value, dict): + return any(contains_probe(item) for item in value.values()) + if isinstance(value, list): + return any(contains_probe(item) for item in value) + return False + + +while time.monotonic() < deadline: + files = sorted(item for item in log_dir.rglob("*") if item.is_file()) + for log_file in files: + try: + lines = log_file.read_text(encoding="utf-8", errors="replace").splitlines() + except OSError as exc: + last_error = f"Could not read {log_file}: {exc}" + continue + + for line_number, line in enumerate(lines, 1): + stripped = line.strip() + if stripped: + try: + event = json.loads(stripped) + except json.JSONDecodeError: + event = None + if event is not None and contains_probe(event): + print(f"Structured log event found in {log_file}:{line_number}") + sys.exit(0) + + if token in line or path in line: + print(f"Log text found in {log_file}:{line_number}") + sys.exit(0) + + if not files: + last_error = f"No log files found in {log_dir}" + time.sleep(1) + +if last_error: + print(last_error, file=sys.stderr) +print(f"No CitrixHoneypot log entry found for token {token}", file=sys.stderr) +sys.exit(1) +PY +} + +assert_no_runtime_errors() { + if grep -R -E "Traceback|NameError|Exception" "${LOG_DIR}" >/dev/null 2>&1; then + test_die "CitrixHoneypot runtime error found in logs" + fi +} + +main() { + parse_args "$@" + validate_args + test_check_dependencies + + if [[ -z "${IMAGE}" ]]; then + IMAGE="$(test_read_compose_image "${TEST_NAME}" "${DEFAULT_IMAGE}")" + fi + + test_info "Using image: ${IMAGE}" + test_require_image "${IMAGE}" "docker compose -f docker/${TEST_NAME}/docker-compose.yml build ${TEST_NAME}" + + if (( HTTPS_PORT < 1024 )); then + test_info "Skipping user-space preflight for privileged port ${HTTPS_PORT}; Docker will validate the binding." + else + test_ensure_port_free "${TEST_BIND_IP}" "${HTTPS_PORT}" || test_die "${TEST_BIND_IP}:${HTTPS_PORT} is already in use. Try --https-port ." + fi + + prepare_citrixhoneypot_harness + test_enable_cleanup + + test_info "Starting isolated CitrixHoneypot container" + test_compose up -d --no-build >/dev/null + + test_wait_for_container || test_die "CitrixHoneypot container did not stay running" + test_ok "Container is running" + + MAPPED_HTTPS_PORT="$(test_get_mapped_port "${TEST_NAME}" "443")" || test_die "Could not resolve mapped host port for 443/tcp" + test_ok "Port ${TEST_BIND_IP}:${MAPPED_HTTPS_PORT} maps to container port 443/tcp" + + local token="citrixhoneypot-test-$(date +%s)-$$" + + test_info "Running CVE-shaped HTTPS probe with token: ${token}" + run_https_probe_with_retries "${token}" || test_die "HTTPS probe failed on ${TEST_BIND_IP}:${HTTPS_PORT}" + test_wait_for_container || test_die "CitrixHoneypot container stopped after HTTPS probe" + + test_info "Waiting for probe entry in CitrixHoneypot logs" + wait_for_log_event "${token}" "${REQUEST_PATH}" || test_die "Probe token or request path was not found in CitrixHoneypot logs" + test_ok "Probe was written to CitrixHoneypot logs" + + assert_no_runtime_errors + test_ok "No CitrixHoneypot runtime errors found in logs" + + test_ok "CitrixHoneypot post-build smoke test completed successfully" +} + +main "$@" diff --git a/docker/citrixhoneypot/Dockerfile b/docker/citrixhoneypot/Dockerfile index 3c855048..275808fe 100644 --- a/docker/citrixhoneypot/Dockerfile +++ b/docker/citrixhoneypot/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.20 AS builder +FROM alpine:3.23 AS builder # # Install packages RUN apk --no-cache -U upgrade && \ @@ -30,7 +30,7 @@ RUN git clone https://github.com/t3chn0m4g3/CitrixHoneypot /opt/citrixhoneypot & WORKDIR /opt/citrixhoneypot RUN pyinstaller CitrixHoneypot.py # -FROM alpine:3.20 +FROM alpine:3.23 RUN apk --no-cache -U upgrade COPY --from=builder /opt/citrixhoneypot/dist/CitrixHoneypot/ /opt/citrixhoneypot COPY --from=builder /opt/citrixhoneypot/ssl /opt/citrixhoneypot/ssl @@ -40,4 +40,4 @@ COPY --from=builder /opt/citrixhoneypot/responses/ /opt/citrixhoneypot/responses STOPSIGNAL SIGINT USER 2000:2000 WORKDIR /opt/citrixhoneypot/ -CMD nohup ./CitrixHoneypot +CMD ["./CitrixHoneypot"]