first commit
This commit is contained in:
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
ARCH="${1:-arm64}"
|
||||
|
||||
case "$ARCH" in
|
||||
amd64)
|
||||
PLATFORM="linux/amd64"
|
||||
;;
|
||||
arm64)
|
||||
PLATFORM="linux/arm64"
|
||||
;;
|
||||
arm)
|
||||
PLATFORM="linux/arm/v7"
|
||||
;;
|
||||
*)
|
||||
echo "usage: $0 [amd64|arm64|arm]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
TAG="routeros-suricata-tzsp:${ARCH}"
|
||||
OUT="build/routeros-suricata-tzsp-${ARCH}.tar"
|
||||
SHA="${OUT}.sha256"
|
||||
mkdir -p build
|
||||
|
||||
if [ -n "${ENGINE:-}" ]; then
|
||||
engine="$ENGINE"
|
||||
elif command -v podman >/dev/null 2>&1; then
|
||||
engine=podman
|
||||
elif command -v docker >/dev/null 2>&1; then
|
||||
engine=docker
|
||||
else
|
||||
echo "Docker or Podman is required" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
echo "[build] engine=$engine platform=$PLATFORM image=$TAG"
|
||||
case "$engine" in
|
||||
podman)
|
||||
podman build --pull --platform "$PLATFORM" -t "$TAG" .
|
||||
podman save -o "$OUT" "$TAG"
|
||||
;;
|
||||
docker)
|
||||
if ! docker buildx version >/dev/null 2>&1; then
|
||||
echo "Docker Buildx is required for RouterOS cross-architecture builds" >&2
|
||||
exit 4
|
||||
fi
|
||||
docker buildx build \
|
||||
--pull \
|
||||
--platform "$PLATFORM" \
|
||||
--provenance=false \
|
||||
--load \
|
||||
-t "$TAG" \
|
||||
.
|
||||
docker save -o "$OUT" "$TAG"
|
||||
;;
|
||||
*)
|
||||
echo "unsupported ENGINE=$engine (use docker or podman)" >&2
|
||||
exit 5
|
||||
;;
|
||||
esac
|
||||
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$OUT" > "$SHA"
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "$OUT" > "$SHA"
|
||||
fi
|
||||
|
||||
printf '[build] created %s\n' "$OUT"
|
||||
ls -lh "$OUT"
|
||||
[ ! -f "$SHA" ] || cat "$SHA"
|
||||
Executable
+338
@@ -0,0 +1,338 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
CONFIG_FILE="${DEPLOY_CONFIG:-deploy-routeros.env}"
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
case "$CONFIG_FILE" in
|
||||
/*) . "$CONFIG_FILE" ;;
|
||||
*) . "./$CONFIG_FILE" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
: "${ROUTER_HOST:=192.168.88.1}"
|
||||
: "${ROUTER_USER:=admin}"
|
||||
: "${ROUTER_PORT:=22}"
|
||||
: "${ROUTER_IDENTITY_FILE:=}"
|
||||
: "${ROUTER_ARCH:=auto}"
|
||||
: "${ROUTER_DISK:=disk1}"
|
||||
: "${ROUTER_SCP_DIR:=$ROUTER_DISK}"
|
||||
: "${CONTAINER_NAME:=suricata-ids}"
|
||||
: "${CONTAINER_IP:=172.31.255.2/30}"
|
||||
: "${CONTAINER_GATEWAY:=172.31.255.1}"
|
||||
: "${CONTAINER_SUBNET:=172.31.255.0/30}"
|
||||
: "${CONTAINER_BRIDGE:=br-ids}"
|
||||
: "${CONTAINER_VETH:=veth-ids}"
|
||||
: "${VLAN_ID:=100}"
|
||||
: "${TZSP_PORT:=37008}"
|
||||
: "${CONFIGURE_SNIFFER:=true}"
|
||||
: "${START_SNIFFER:=true}"
|
||||
: "${SURICATA_HOME_NET:=[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]}"
|
||||
: "${MONITORED_NETWORKS:=192.168.100.0/24}"
|
||||
: "${AUTO_BLOCK:=false}"
|
||||
: "${AUTO_BLOCK_MAX_SEVERITY:=1}"
|
||||
: "${BLOCK_TIMEOUT:=1h}"
|
||||
: "${UPDATE_RULES_ON_START:=false}"
|
||||
: "${CREATE_REST_USER:=false}"
|
||||
: "${ENABLE_WWW_SSL:=false}"
|
||||
: "${ROUTEROS_REST_USER:=suricata-api}"
|
||||
: "${ROUTEROS_REST_PASSWORD:=CHANGE_ME}"
|
||||
: "${ROUTEROS_VERIFY_TLS:=false}"
|
||||
: "${ROUTEROS_ADDRESS_LIST:=IDS-BLOCK}"
|
||||
: "${REPLACE_EXISTING:=true}"
|
||||
: "${KEEP_REMOTE_TAR:=true}"
|
||||
: "${ENGINE:=}"
|
||||
|
||||
need() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
echo "$1 is required" >&2
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
need ssh
|
||||
need scp
|
||||
|
||||
case "$ROUTER_PORT" in
|
||||
*[!0-9]*|'') echo "ROUTER_PORT must be numeric" >&2; exit 2 ;;
|
||||
esac
|
||||
case "$VLAN_ID" in
|
||||
*[!0-9]*|'') echo "VLAN_ID must be numeric" >&2; exit 2 ;;
|
||||
esac
|
||||
case "$TZSP_PORT" in
|
||||
*[!0-9]*|'') echo "TZSP_PORT must be numeric" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
# Values are inserted into a RouterOS script inside quoted strings. Reject
|
||||
# characters that would require RouterOS-specific escaping.
|
||||
check_ros_value() {
|
||||
label="$1"
|
||||
value="$2"
|
||||
case "$value" in
|
||||
*'"'*|*'\\'*|*'$'*|*';'*|*'`'*)
|
||||
echo "$label contains a character not supported by the deploy script: $value" >&2
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
case "$value" in
|
||||
*'\n'*|*'\r'*)
|
||||
echo "$label contains a newline" >&2
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
for pair in \
|
||||
"ROUTER_DISK=$ROUTER_DISK" \
|
||||
"CONTAINER_NAME=$CONTAINER_NAME" \
|
||||
"CONTAINER_IP=$CONTAINER_IP" \
|
||||
"CONTAINER_GATEWAY=$CONTAINER_GATEWAY" \
|
||||
"CONTAINER_SUBNET=$CONTAINER_SUBNET" \
|
||||
"CONTAINER_BRIDGE=$CONTAINER_BRIDGE" \
|
||||
"CONTAINER_VETH=$CONTAINER_VETH" \
|
||||
"SURICATA_HOME_NET=$SURICATA_HOME_NET" \
|
||||
"MONITORED_NETWORKS=$MONITORED_NETWORKS" \
|
||||
"BLOCK_TIMEOUT=$BLOCK_TIMEOUT" \
|
||||
"ROUTEROS_REST_USER=$ROUTEROS_REST_USER" \
|
||||
"ROUTEROS_REST_PASSWORD=$ROUTEROS_REST_PASSWORD" \
|
||||
"ROUTEROS_ADDRESS_LIST=$ROUTEROS_ADDRESS_LIST"
|
||||
do
|
||||
check_ros_value "${pair%%=*}" "${pair#*=}"
|
||||
done
|
||||
|
||||
SSH_TARGET="${ROUTER_USER}@${ROUTER_HOST}"
|
||||
ssh_run() {
|
||||
if [ -n "$ROUTER_IDENTITY_FILE" ]; then
|
||||
ssh -i "$ROUTER_IDENTITY_FILE" -p "$ROUTER_PORT" "$SSH_TARGET" "$1"
|
||||
else
|
||||
ssh -p "$ROUTER_PORT" "$SSH_TARGET" "$1"
|
||||
fi
|
||||
}
|
||||
scp_put() {
|
||||
src="$1"
|
||||
dst="$2"
|
||||
if [ -n "$ROUTER_IDENTITY_FILE" ]; then
|
||||
scp -i "$ROUTER_IDENTITY_FILE" -P "$ROUTER_PORT" "$src" "${SSH_TARGET}:$dst"
|
||||
else
|
||||
scp -P "$ROUTER_PORT" "$src" "${SSH_TARGET}:$dst"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "[deploy] RouterOS preflight"
|
||||
if ! ssh_run '/container/print' >/dev/null; then
|
||||
echo "RouterOS container menu is unavailable. Install the matching container package and enable container device-mode first." >&2
|
||||
exit 4
|
||||
fi
|
||||
if [ "$CONFIGURE_SNIFFER" = "true" ]; then
|
||||
if ! ssh_run '/tool/sniffer/print' >/dev/null; then
|
||||
echo "RouterOS sniffer is unavailable. Check device-mode sniffer=yes before deployment." >&2
|
||||
exit 4
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$ROUTER_ARCH" = "auto" ]; then
|
||||
echo "[deploy] detecting RouterOS architecture"
|
||||
RESOURCE="$(ssh_run '/system/resource/print without-paging')"
|
||||
DETECTED="$(printf '%s\n' "$RESOURCE" | awk -F: '/architecture-name/ {gsub(/[[:space:]]/, "", $2); print $2; exit}')"
|
||||
case "$DETECTED" in
|
||||
arm64) ROUTER_ARCH=arm64 ;;
|
||||
x86_64|x86|amd64) ROUTER_ARCH=amd64 ;;
|
||||
arm) ROUTER_ARCH=arm ;;
|
||||
*)
|
||||
echo "Unsupported or undetected RouterOS architecture: ${DETECTED:-unknown}" >&2
|
||||
echo "Set ROUTER_ARCH manually to arm64, amd64 or arm." >&2
|
||||
exit 4
|
||||
;;
|
||||
esac
|
||||
echo "[deploy] RouterOS architecture: $DETECTED -> image target $ROUTER_ARCH"
|
||||
fi
|
||||
|
||||
case "$ROUTER_ARCH" in
|
||||
arm64|amd64|arm) ;;
|
||||
*) echo "ROUTER_ARCH must be auto, arm64, amd64 or arm" >&2; exit 4 ;;
|
||||
esac
|
||||
|
||||
if [ -n "$ENGINE" ]; then
|
||||
ENGINE="$ENGINE" ./scripts/build-routeros.sh "$ROUTER_ARCH"
|
||||
else
|
||||
./scripts/build-routeros.sh "$ROUTER_ARCH"
|
||||
fi
|
||||
|
||||
LOCAL_TAR="build/routeros-suricata-tzsp-${ROUTER_ARCH}.tar"
|
||||
[ -f "$LOCAL_TAR" ] || { echo "Missing $LOCAL_TAR" >&2; exit 5; }
|
||||
|
||||
DEPLOY_ID="$(date -u +%Y%m%d%H%M%S)"
|
||||
REMOTE_TAR_NAME="routeros-suricata-tzsp-${ROUTER_ARCH}-${DEPLOY_ID}.tar"
|
||||
REMOTE_TAR_ROS="${ROUTER_DISK}/${REMOTE_TAR_NAME}"
|
||||
REMOTE_TAR_SCP="${ROUTER_SCP_DIR%/}/${REMOTE_TAR_NAME}"
|
||||
LOCAL_RSC="build/deploy-${DEPLOY_ID}.rsc"
|
||||
REMOTE_RSC_NAME="deploy-${DEPLOY_ID}.rsc"
|
||||
REMOTE_RSC_ROS="${ROUTER_DISK}/${REMOTE_RSC_NAME}"
|
||||
REMOTE_RSC_SCP="${ROUTER_SCP_DIR%/}/${REMOTE_RSC_NAME}"
|
||||
ROOT_DIR="${ROUTER_DISK}/containers/${CONTAINER_NAME}-${DEPLOY_ID}"
|
||||
DATA_DIR="${ROUTER_DISK}/containers/${CONTAINER_NAME}-data"
|
||||
LOG_DIR="${ROUTER_DISK}/containers/${CONTAINER_NAME}-logs"
|
||||
RULES_DIR="${ROUTER_DISK}/containers/${CONTAINER_NAME}-rules"
|
||||
|
||||
REST_URL="https://${CONTAINER_GATEWAY}"
|
||||
CONTAINER_IP_ONLY="${CONTAINER_IP%/*}"
|
||||
|
||||
if [ "$CREATE_REST_USER" = "true" ] && [ "$ROUTEROS_REST_PASSWORD" = "CHANGE_ME" ]; then
|
||||
echo "CREATE_REST_USER=true requires a real ROUTEROS_REST_PASSWORD" >&2
|
||||
exit 6
|
||||
fi
|
||||
|
||||
cat > "$LOCAL_RSC" <<RSC
|
||||
# Generated by scripts/deploy-routeros.sh at ${DEPLOY_ID} UTC.
|
||||
# Image: ${REMOTE_TAR_ROS}
|
||||
|
||||
:if ([:len [/interface/bridge/find where name="${CONTAINER_BRIDGE}"]] = 0) do={
|
||||
/interface/bridge/add name="${CONTAINER_BRIDGE}" comment="Suricata IDS container bridge"
|
||||
}
|
||||
:if ([:len [/ip/address/find where interface="${CONTAINER_BRIDGE}" and address="${CONTAINER_GATEWAY}/30"]] = 0) do={
|
||||
/ip/address/add address="${CONTAINER_GATEWAY}/30" interface="${CONTAINER_BRIDGE}" comment="Suricata IDS gateway"
|
||||
}
|
||||
:if ([:len [/interface/veth/find where name="${CONTAINER_VETH}"]] = 0) do={
|
||||
/interface/veth/add name="${CONTAINER_VETH}" address="${CONTAINER_IP}" gateway="${CONTAINER_GATEWAY}" comment="Suricata IDS container"
|
||||
}
|
||||
:if ([:len [/interface/bridge/port/find where bridge="${CONTAINER_BRIDGE}" and interface="${CONTAINER_VETH}"]] = 0) do={
|
||||
/interface/bridge/port/add bridge="${CONTAINER_BRIDGE}" interface="${CONTAINER_VETH}"
|
||||
}
|
||||
:if ([:len [/ip/firewall/nat/find where comment="Suricata IDS outbound NAT"]] = 0) do={
|
||||
/ip/firewall/nat/add chain=srcnat src-address="${CONTAINER_SUBNET}" action=masquerade comment="Suricata IDS outbound NAT"
|
||||
}
|
||||
|
||||
/container/envs/remove [find where list="IDS_ENV"]
|
||||
/container/envs/add list=IDS_ENV key=TZSP_BIND value="0.0.0.0"
|
||||
/container/envs/add list=IDS_ENV key=TZSP_PORT value="${TZSP_PORT}"
|
||||
/container/envs/add list=IDS_ENV key=TAP_NAME value="suritap0"
|
||||
/container/envs/add list=IDS_ENV key=TAP_MTU value="9000"
|
||||
/container/envs/add list=IDS_ENV key=SURICATA_HOME_NET value="${SURICATA_HOME_NET}"
|
||||
/container/envs/add list=IDS_ENV key=MONITORED_NETWORKS value="${MONITORED_NETWORKS}"
|
||||
/container/envs/add list=IDS_ENV key=AUTO_BLOCK value="${AUTO_BLOCK}"
|
||||
/container/envs/add list=IDS_ENV key=AUTO_BLOCK_MAX_SEVERITY value="${AUTO_BLOCK_MAX_SEVERITY}"
|
||||
/container/envs/add list=IDS_ENV key=BLOCK_TIMEOUT value="${BLOCK_TIMEOUT}"
|
||||
/container/envs/add list=IDS_ENV key=ROUTEROS_URL value="${REST_URL}"
|
||||
/container/envs/add list=IDS_ENV key=ROUTEROS_USER value="${ROUTEROS_REST_USER}"
|
||||
/container/envs/add list=IDS_ENV key=ROUTEROS_PASSWORD value="${ROUTEROS_REST_PASSWORD}"
|
||||
/container/envs/add list=IDS_ENV key=ROUTEROS_VERIFY_TLS value="${ROUTEROS_VERIFY_TLS}"
|
||||
/container/envs/add list=IDS_ENV key=ROUTEROS_ADDRESS_LIST value="${ROUTEROS_ADDRESS_LIST}"
|
||||
/container/envs/add list=IDS_ENV key=UPDATE_RULES_ON_START value="${UPDATE_RULES_ON_START}"
|
||||
|
||||
/container/mounts/remove [find where list="IDS_MOUNTS"]
|
||||
/container/mounts/add list=IDS_MOUNTS src="${DATA_DIR}" dst=/data
|
||||
/container/mounts/add list=IDS_MOUNTS src="${LOG_DIR}" dst=/var/log/suricata
|
||||
/container/mounts/add list=IDS_MOUNTS src="${RULES_DIR}" dst=/var/lib/suricata
|
||||
RSC
|
||||
|
||||
if [ "$CREATE_REST_USER" = "true" ]; then
|
||||
cat >> "$LOCAL_RSC" <<RSC
|
||||
|
||||
:if ([:len [/user/group/find where name="ids-rest"]] = 0) do={
|
||||
/user/group/add name=ids-rest policy=read,write,rest-api comment="Suricata IDS REST group"
|
||||
}
|
||||
:if ([:len [/user/find where name="${ROUTEROS_REST_USER}"]] = 0) do={
|
||||
/user/add name="${ROUTEROS_REST_USER}" group=ids-rest address="${CONTAINER_IP_ONLY}/32" password="${ROUTEROS_REST_PASSWORD}"
|
||||
} else={
|
||||
/user/set [find where name="${ROUTEROS_REST_USER}"] group=ids-rest address="${CONTAINER_IP_ONLY}/32" password="${ROUTEROS_REST_PASSWORD}"
|
||||
}
|
||||
RSC
|
||||
fi
|
||||
|
||||
if [ "$ENABLE_WWW_SSL" = "true" ]; then
|
||||
cat >> "$LOCAL_RSC" <<'RSC'
|
||||
/ip/service/enable www-ssl
|
||||
RSC
|
||||
fi
|
||||
|
||||
cat >> "$LOCAL_RSC" <<RSC
|
||||
|
||||
:if ([:len [/ip/firewall/filter/find where comment="IDS-BLOCK source"]] = 0) do={
|
||||
/ip/firewall/filter/add chain=forward action=drop src-address-list="${ROUTEROS_ADDRESS_LIST}" disabled=yes comment="IDS-BLOCK source"
|
||||
}
|
||||
:if ([:len [/ip/firewall/filter/find where comment="IDS-BLOCK destination"]] = 0) do={
|
||||
/ip/firewall/filter/add chain=forward action=drop dst-address-list="${ROUTEROS_ADDRESS_LIST}" disabled=yes comment="IDS-BLOCK destination"
|
||||
}
|
||||
|
||||
:local old [/container/find where name="${CONTAINER_NAME}"]
|
||||
:if ([:len \$old] > 0) do={
|
||||
RSC
|
||||
if [ "$REPLACE_EXISTING" = "true" ]; then
|
||||
cat >> "$LOCAL_RSC" <<'RSC'
|
||||
:if ([/container/get $old status] = "running") do={
|
||||
/container/stop $old
|
||||
:delay 3s
|
||||
}
|
||||
/container/remove $old
|
||||
RSC
|
||||
else
|
||||
cat >> "$LOCAL_RSC" <<'RSC'
|
||||
:error "Container already exists and REPLACE_EXISTING=false"
|
||||
RSC
|
||||
fi
|
||||
|
||||
cat >> "$LOCAL_RSC" <<RSC
|
||||
}
|
||||
|
||||
/container/add file="${REMOTE_TAR_ROS}" interface="${CONTAINER_VETH}" root-dir="${ROOT_DIR}" mountlists=IDS_MOUNTS envlist=IDS_ENV name="${CONTAINER_NAME}" start-on-boot=yes logging=yes
|
||||
|
||||
:local cid [/container/find where name="${CONTAINER_NAME}"]
|
||||
:local tries 0
|
||||
:while (\$tries < 180) do={
|
||||
:if ([/container/get \$cid status] = "stopped") do={
|
||||
:set tries 999
|
||||
} else={
|
||||
:delay 2s
|
||||
:set tries (\$tries + 1)
|
||||
}
|
||||
}
|
||||
:if ([/container/get \$cid status] != "stopped") do={
|
||||
:error "Container image extraction did not reach stopped state"
|
||||
}
|
||||
/container/start \$cid
|
||||
:delay 5s
|
||||
RSC
|
||||
|
||||
if [ "$CONFIGURE_SNIFFER" = "true" ]; then
|
||||
cat >> "$LOCAL_RSC" <<RSC
|
||||
|
||||
/tool/sniffer/stop
|
||||
/tool/sniffer/set filter-vlan=${VLAN_ID} filter-direction=any filter-stream=yes only-headers=no streaming-enabled=yes streaming-server="${CONTAINER_IP_ONLY}" streaming-port=${TZSP_PORT}
|
||||
RSC
|
||||
if [ "$START_SNIFFER" = "true" ]; then
|
||||
cat >> "$LOCAL_RSC" <<'RSC'
|
||||
/tool/sniffer/start
|
||||
RSC
|
||||
fi
|
||||
fi
|
||||
|
||||
cat >> "$LOCAL_RSC" <<RSC
|
||||
|
||||
:log info "Suricata IDS deployment ${DEPLOY_ID}: container created and start requested"
|
||||
/container/print detail where name="${CONTAINER_NAME}"
|
||||
RSC
|
||||
|
||||
printf '[deploy] uploading image via SCP: %s -> %s:%s\n' "$LOCAL_TAR" "$SSH_TARGET" "$REMOTE_TAR_SCP"
|
||||
scp_put "$LOCAL_TAR" "$REMOTE_TAR_SCP"
|
||||
printf '[deploy] uploading RouterOS deployment script: %s\n' "$REMOTE_RSC_SCP"
|
||||
scp_put "$LOCAL_RSC" "$REMOTE_RSC_SCP"
|
||||
|
||||
echo "[deploy] importing configuration and starting container"
|
||||
ssh_run "/import file-name=\"${REMOTE_RSC_ROS}\""
|
||||
|
||||
if [ "$KEEP_REMOTE_TAR" != "true" ]; then
|
||||
ssh_run "/file/remove [find where name=\"${REMOTE_TAR_ROS}\"]" || true
|
||||
fi
|
||||
# The generated RSC can contain the REST password, so remove it after import.
|
||||
ssh_run "/file/remove [find where name=\"${REMOTE_RSC_ROS}\"]" || true
|
||||
|
||||
echo "[deploy] final status"
|
||||
ssh_run "/container/print detail where name=\"${CONTAINER_NAME}\""
|
||||
echo "[deploy] recent container log lines"
|
||||
ssh_run "/log/print without-paging where message~\"suricata|TZSP|IDS\"" || true
|
||||
|
||||
echo "[deploy] done"
|
||||
echo "Container IP: ${CONTAINER_IP_ONLY}"
|
||||
echo "Dashboard: http://${CONTAINER_IP_ONLY}:8080/"
|
||||
echo "AUTO_BLOCK=${AUTO_BLOCK}; firewall DROP rules remain disabled by design."
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
mkdir -p /data /var/log/suricata /var/lib/suricata/rules /run/suricata
|
||||
|
||||
case "${UPDATE_RULES_ON_START:-false}" in
|
||||
1|true|TRUE|yes|YES|on|ON)
|
||||
echo "[entrypoint] updating ET Open rules"
|
||||
if ! suricata-update; then
|
||||
echo "[entrypoint] WARNING: suricata-update failed; continuing with existing/local rules" >&2
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
RULES=/var/lib/suricata/rules/suricata.rules
|
||||
LOCAL=/opt/ids/suricata/local.rules
|
||||
[ -f "$RULES" ] || : > "$RULES"
|
||||
if ! grep -q 'sid:1000001;' "$RULES"; then
|
||||
printf '\n# ---- local project rules ----\n' >> "$RULES"
|
||||
cat "$LOCAL" >> "$RULES"
|
||||
fi
|
||||
|
||||
chown -R suricata:suricata /var/log/suricata /var/lib/suricata /run/suricata
|
||||
|
||||
exec python3 -m app.main
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "Docker Engine is required" >&2
|
||||
exit 2
|
||||
fi
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
echo "Docker Compose v2 is required (docker compose ...)" >&2
|
||||
exit 3
|
||||
fi
|
||||
if [ ! -c /dev/net/tun ]; then
|
||||
echo "/dev/net/tun is missing; TAP mode requires a Linux host with TUN/TAP enabled" >&2
|
||||
exit 4
|
||||
fi
|
||||
[ -f .env ] || cp .env.example .env
|
||||
|
||||
echo "[first-run] building and starting IDS"
|
||||
docker compose up -d --build
|
||||
|
||||
echo "[first-run] running end-to-end TZSP test"
|
||||
./scripts/selftest.sh
|
||||
|
||||
echo "[first-run] dashboard: http://127.0.0.1:8080/"
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen("http://127.0.0.1:8080/api/status", timeout=3) as response:
|
||||
data = json.load(response)
|
||||
raise SystemExit(0 if data.get("operational") else 1)
|
||||
except Exception as exc:
|
||||
print(exc, file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
CONFIG_FILE="${DEPLOY_CONFIG:-deploy-routeros.env}"
|
||||
[ ! -f "$CONFIG_FILE" ] || . "./$CONFIG_FILE"
|
||||
: "${ROUTER_HOST:=192.168.88.1}"
|
||||
: "${ROUTER_USER:=admin}"
|
||||
: "${ROUTER_PORT:=22}"
|
||||
: "${ROUTER_IDENTITY_FILE:=}"
|
||||
: "${CONTAINER_NAME:=suricata-ids}"
|
||||
TARGET="${ROUTER_USER}@${ROUTER_HOST}"
|
||||
run() {
|
||||
if [ -n "$ROUTER_IDENTITY_FILE" ]; then
|
||||
ssh -i "$ROUTER_IDENTITY_FILE" -p "$ROUTER_PORT" "$TARGET" "$1"
|
||||
else
|
||||
ssh -p "$ROUTER_PORT" "$TARGET" "$1"
|
||||
fi
|
||||
}
|
||||
run "/container/print detail where name=\"${CONTAINER_NAME}\""
|
||||
run "/tool/sniffer/print"
|
||||
run "/log/print without-paging where message~\"suricata|TZSP|IDS\""
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
[ -f .env ] || cp .env.example .env
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "docker is required for the full self-test" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "[selftest] waiting for dashboard/Suricata"
|
||||
i=0
|
||||
while [ "$i" -lt 30 ]; do
|
||||
if docker compose exec -T ids python3 /opt/ids/scripts/healthcheck.py >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
i=$((i + 1))
|
||||
sleep 1
|
||||
done
|
||||
if [ "$i" -ge 30 ]; then
|
||||
echo "[selftest] service did not become healthy" >&2
|
||||
docker compose logs --tail=100 ids >&2 || true
|
||||
exit 3
|
||||
fi
|
||||
|
||||
docker compose exec -T ids python3 /opt/ids/scripts/send_test_tzsp.py --host 127.0.0.1 --count 3
|
||||
sleep 3
|
||||
|
||||
docker compose exec -T ids python3 - <<'PY'
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
with urllib.request.urlopen('http://127.0.0.1:8080/api/alerts?limit=100', timeout=5) as r:
|
||||
data = json.load(r)
|
||||
match = [a for a in data.get('alerts', []) if a.get('signature_id') == 1000001]
|
||||
if not match:
|
||||
raise SystemExit('SELFTEST FAILED: SID 1000001 not found')
|
||||
print('SELFTEST OK: Suricata emitted LOCAL TZSP PIPELINE TEST')
|
||||
PY
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
|
||||
|
||||
def checksum(data: bytes) -> int:
|
||||
if len(data) % 2:
|
||||
data += b"\x00"
|
||||
total = sum(struct.unpack(f"!{len(data) // 2}H", data))
|
||||
total = (total >> 16) + (total & 0xFFFF)
|
||||
total += total >> 16
|
||||
return (~total) & 0xFFFF
|
||||
|
||||
|
||||
def ipv4_bytes(address: str) -> bytes:
|
||||
return socket.inet_aton(address)
|
||||
|
||||
|
||||
def build_icmp_frame(src_ip: str, dst_ip: str, sequence: int) -> bytes:
|
||||
dst_mac = bytes.fromhex("020000000002")
|
||||
src_mac = bytes.fromhex("020000000001")
|
||||
ethernet = dst_mac + src_mac + struct.pack("!H", 0x0800)
|
||||
|
||||
payload = b"routeros-suricata-tzsp-selftest"
|
||||
icmp_header = struct.pack("!BBHHH", 8, 0, 0, 0x1234, sequence & 0xFFFF)
|
||||
icmp_sum = checksum(icmp_header + payload)
|
||||
icmp = struct.pack("!BBHHH", 8, 0, icmp_sum, 0x1234, sequence & 0xFFFF) + payload
|
||||
|
||||
total_length = 20 + len(icmp)
|
||||
ip_header = struct.pack(
|
||||
"!BBHHHBBH4s4s",
|
||||
0x45,
|
||||
0,
|
||||
total_length,
|
||||
sequence & 0xFFFF,
|
||||
0,
|
||||
64,
|
||||
socket.IPPROTO_ICMP,
|
||||
0,
|
||||
ipv4_bytes(src_ip),
|
||||
ipv4_bytes(dst_ip),
|
||||
)
|
||||
ip_sum = checksum(ip_header)
|
||||
ip_header = struct.pack(
|
||||
"!BBHHHBBH4s4s",
|
||||
0x45,
|
||||
0,
|
||||
total_length,
|
||||
sequence & 0xFFFF,
|
||||
0,
|
||||
64,
|
||||
socket.IPPROTO_ICMP,
|
||||
ip_sum,
|
||||
ipv4_bytes(src_ip),
|
||||
ipv4_bytes(dst_ip),
|
||||
)
|
||||
return ethernet + ip_header + icmp
|
||||
|
||||
|
||||
def wrap_tzsp(frame: bytes) -> bytes:
|
||||
# Version=1, Type=0 (received packet), Encapsulation=1 (Ethernet), END tag=1.
|
||||
return b"\x01\x00\x00\x01\x01" + frame
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Send deterministic TZSP Ethernet frames for Suricata testing")
|
||||
parser.add_argument("--host", default="127.0.0.1")
|
||||
parser.add_argument("--port", type=int, default=37008)
|
||||
parser.add_argument("--count", type=int, default=3)
|
||||
parser.add_argument("--src-ip", default="192.168.100.10")
|
||||
parser.add_argument("--dst-ip", default="1.1.1.1")
|
||||
args = parser.parse_args()
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
for seq in range(1, args.count + 1):
|
||||
packet = wrap_tzsp(build_icmp_frame(args.src_ip, args.dst_ip, seq))
|
||||
sock.sendto(packet, (args.host, args.port))
|
||||
print(f"sent TZSP test datagram {seq}/{args.count} to {args.host}:{args.port}")
|
||||
time.sleep(0.1)
|
||||
sock.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if ! command -v suricata-update >/dev/null 2>&1; then
|
||||
cd "$(dirname "$0")/.."
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
exec docker compose exec -T ids /opt/ids/scripts/update-rules.sh
|
||||
fi
|
||||
echo "suricata-update is not installed; run this script inside the IDS container" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
suricata-update
|
||||
RULES=/var/lib/suricata/rules/suricata.rules
|
||||
LOCAL=/opt/ids/suricata/local.rules
|
||||
if ! grep -q 'sid:1000001;' "$RULES"; then
|
||||
printf '\n# ---- local project rules ----\n' >> "$RULES"
|
||||
cat "$LOCAL" >> "$RULES"
|
||||
fi
|
||||
chown suricata:suricata "$RULES"
|
||||
|
||||
PID=""
|
||||
if [ -f /run/suricata.pid ]; then
|
||||
PID="$(cat /run/suricata.pid)"
|
||||
fi
|
||||
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
|
||||
kill -USR2 "$PID"
|
||||
echo "Rules updated; live reload requested for Suricata PID $PID"
|
||||
else
|
||||
echo "Rules updated; Suricata is not currently running"
|
||||
fi
|
||||
Reference in New Issue
Block a user