mirror of
https://github.com/telekom-security/tpotce.git
synced 2026-05-29 17:24:15 +00:00
add test for conpot, fix ipmi issue on pinned version
This commit is contained in:
parent
d830a6f3cc
commit
a945f865f1
4 changed files with 741 additions and 0 deletions
|
|
@ -13,6 +13,7 @@ not touch production `data/` or `data_backup/` paths.
|
|||
./docker/_tests/run.sh adbhoney
|
||||
./docker/_tests/run.sh ciscoasa
|
||||
./docker/_tests/run.sh citrixhoneypot
|
||||
./docker/_tests/run.sh conpot
|
||||
```
|
||||
|
||||
Common options:
|
||||
|
|
@ -29,6 +30,7 @@ Individual tests can also be run directly:
|
|||
./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
|
||||
./docker/_tests/tests/conpot.sh --guardian-ast-port 11001 --ipmi-port 1623
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
|
|
|||
677
docker/_tests/tests/conpot.sh
Executable file
677
docker/_tests/tests/conpot.sh
Executable file
|
|
@ -0,0 +1,677 @@
|
|||
#!/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="conpot"
|
||||
DEFAULT_IMAGE="dtagdevsec/conpot:24.04"
|
||||
IMAGE=""
|
||||
IEC104_PORT="2404"
|
||||
GUARDIAN_AST_PORT="10001"
|
||||
IPMI_PORT="623"
|
||||
KAMSTRUP_PORT="1025"
|
||||
KAMSTRUP_MANAGEMENT_PORT="50100"
|
||||
LOG_DIR=""
|
||||
|
||||
CONPOT_CONTAINER_IEC104=""
|
||||
CONPOT_CONTAINER_GUARDIAN_AST=""
|
||||
CONPOT_CONTAINER_IPMI=""
|
||||
CONPOT_CONTAINER_KAMSTRUP=""
|
||||
CONPOT_CONTAINER_NAMES=()
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 [options]
|
||||
|
||||
Run an isolated post-build smoke test for the Conpot image.
|
||||
|
||||
Options:
|
||||
--image IMAGE Image to test. Defaults to docker/conpot/docker-compose.yml.
|
||||
--iec104-port PORT Host TCP port for IEC104. Default: 2404.
|
||||
--guardian-ast-port PORT Host TCP port for Guardian AST. Default: 10001.
|
||||
--ipmi-port PORT Host UDP port for IPMI. Default: 623.
|
||||
--kamstrup-port PORT Host TCP port for Kamstrup meter. Default: 1025.
|
||||
--kamstrup-management-port PORT Host TCP port for Kamstrup management. Default: 50100.
|
||||
--timeout SEC Timeout for startup, protocol, and log checks. Default: 30.
|
||||
--bind-ip IP Host IP to bind. Default: 127.0.0.1.
|
||||
--keep-artifacts Keep temporary compose file and logs for debugging.
|
||||
-h, --help Show this help message.
|
||||
EOF
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--image)
|
||||
[[ $# -ge 2 ]] || test_die "--image requires an argument"
|
||||
IMAGE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--image=*)
|
||||
IMAGE="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--iec104-port)
|
||||
[[ $# -ge 2 ]] || test_die "--iec104-port requires an argument"
|
||||
IEC104_PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--iec104-port=*)
|
||||
IEC104_PORT="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--guardian-ast-port)
|
||||
[[ $# -ge 2 ]] || test_die "--guardian-ast-port requires an argument"
|
||||
GUARDIAN_AST_PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--guardian-ast-port=*)
|
||||
GUARDIAN_AST_PORT="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--ipmi-port)
|
||||
[[ $# -ge 2 ]] || test_die "--ipmi-port requires an argument"
|
||||
IPMI_PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--ipmi-port=*)
|
||||
IPMI_PORT="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--kamstrup-port)
|
||||
[[ $# -ge 2 ]] || test_die "--kamstrup-port requires an argument"
|
||||
KAMSTRUP_PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--kamstrup-port=*)
|
||||
KAMSTRUP_PORT="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--kamstrup-management-port)
|
||||
[[ $# -ge 2 ]] || test_die "--kamstrup-management-port requires an argument"
|
||||
KAMSTRUP_MANAGEMENT_PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--kamstrup-management-port=*)
|
||||
KAMSTRUP_MANAGEMENT_PORT="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--timeout)
|
||||
[[ $# -ge 2 ]] || test_die "--timeout requires an argument"
|
||||
TEST_TIMEOUT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--timeout=*)
|
||||
TEST_TIMEOUT="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--bind-ip)
|
||||
[[ $# -ge 2 ]] || test_die "--bind-ip requires an argument"
|
||||
TEST_BIND_IP="$2"
|
||||
shift 2
|
||||
;;
|
||||
--bind-ip=*)
|
||||
TEST_BIND_IP="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--keep-artifacts)
|
||||
TEST_KEEP_ARTIFACTS="true"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
test_die "Unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
validate_args() {
|
||||
test_validate_timeout
|
||||
test_validate_port "${IEC104_PORT}"
|
||||
test_validate_port "${GUARDIAN_AST_PORT}"
|
||||
test_validate_port "${IPMI_PORT}"
|
||||
test_validate_port "${KAMSTRUP_PORT}"
|
||||
test_validate_port "${KAMSTRUP_MANAGEMENT_PORT}"
|
||||
}
|
||||
|
||||
ensure_tcp_port_free_for_docker() {
|
||||
local port="$1"
|
||||
local option="$2"
|
||||
|
||||
if (( port < 1024 )); then
|
||||
test_info "Skipping user-space preflight for privileged TCP port ${port}; Docker will validate the binding."
|
||||
else
|
||||
test_ensure_port_free "${TEST_BIND_IP}" "${port}" || test_die "${TEST_BIND_IP}:${port} is already in use. Try ${option} <free-port>."
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_udp_port_free_for_docker() {
|
||||
local port="$1"
|
||||
local option="$2"
|
||||
|
||||
if (( port < 1024 )); then
|
||||
test_info "Skipping user-space preflight for privileged UDP port ${port}; Docker will validate the binding."
|
||||
else
|
||||
test_ensure_udp_port_free "${TEST_BIND_IP}" "${port}" || test_die "${TEST_BIND_IP}:${port}/udp is already in use. Try ${option} <free-port>."
|
||||
fi
|
||||
}
|
||||
|
||||
prepare_conpot_harness() {
|
||||
test_prepare_harness "${TEST_NAME}"
|
||||
|
||||
LOG_DIR="${TEST_TMP_ROOT}/log"
|
||||
TEST_ARTIFACT_LOG_DIR="${LOG_DIR}"
|
||||
|
||||
CONPOT_CONTAINER_IEC104="${TEST_PROJECT_NAME}-iec104"
|
||||
CONPOT_CONTAINER_GUARDIAN_AST="${TEST_PROJECT_NAME}-guardian-ast"
|
||||
CONPOT_CONTAINER_IPMI="${TEST_PROJECT_NAME}-ipmi"
|
||||
CONPOT_CONTAINER_KAMSTRUP="${TEST_PROJECT_NAME}-kamstrup-382"
|
||||
CONPOT_CONTAINER_NAMES=(
|
||||
"${CONPOT_CONTAINER_IEC104}"
|
||||
"${CONPOT_CONTAINER_GUARDIAN_AST}"
|
||||
"${CONPOT_CONTAINER_IPMI}"
|
||||
"${CONPOT_CONTAINER_KAMSTRUP}"
|
||||
)
|
||||
|
||||
mkdir -p "${LOG_DIR}"
|
||||
chmod 0777 "${LOG_DIR}"
|
||||
|
||||
cat > "${TEST_HARNESS_COMPOSE}" <<EOF
|
||||
networks:
|
||||
conpot_local_IEC104:
|
||||
conpot_local_guardian_ast:
|
||||
conpot_local_ipmi:
|
||||
conpot_local_kamstrup_382:
|
||||
|
||||
services:
|
||||
conpot_IEC104:
|
||||
image: "${IMAGE}"
|
||||
container_name: "${CONPOT_CONTAINER_IEC104}"
|
||||
restart: "no"
|
||||
read_only: true
|
||||
user: "2000:2000"
|
||||
environment:
|
||||
- CONPOT_CONFIG=/etc/conpot/conpot.cfg
|
||||
- CONPOT_JSON_LOG=/var/log/conpot/conpot_IEC104.json
|
||||
- CONPOT_LOG=/var/log/conpot/conpot_IEC104.log
|
||||
- CONPOT_TEMPLATE=IEC104
|
||||
- CONPOT_TMP=/tmp/conpot
|
||||
tmpfs:
|
||||
- /tmp/conpot:uid=2000,gid=2000
|
||||
networks:
|
||||
- conpot_local_IEC104
|
||||
ports:
|
||||
- "${TEST_BIND_IP}:${IEC104_PORT}:2404"
|
||||
volumes:
|
||||
- "${LOG_DIR}:/var/log/conpot"
|
||||
|
||||
conpot_guardian_ast:
|
||||
image: "${IMAGE}"
|
||||
container_name: "${CONPOT_CONTAINER_GUARDIAN_AST}"
|
||||
restart: "no"
|
||||
read_only: true
|
||||
user: "2000:2000"
|
||||
environment:
|
||||
- CONPOT_CONFIG=/etc/conpot/conpot.cfg
|
||||
- CONPOT_JSON_LOG=/var/log/conpot/conpot_guardian_ast.json
|
||||
- CONPOT_LOG=/var/log/conpot/conpot_guardian_ast.log
|
||||
- CONPOT_TEMPLATE=guardian_ast
|
||||
- CONPOT_TMP=/tmp/conpot
|
||||
tmpfs:
|
||||
- /tmp/conpot:uid=2000,gid=2000
|
||||
networks:
|
||||
- conpot_local_guardian_ast
|
||||
ports:
|
||||
- "${TEST_BIND_IP}:${GUARDIAN_AST_PORT}:10001"
|
||||
volumes:
|
||||
- "${LOG_DIR}:/var/log/conpot"
|
||||
|
||||
conpot_ipmi:
|
||||
image: "${IMAGE}"
|
||||
container_name: "${CONPOT_CONTAINER_IPMI}"
|
||||
restart: "no"
|
||||
read_only: true
|
||||
user: "2000:2000"
|
||||
environment:
|
||||
- CONPOT_CONFIG=/etc/conpot/conpot.cfg
|
||||
- CONPOT_JSON_LOG=/var/log/conpot/conpot_ipmi.json
|
||||
- CONPOT_LOG=/var/log/conpot/conpot_ipmi.log
|
||||
- CONPOT_TEMPLATE=ipmi
|
||||
- CONPOT_TMP=/tmp/conpot
|
||||
tmpfs:
|
||||
- /tmp/conpot:uid=2000,gid=2000
|
||||
networks:
|
||||
- conpot_local_ipmi
|
||||
ports:
|
||||
- "${TEST_BIND_IP}:${IPMI_PORT}:623/udp"
|
||||
volumes:
|
||||
- "${LOG_DIR}:/var/log/conpot"
|
||||
|
||||
conpot_kamstrup_382:
|
||||
image: "${IMAGE}"
|
||||
container_name: "${CONPOT_CONTAINER_KAMSTRUP}"
|
||||
restart: "no"
|
||||
read_only: true
|
||||
user: "2000:2000"
|
||||
environment:
|
||||
- CONPOT_CONFIG=/etc/conpot/conpot.cfg
|
||||
- CONPOT_JSON_LOG=/var/log/conpot/conpot_kamstrup_382.json
|
||||
- CONPOT_LOG=/var/log/conpot/conpot_kamstrup_382.log
|
||||
- CONPOT_TEMPLATE=kamstrup_382
|
||||
- CONPOT_TMP=/tmp/conpot
|
||||
tmpfs:
|
||||
- /tmp/conpot:uid=2000,gid=2000
|
||||
networks:
|
||||
- conpot_local_kamstrup_382
|
||||
ports:
|
||||
- "${TEST_BIND_IP}:${KAMSTRUP_PORT}:1025"
|
||||
- "${TEST_BIND_IP}:${KAMSTRUP_MANAGEMENT_PORT}:50100"
|
||||
volumes:
|
||||
- "${LOG_DIR}:/var/log/conpot"
|
||||
EOF
|
||||
}
|
||||
|
||||
test_show_diagnostics() {
|
||||
local container=""
|
||||
local file=""
|
||||
|
||||
printf '\n[diagnostics] Container states\n' >&2
|
||||
if [[ ${#CONPOT_CONTAINER_NAMES[@]} -gt 0 ]]; then
|
||||
for container in "${CONPOT_CONTAINER_NAMES[@]}"; do
|
||||
printf '%s: ' "${container}" >&2
|
||||
docker inspect -f 'status={{.State.Status}} exit={{.State.ExitCode}} error={{.State.Error}}' "${container}" >&2 || true
|
||||
done
|
||||
else
|
||||
printf 'No Conpot container names available.\n' >&2
|
||||
fi
|
||||
|
||||
printf '\n[diagnostics] Docker logs\n' >&2
|
||||
if [[ -n "${TEST_HARNESS_COMPOSE}" && -f "${TEST_HARNESS_COMPOSE}" ]]; then
|
||||
test_compose logs --no-color --tail=160 >&2 || true
|
||||
else
|
||||
printf 'No temporary compose file available.\n' >&2
|
||||
fi
|
||||
|
||||
printf '\n[diagnostics] Test log artifacts\n' >&2
|
||||
if [[ -n "${LOG_DIR}" && -d "${LOG_DIR}" ]]; then
|
||||
find "${LOG_DIR}" -maxdepth 1 -type f -print | sort >&2 || true
|
||||
while IFS= read -r file; do
|
||||
printf '\n--- %s ---\n' "${file}" >&2
|
||||
tail -n 120 "${file}" >&2 || true
|
||||
done < <(find "${LOG_DIR}" -maxdepth 1 -type f -print | sort)
|
||||
else
|
||||
printf 'No temporary log directory available.\n' >&2
|
||||
fi
|
||||
}
|
||||
|
||||
wait_for_containers() {
|
||||
local deadline=$((SECONDS + TEST_TIMEOUT))
|
||||
local container=""
|
||||
local state=""
|
||||
local all_running=""
|
||||
|
||||
while (( SECONDS < deadline )); do
|
||||
all_running="true"
|
||||
for container in "${CONPOT_CONTAINER_NAMES[@]}"; do
|
||||
state="$(docker inspect -f '{{.State.Status}}' "${container}" 2>/dev/null || true)"
|
||||
case "${state}" in
|
||||
running)
|
||||
;;
|
||||
exited|dead)
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
all_running="false"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[[ "${all_running}" == "true" ]] && return 0
|
||||
sleep 1
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
assert_containers_running() {
|
||||
local container=""
|
||||
local state=""
|
||||
|
||||
for container in "${CONPOT_CONTAINER_NAMES[@]}"; do
|
||||
state="$(docker inspect -f '{{.State.Status}}' "${container}" 2>/dev/null || true)"
|
||||
[[ "${state}" == "running" ]] || test_die "${container} is not running; state=${state:-unknown}"
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_log_line() {
|
||||
local log_file="$1"
|
||||
local pattern="$2"
|
||||
local deadline=$((SECONDS + TEST_TIMEOUT))
|
||||
|
||||
while (( SECONDS < deadline )); do
|
||||
if [[ -f "${log_file}" ]] && grep -F -- "${pattern}" "${log_file}" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_json_event() {
|
||||
local json_file="$1"
|
||||
local mode="$2"
|
||||
|
||||
python3 - "${json_file}" "${mode}" "${TEST_TIMEOUT}" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
mode = sys.argv[2]
|
||||
timeout = int(sys.argv[3])
|
||||
deadline = time.monotonic() + timeout
|
||||
last_error = None
|
||||
|
||||
|
||||
def matches(event):
|
||||
data_type = str(event.get("data_type", ""))
|
||||
event_type = str(event.get("event_type", ""))
|
||||
request = str(event.get("request", ""))
|
||||
response = str(event.get("response", ""))
|
||||
dst_port = str(event.get("dst_port", ""))
|
||||
|
||||
if mode == "iec104":
|
||||
return data_type == "IEC104"
|
||||
if mode == "guardian_ast":
|
||||
return data_type == "guardian_ast" and (
|
||||
event_type == "AST I20100" or "I20100" in request or "I20100" in response
|
||||
)
|
||||
if mode == "ipmi":
|
||||
return data_type == "ipmi" and event_type == "GET_CHANNEL_AUTH_CAPABILITIES" and response not in ("", "None")
|
||||
if mode == "kamstrup_meter":
|
||||
return data_type == "kamstrup_protocol" and dst_port == "1025"
|
||||
if mode == "kamstrup_management":
|
||||
return data_type == "kamstrup_management_protocol" and dst_port == "50100"
|
||||
raise RuntimeError(f"Unknown JSON event mode: {mode}")
|
||||
|
||||
|
||||
while time.monotonic() < deadline:
|
||||
if not path.exists():
|
||||
last_error = f"{path} does not exist yet"
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
raw = path.read_text(encoding="utf-8", errors="replace")
|
||||
if not raw.strip():
|
||||
last_error = f"{path} is empty"
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
found = False
|
||||
invalid = None
|
||||
for line_number, line in enumerate(raw.splitlines(), 1):
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
event = json.loads(line)
|
||||
except json.JSONDecodeError as exc:
|
||||
invalid = f"{path}:{line_number}: invalid JSON: {exc}"
|
||||
continue
|
||||
if matches(event):
|
||||
found = True
|
||||
|
||||
if invalid is None and found:
|
||||
print(f"JSON event found in {path}: {mode}")
|
||||
sys.exit(0)
|
||||
|
||||
last_error = invalid or f"No matching {mode} JSON event found in {path}"
|
||||
time.sleep(1)
|
||||
|
||||
if last_error:
|
||||
print(last_error, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
run_iec104_probe() {
|
||||
python3 - "${TEST_BIND_IP}" "${IEC104_PORT}" "${TEST_TIMEOUT}" <<'PY'
|
||||
import socket
|
||||
import sys
|
||||
|
||||
host = sys.argv[1]
|
||||
port = int(sys.argv[2])
|
||||
timeout = int(sys.argv[3])
|
||||
payload = bytes.fromhex("68 04 07 00 00 00")
|
||||
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=timeout) as sock:
|
||||
sock.settimeout(2)
|
||||
sock.sendall(payload)
|
||||
try:
|
||||
response = sock.recv(64)
|
||||
except socket.timeout:
|
||||
response = b""
|
||||
print(f"IEC104 probe sent {len(payload)} bytes; received {len(response)} bytes")
|
||||
except Exception as exc:
|
||||
print(f"IEC104 probe failed: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
run_guardian_ast_probe() {
|
||||
python3 - "${TEST_BIND_IP}" "${GUARDIAN_AST_PORT}" "${TEST_TIMEOUT}" <<'PY'
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
host = sys.argv[1]
|
||||
port = int(sys.argv[2])
|
||||
timeout = int(sys.argv[3])
|
||||
deadline = time.monotonic() + timeout
|
||||
payload = b"\x01I20100\n"
|
||||
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=timeout) as sock:
|
||||
sock.settimeout(1)
|
||||
sock.sendall(payload)
|
||||
chunks = []
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
chunk = sock.recv(4096)
|
||||
except socket.timeout:
|
||||
continue
|
||||
if not chunk:
|
||||
break
|
||||
chunks.append(chunk)
|
||||
if b"I20100" in b"".join(chunks):
|
||||
break
|
||||
response = b"".join(chunks)
|
||||
if b"I20100" not in response and b"IN-TANK" not in response:
|
||||
raise RuntimeError(f"Expected Guardian AST inventory response, got {response[:120]!r}")
|
||||
print(f"Guardian AST response: {response[:80]!r}")
|
||||
except Exception as exc:
|
||||
print(f"Guardian AST probe failed: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
run_ipmi_probe() {
|
||||
python3 - "${TEST_BIND_IP}" "${IPMI_PORT}" "${TEST_TIMEOUT}" <<'PY'
|
||||
import socket
|
||||
import sys
|
||||
|
||||
host = sys.argv[1]
|
||||
port = int(sys.argv[2])
|
||||
timeout = int(sys.argv[3])
|
||||
# RMCP/IPMI v1.5 Get Channel Authentication Capabilities.
|
||||
payload = bytes.fromhex(
|
||||
"06 00 ff 07"
|
||||
" 00 00 00 00 00 00 00 00 00 09"
|
||||
" 20 18 c8 81 00 38 8e 04 b5"
|
||||
)
|
||||
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||
sock.settimeout(min(timeout, 3))
|
||||
sock.sendto(payload, (host, port))
|
||||
try:
|
||||
response, _ = sock.recvfrom(4096)
|
||||
except socket.timeout:
|
||||
response = b""
|
||||
if not response:
|
||||
raise RuntimeError("Expected RMCP/IPMI response, got no UDP response")
|
||||
if not response.startswith(bytes.fromhex("06 00 ff 07")):
|
||||
raise RuntimeError(f"Expected RMCP response, got {response[:16]!r}")
|
||||
if b"\x38" not in response:
|
||||
raise RuntimeError(f"Expected IPMI command 0x38 response, got {response[:32]!r}")
|
||||
print(f"IPMI probe sent {len(payload)} bytes; received {len(response)} bytes")
|
||||
except Exception as exc:
|
||||
print(f"IPMI probe failed: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
run_kamstrup_meter_probe() {
|
||||
python3 - "${TEST_BIND_IP}" "${KAMSTRUP_PORT}" "${TEST_TIMEOUT}" <<'PY'
|
||||
import socket
|
||||
import sys
|
||||
|
||||
host = sys.argv[1]
|
||||
port = int(sys.argv[2])
|
||||
timeout = int(sys.argv[3])
|
||||
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=timeout):
|
||||
pass
|
||||
print("Kamstrup meter TCP connection accepted")
|
||||
except Exception as exc:
|
||||
print(f"Kamstrup meter probe failed: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
run_kamstrup_management_probe() {
|
||||
python3 - "${TEST_BIND_IP}" "${KAMSTRUP_MANAGEMENT_PORT}" "${TEST_TIMEOUT}" <<'PY'
|
||||
import socket
|
||||
import sys
|
||||
|
||||
host = sys.argv[1]
|
||||
port = int(sys.argv[2])
|
||||
timeout = int(sys.argv[3])
|
||||
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=timeout) as sock:
|
||||
sock.settimeout(3)
|
||||
banner = sock.recv(1024)
|
||||
if b"Welcome" not in banner:
|
||||
raise RuntimeError(f"Expected Kamstrup management banner, got {banner[:120]!r}")
|
||||
sock.sendall(b"help\r\n")
|
||||
try:
|
||||
response = sock.recv(1024)
|
||||
except socket.timeout:
|
||||
response = b""
|
||||
print(f"Kamstrup management banner: {banner[:80]!r}; response bytes={len(response)}")
|
||||
except Exception as exc:
|
||||
print(f"Kamstrup management probe failed: {exc}", 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 "Conpot runtime error found in log files"
|
||||
fi
|
||||
|
||||
if test_compose logs --no-color 2>/dev/null | grep -E "Traceback|NameError|Exception" >/dev/null 2>&1; then
|
||||
test_die "Conpot runtime error found in Docker 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 conpot_default"
|
||||
|
||||
ensure_tcp_port_free_for_docker "${IEC104_PORT}" "--iec104-port"
|
||||
ensure_tcp_port_free_for_docker "${GUARDIAN_AST_PORT}" "--guardian-ast-port"
|
||||
ensure_udp_port_free_for_docker "${IPMI_PORT}" "--ipmi-port"
|
||||
ensure_tcp_port_free_for_docker "${KAMSTRUP_PORT}" "--kamstrup-port"
|
||||
ensure_tcp_port_free_for_docker "${KAMSTRUP_MANAGEMENT_PORT}" "--kamstrup-management-port"
|
||||
|
||||
prepare_conpot_harness
|
||||
test_enable_cleanup
|
||||
|
||||
test_info "Starting isolated Conpot containers"
|
||||
test_compose up -d --no-build >/dev/null
|
||||
|
||||
wait_for_containers || test_die "One or more Conpot containers did not stay running"
|
||||
test_ok "All Conpot containers are running"
|
||||
|
||||
test_info "Waiting for Conpot listener entries"
|
||||
wait_for_log_line "${LOG_DIR}/conpot_IEC104.log" "IEC 60870-5-104 protocol server started on" || test_die "IEC104 listener entry was not found"
|
||||
wait_for_log_line "${LOG_DIR}/conpot_guardian_ast.log" "GuardianAST server started on" || test_die "Guardian AST listener entry was not found"
|
||||
wait_for_log_line "${LOG_DIR}/conpot_ipmi.log" "IPMI server started on" || test_die "IPMI listener entry was not found"
|
||||
wait_for_log_line "${LOG_DIR}/conpot_kamstrup_382.log" "Kamstrup protocol server started on" || test_die "Kamstrup meter listener entry was not found"
|
||||
wait_for_log_line "${LOG_DIR}/conpot_kamstrup_382.log" "Kamstrup management protocol server started on" || test_die "Kamstrup management listener entry was not found"
|
||||
test_ok "All Conpot listener entries were found"
|
||||
|
||||
assert_containers_running
|
||||
|
||||
test_info "Running IEC104 probe"
|
||||
run_iec104_probe
|
||||
|
||||
test_info "Running Guardian AST probe"
|
||||
run_guardian_ast_probe
|
||||
|
||||
test_info "Running IPMI probe"
|
||||
run_ipmi_probe
|
||||
|
||||
test_info "Running Kamstrup meter probe"
|
||||
run_kamstrup_meter_probe
|
||||
|
||||
test_info "Running Kamstrup management probe"
|
||||
run_kamstrup_management_probe
|
||||
|
||||
assert_containers_running
|
||||
assert_no_runtime_errors
|
||||
test_ok "No Conpot runtime errors found in logs"
|
||||
|
||||
test_info "Validating Conpot JSON events"
|
||||
wait_for_json_event "${LOG_DIR}/conpot_IEC104.json" "iec104" || test_die "IEC104 JSON event was not found"
|
||||
test_ok "IEC104 JSON event found"
|
||||
|
||||
wait_for_json_event "${LOG_DIR}/conpot_guardian_ast.json" "guardian_ast" || test_die "Guardian AST JSON event was not found"
|
||||
test_ok "Guardian AST JSON event found"
|
||||
|
||||
wait_for_json_event "${LOG_DIR}/conpot_kamstrup_382.json" "kamstrup_meter" || test_die "Kamstrup meter JSON event was not found"
|
||||
test_ok "Kamstrup meter JSON event found"
|
||||
|
||||
wait_for_json_event "${LOG_DIR}/conpot_kamstrup_382.json" "kamstrup_management" || test_die "Kamstrup management JSON event was not found"
|
||||
test_ok "Kamstrup management JSON event found"
|
||||
|
||||
wait_for_log_line "${LOG_DIR}/conpot_ipmi.log" "Connection established with" || test_die "IPMI connection establishment was not found in conpot_ipmi.log"
|
||||
wait_for_log_line "${LOG_DIR}/conpot_ipmi.log" "IPMI response sent to" || test_die "IPMI response was not found in conpot_ipmi.log"
|
||||
test_ok "IPMI protocol exchange was written to conpot_ipmi.log"
|
||||
|
||||
wait_for_json_event "${LOG_DIR}/conpot_ipmi.json" "ipmi" || test_die "IPMI JSON event was not found"
|
||||
test_ok "IPMI JSON event found"
|
||||
|
||||
test_ok "Conpot post-build smoke test completed successfully"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
|
@ -62,6 +62,7 @@ RUN apk --no-cache -U upgrade && \
|
|||
sed -i 's/port="6969"/port="69"/' /opt/conpot/conpot/templates/default/tftp/tftp.xml && \
|
||||
sed -i 's/port="16100"/port="161"/' /opt/conpot/conpot/templates/IEC104/snmp/snmp.xml && \
|
||||
sed -i 's/port="6230"/port="623"/' /opt/conpot/conpot/templates/ipmi/ipmi/ipmi.xml && \
|
||||
patch -p1 < /root/dist/patches/ipmi-json-events.patch && \
|
||||
cp /root/dist/requirements.txt . && \
|
||||
pip3 install --break-system-packages --no-cache-dir . && \
|
||||
cd / && \
|
||||
|
|
|
|||
61
docker/conpot/dist/patches/ipmi-json-events.patch
vendored
Normal file
61
docker/conpot/dist/patches/ipmi-json-events.patch
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
--- a/conpot/protocols/ipmi/ipmi_server.py
|
||||
+++ b/conpot/protocols/ipmi/ipmi_server.py
|
||||
@@ -92,11 +92,25 @@
|
||||
csum &= 0xFF
|
||||
return csum
|
||||
|
||||
+ def _add_event(self, address, event_data):
|
||||
+ session = conpot_core.get_session(
|
||||
+ "ipmi",
|
||||
+ address[0],
|
||||
+ address[1],
|
||||
+ self.sock.getsockname()[0],
|
||||
+ self.port,
|
||||
+ )
|
||||
+ session.add_event(event_data)
|
||||
+
|
||||
def handle(self, data, address):
|
||||
# make sure self.session exists
|
||||
if not address[0] in self.sessions.keys() or not hasattr(self, "session"):
|
||||
# new session for new source
|
||||
logger.info("New IPMI traffic from %s", address)
|
||||
+ self._add_event(
|
||||
+ address,
|
||||
+ {"type": "NEW_CONNECTION", "request": data, "response": None},
|
||||
+ )
|
||||
self.session = FakeSession(address[0], "", "", address[1])
|
||||
self.session.server = self
|
||||
self.uuid = uuid.uuid4()
|
||||
@@ -165,10 +179,10 @@
|
||||
(clientaddr, clientlun) = struct.unpack("BB", data[17:19])
|
||||
level &= 0b1111
|
||||
self.send_auth_cap(
|
||||
- myaddr, mylun, clientaddr, clientlun, session.sockaddr
|
||||
+ myaddr, mylun, clientaddr, clientlun, session.sockaddr, data
|
||||
)
|
||||
|
||||
- def send_auth_cap(self, myaddr, mylun, clientaddr, clientlun, sockaddr):
|
||||
+ def send_auth_cap(self, myaddr, mylun, clientaddr, clientlun, sockaddr, request=None):
|
||||
header = b"\x06\x00\xff\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10"
|
||||
|
||||
headerdata = (clientaddr, clientlun | (7 << 2))
|
||||
@@ -181,11 +195,19 @@
|
||||
header += chr_py3(self._checksum(*bodydata))
|
||||
self.session.stage += 1
|
||||
logger.info("Connection established with %s", sockaddr)
|
||||
+ self._add_event(
|
||||
+ sockaddr,
|
||||
+ {"type": "GET_CHANNEL_AUTH_CAPABILITIES", "request": request, "response": header},
|
||||
+ )
|
||||
self.session.send_data(header, sockaddr)
|
||||
|
||||
def close_server_session(self):
|
||||
logger.info("IPMI Session closed %s", self.session.sockaddr[0])
|
||||
# cleanup session
|
||||
+ self._add_event(
|
||||
+ self.session.sockaddr,
|
||||
+ {"type": "CONNECTION_LOST", "request": None, "response": None},
|
||||
+ )
|
||||
del self.sessions[self.session.sockaddr[0]]
|
||||
del self.session
|
||||
|
||||
Loading…
Reference in a new issue