worked poc
This commit is contained in:
@@ -25,6 +25,10 @@ OUT="build/routeros-suricata-tzsp-${ARCH}.tar"
|
||||
SHA="${OUT}.sha256"
|
||||
mkdir -p build
|
||||
|
||||
# podman save with the docker-archive transport refuses to modify an
|
||||
# existing archive. Remove artifacts from a previous build before exporting.
|
||||
rm -f "$OUT" "$SHA"
|
||||
|
||||
if [ -n "${ENGINE:-}" ]; then
|
||||
engine="$ENGINE"
|
||||
elif command -v podman >/dev/null 2>&1; then
|
||||
|
||||
+100
-95
@@ -12,14 +12,21 @@ if [ -f "$CONFIG_FILE" ]; then
|
||||
esac
|
||||
fi
|
||||
|
||||
VERSION="$(tr -d '[:space:]' < VERSION)"
|
||||
[ -n "$VERSION" ] || { echo "VERSION is empty" >&2; exit 2; }
|
||||
case "$VERSION" in
|
||||
*[!A-Za-z0-9._-]*) echo "VERSION contains unsupported characters: $VERSION" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
CONTAINER_NAME="suricata_${VERSION}"
|
||||
ROOT_DIR="/containers/${CONTAINER_NAME}/root"
|
||||
|
||||
: "${ROUTER_HOST:=192.168.88.1}"
|
||||
: "${ROUTER_USER:=admin}"
|
||||
: "${ROUTER_PORT:=22}"
|
||||
: "${ROUTER_IDENTITY_FILE:=}"
|
||||
: "${ROUTER_ARCH:=auto}"
|
||||
: "${ROUTER_SCP_DIR:=/}"
|
||||
: "${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}"
|
||||
@@ -35,15 +42,43 @@ fi
|
||||
: "${AUTO_BLOCK_MAX_SEVERITY:=1}"
|
||||
: "${BLOCK_TIMEOUT:=1h}"
|
||||
: "${UPDATE_RULES_ON_START:=false}"
|
||||
: "${RULE_UPDATE_INTERVAL_HOURS:=24}"
|
||||
: "${ALERT_RETENTION_DAYS:=14}"
|
||||
: "${ALERT_MAX_SEVERITY:=2}"
|
||||
: "${ALERT_DEDUP_WINDOW_SECONDS:=300}"
|
||||
: "${ALERT_IGNORE_SIDS:=1000001}"
|
||||
: "${ALERT_IGNORE_CATEGORIES:=}"
|
||||
: "${ADMIN_TOKEN:=}"
|
||||
: "${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:=}"
|
||||
: "${KEEP_REMOTE_RSC:=false}"
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $0 <RouterOS image TAR path>
|
||||
|
||||
The TAR must already exist on RouterOS. Example:
|
||||
./scripts/upload-routeros-image.sh build/routeros-suricata-tzsp-arm64.tar
|
||||
$0 routeros-suricata-tzsp-arm64.tar
|
||||
|
||||
Container name: ${CONTAINER_NAME}
|
||||
Root dir: ${ROOT_DIR}
|
||||
USAGE
|
||||
}
|
||||
|
||||
IMAGE_TAR_ROS="${1:-}"
|
||||
if [ -z "$IMAGE_TAR_ROS" ]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
case "$IMAGE_TAR_ROS" in
|
||||
*.tar) ;;
|
||||
*) echo "RouterOS image path must point to a .tar file: $IMAGE_TAR_ROS" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
need() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
@@ -64,8 +99,6 @@ 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"
|
||||
@@ -76,7 +109,7 @@ check_ros_value() {
|
||||
;;
|
||||
esac
|
||||
case "$value" in
|
||||
*'\n'*|*'\r'*)
|
||||
*"\n"*|*"\r"*)
|
||||
echo "$label contains a newline" >&2
|
||||
exit 3
|
||||
;;
|
||||
@@ -84,8 +117,8 @@ check_ros_value() {
|
||||
}
|
||||
|
||||
for pair in \
|
||||
"IMAGE_TAR_ROS=$IMAGE_TAR_ROS" \
|
||||
"ROUTER_DISK=$ROUTER_DISK" \
|
||||
"CONTAINER_NAME=$CONTAINER_NAME" \
|
||||
"CONTAINER_IP=$CONTAINER_IP" \
|
||||
"CONTAINER_GATEWAY=$CONTAINER_GATEWAY" \
|
||||
"CONTAINER_SUBNET=$CONTAINER_SUBNET" \
|
||||
@@ -94,6 +127,9 @@ for pair in \
|
||||
"SURICATA_HOME_NET=$SURICATA_HOME_NET" \
|
||||
"MONITORED_NETWORKS=$MONITORED_NETWORKS" \
|
||||
"BLOCK_TIMEOUT=$BLOCK_TIMEOUT" \
|
||||
"ALERT_IGNORE_SIDS=$ALERT_IGNORE_SIDS" \
|
||||
"ALERT_IGNORE_CATEGORIES=$ALERT_IGNORE_CATEGORIES" \
|
||||
"ADMIN_TOKEN=$ADMIN_TOKEN" \
|
||||
"ROUTEROS_REST_USER=$ROUTEROS_REST_USER" \
|
||||
"ROUTEROS_REST_PASSWORD=$ROUTEROS_REST_PASSWORD" \
|
||||
"ROUTEROS_ADDRESS_LIST=$ROUTEROS_ADDRESS_LIST"
|
||||
@@ -119,73 +155,53 @@ scp_put() {
|
||||
fi
|
||||
}
|
||||
|
||||
printf '[deploy] version: %s\n' "$VERSION"
|
||||
printf '[deploy] container: %s\n' "$CONTAINER_NAME"
|
||||
printf '[deploy] image on RouterOS: %s\n' "$IMAGE_TAR_ROS"
|
||||
printf '[deploy] root-dir: %s\n' "$ROOT_DIR"
|
||||
|
||||
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
|
||||
echo "RouterOS container menu is unavailable." >&2
|
||||
exit 4
|
||||
fi
|
||||
if ! ssh_run "/file/print without-paging where name=\"${IMAGE_TAR_ROS}\"" | grep -F "$IMAGE_TAR_ROS" >/dev/null 2>&1; then
|
||||
echo "Image TAR not found on RouterOS: $IMAGE_TAR_ROS" >&2
|
||||
echo "Upload it first with scripts/upload-routeros-image.sh." >&2
|
||||
exit 5
|
||||
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
|
||||
echo "RouterOS sniffer is unavailable." >&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
|
||||
|
||||
DEPLOY_ID="$(date -u +%Y%m%d%H%M%S)"
|
||||
mkdir -p build
|
||||
LOCAL_RSC="build/deploy-${CONTAINER_NAME}-${DEPLOY_ID}.rsc"
|
||||
REMOTE_RSC_NAME="deploy-${CONTAINER_NAME}-${DEPLOY_ID}.rsc"
|
||||
REMOTE_RSC_SCP="${ROUTER_SCP_DIR%/}/${REMOTE_RSC_NAME}"
|
||||
[ "$ROUTER_SCP_DIR" = "/" ] && REMOTE_RSC_SCP="/${REMOTE_RSC_NAME}"
|
||||
|
||||
# Keep persistent application state stable between versioned containers.
|
||||
DATA_DIR="${ROUTER_DISK}/containers/suricata-data"
|
||||
LOG_DIR="${ROUTER_DISK}/containers/suricata-logs"
|
||||
RULES_DIR="${ROUTER_DISK}/containers/suricata-rules"
|
||||
REST_URL="https://${CONTAINER_GATEWAY}"
|
||||
CONTAINER_IP_ONLY="${CONTAINER_IP%/*}"
|
||||
|
||||
cat > "$LOCAL_RSC" <<RSC
|
||||
# Generated by scripts/deploy-routeros.sh at ${DEPLOY_ID} UTC.
|
||||
# Image: ${REMOTE_TAR_ROS}
|
||||
# Version: ${VERSION}
|
||||
# Container: ${CONTAINER_NAME}
|
||||
# Image: ${IMAGE_TAR_ROS}
|
||||
# Root: ${ROOT_DIR}
|
||||
|
||||
:if ([:len [/interface/bridge/find where name="${CONTAINER_BRIDGE}"]] = 0) do={
|
||||
/interface/bridge/add name="${CONTAINER_BRIDGE}" comment="Suricata IDS container bridge"
|
||||
@@ -219,6 +235,13 @@ cat > "$LOCAL_RSC" <<RSC
|
||||
/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/envs/add list=IDS_ENV key=RULE_UPDATE_INTERVAL_HOURS value="${RULE_UPDATE_INTERVAL_HOURS}"
|
||||
/container/envs/add list=IDS_ENV key=ALERT_RETENTION_DAYS value="${ALERT_RETENTION_DAYS}"
|
||||
/container/envs/add list=IDS_ENV key=ALERT_MAX_SEVERITY value="${ALERT_MAX_SEVERITY}"
|
||||
/container/envs/add list=IDS_ENV key=ALERT_DEDUP_WINDOW_SECONDS value="${ALERT_DEDUP_WINDOW_SECONDS}"
|
||||
/container/envs/add list=IDS_ENV key=ALERT_IGNORE_SIDS value="${ALERT_IGNORE_SIDS}"
|
||||
/container/envs/add list=IDS_ENV key=ALERT_IGNORE_CATEGORIES value="${ALERT_IGNORE_CATEGORIES}"
|
||||
/container/envs/add list=IDS_ENV key=ADMIN_TOKEN value="${ADMIN_TOKEN}"
|
||||
|
||||
/container/mounts/remove [find where list="IDS_MOUNTS"]
|
||||
/container/mounts/add list=IDS_MOUNTS src="${DATA_DIR}" dst=/data
|
||||
@@ -255,42 +278,26 @@ cat >> "$LOCAL_RSC" <<RSC
|
||||
/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
|
||||
:if ([:len [/container/find where name="${CONTAINER_NAME}"]] > 0) do={
|
||||
:error "Container ${CONTAINER_NAME} already exists"
|
||||
}
|
||||
|
||||
/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
|
||||
/container/add name="${CONTAINER_NAME}" file="${IMAGE_TAR_ROS}" interface="${CONTAINER_VETH}" root-dir="${ROOT_DIR}" mountlists=IDS_MOUNTS envlist=IDS_ENV start-on-boot=yes logging=yes
|
||||
|
||||
:local cid [/container/find where name="${CONTAINER_NAME}"]
|
||||
# Wait until the newly added image has finished extracting, using find filters only.
|
||||
:local tries 0
|
||||
:while (\$tries < 180) do={
|
||||
:if ([/container/get \$cid status] = "stopped") do={
|
||||
:if ([:len [/container/find where name="${CONTAINER_NAME}" and status="stopped"]] > 0) 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"
|
||||
:if ([:len [/container/find where name="${CONTAINER_NAME}" and status="stopped"]] = 0) do={
|
||||
:error "Container extraction did not reach stopped state"
|
||||
}
|
||||
/container/start \$cid
|
||||
/container/start [find where name="${CONTAINER_NAME}"]
|
||||
:delay 5s
|
||||
RSC
|
||||
|
||||
@@ -309,23 +316,20 @@ fi
|
||||
|
||||
cat >> "$LOCAL_RSC" <<RSC
|
||||
|
||||
:log info "Suricata IDS deployment ${DEPLOY_ID}: container created and start requested"
|
||||
:log info "Suricata deployment ${VERSION}: ${CONTAINER_NAME} created from ${IMAGE_TAR_ROS}"
|
||||
/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}\""
|
||||
echo "[deploy] importing configuration and creating ${CONTAINER_NAME}"
|
||||
ssh_run "/import file-name=\"${REMOTE_RSC_NAME}\""
|
||||
|
||||
if [ "$KEEP_REMOTE_TAR" != "true" ]; then
|
||||
ssh_run "/file/remove [find where name=\"${REMOTE_TAR_ROS}\"]" || true
|
||||
if [ "$KEEP_REMOTE_RSC" != "true" ]; then
|
||||
# The generated RSC may contain the REST password.
|
||||
ssh_run "/file/remove [find where name=\"${REMOTE_RSC_NAME}\"]" || 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}\""
|
||||
@@ -333,6 +337,7 @@ 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 "Container: ${CONTAINER_NAME}"
|
||||
echo "Image: ${IMAGE_TAR_ROS}"
|
||||
echo "Root dir: ${ROOT_DIR}"
|
||||
echo "Dashboard: http://${CONTAINER_IP_ONLY}:8080/"
|
||||
echo "AUTO_BLOCK=${AUTO_BLOCK}; firewall DROP rules remain disabled by design."
|
||||
|
||||
+38
-9
@@ -1,25 +1,54 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
mkdir -p /data /var/log/suricata /var/lib/suricata/rules /run/suricata
|
||||
mkdir -p /data /data/suricata /var/log/suricata /var/lib/suricata/rules /run/suricata
|
||||
|
||||
if ! id -u suricata >/dev/null 2>&1 || ! getent group suricata >/dev/null 2>&1; then
|
||||
echo "[entrypoint] FATAL: missing suricata user/group in the image; rebuild the image from the current Dockerfile" >&2
|
||||
exit 70
|
||||
fi
|
||||
|
||||
init_file() {
|
||||
src="$1"
|
||||
dst="$2"
|
||||
if [ ! -e "$dst" ]; then
|
||||
cp "$src" "$dst"
|
||||
fi
|
||||
chmod 0644 "$dst"
|
||||
}
|
||||
|
||||
cp /opt/ids/suricata/local.rules /data/suricata/local.rules
|
||||
chmod 0644 /data/suricata/local.rules
|
||||
init_file /opt/ids/suricata/custom.rules.default /data/suricata/custom.rules
|
||||
init_file /opt/ids/suricata/threshold.config /data/suricata/threshold.config
|
||||
init_file /opt/ids/suricata/disable.conf /data/suricata/disable.conf
|
||||
init_file /opt/ids/suricata/enable.conf /data/suricata/enable.conf
|
||||
init_file /opt/ids/suricata/modify.conf /data/suricata/modify.conf
|
||||
|
||||
# RouterOS mounts /var/lib/suricata from persistent storage. On the first
|
||||
# deployment that mount is empty, so seed it from the ET/Open snapshot baked
|
||||
# into the image before optional online updates run.
|
||||
if [ ! -s /var/lib/suricata/rules/suricata.rules ] && [ -d /opt/ids/vendor-rules-seed ]; then
|
||||
echo "[entrypoint] seeding baseline vendor rules into persistent storage"
|
||||
cp -a /opt/ids/vendor-rules-seed/. /var/lib/suricata/
|
||||
fi
|
||||
|
||||
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
|
||||
echo "[entrypoint] updating managed rules"
|
||||
if ! /opt/ids/scripts/update-rules.sh --no-reload; then
|
||||
echo "[entrypoint] WARNING: suricata-update failed; continuing with existing 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
|
||||
# Rule state is edited by the root Python supervisor but must remain readable by
|
||||
# the Suricata process after it drops privileges.
|
||||
chmod 0755 /data /data/suricata || true
|
||||
chmod 0644 /data/suricata/* 2>/dev/null || true
|
||||
|
||||
exec python3 -m app.main
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen("http://127.0.0.1:8080/api/status", timeout=3) as response:
|
||||
port = int(os.getenv("WEB_PORT", "8080"))
|
||||
with urllib.request.urlopen(f"http://127.0.0.1:{port}/api/status", timeout=3) as response:
|
||||
data = json.load(response)
|
||||
raise SystemExit(0 if data.get("operational") else 1)
|
||||
except Exception as exc:
|
||||
|
||||
@@ -7,7 +7,8 @@ CONFIG_FILE="${DEPLOY_CONFIG:-deploy-routeros.env}"
|
||||
: "${ROUTER_USER:=admin}"
|
||||
: "${ROUTER_PORT:=22}"
|
||||
: "${ROUTER_IDENTITY_FILE:=}"
|
||||
: "${CONTAINER_NAME:=suricata-ids}"
|
||||
VERSION="$(tr -d '[:space:]' < VERSION)"
|
||||
: "${CONTAINER_NAME:=suricata_${VERSION}}"
|
||||
TARGET="${ROUTER_USER}@${ROUTER_HOST}"
|
||||
run() {
|
||||
if [ -n "$ROUTER_IDENTITY_FILE" ]; then
|
||||
|
||||
+28
-8
@@ -24,17 +24,37 @@ if [ "$i" -ge 30 ]; then
|
||||
exit 3
|
||||
fi
|
||||
|
||||
START_SIZE="$(docker compose exec -T ids python3 - <<'PY'
|
||||
import os
|
||||
print(os.path.getsize('/var/log/suricata/eve.json') if os.path.exists('/var/log/suricata/eve.json') else 0)
|
||||
PY
|
||||
)"
|
||||
START_SIZE="$(printf '%s' "$START_SIZE" | tr -d '\r\n ')"
|
||||
|
||||
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'
|
||||
docker compose exec -T -e SELFTEST_START_SIZE="$START_SIZE" ids python3 - <<'PY'
|
||||
import json
|
||||
import urllib.request
|
||||
import os
|
||||
|
||||
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')
|
||||
path = '/var/log/suricata/eve.json'
|
||||
start = int(os.environ.get('SELFTEST_START_SIZE', '0'))
|
||||
found = 0
|
||||
with open(path, 'r', encoding='utf-8', errors='replace') as handle:
|
||||
try:
|
||||
handle.seek(start)
|
||||
except OSError:
|
||||
handle.seek(0)
|
||||
for line in handle:
|
||||
try:
|
||||
event = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
alert = event.get('alert') or {}
|
||||
if alert.get('signature_id') == 1000001:
|
||||
found += 1
|
||||
if not found:
|
||||
raise SystemExit('SELFTEST FAILED: Suricata did not emit reserved SID 1000001')
|
||||
print(f'SELFTEST OK: Suricata emitted {found} marked TZSP pipeline test alert(s); UI filtering remains enabled')
|
||||
PY
|
||||
|
||||
+71
-9
@@ -1,23 +1,85 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
NO_RELOAD=false
|
||||
if [ "${1:-}" = "--no-reload" ]; then
|
||||
NO_RELOAD=true
|
||||
fi
|
||||
|
||||
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
|
||||
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"
|
||||
STATE_DIR="${SURICATA_STATE_DIR:-/data/suricata}"
|
||||
RULES="/var/lib/suricata/rules/suricata.rules"
|
||||
SURICATA_CONFIG="${SURICATA_CONFIG:-/etc/suricata/suricata.yaml}"
|
||||
SURICATA_HOME_NET="${SURICATA_HOME_NET:-[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]}"
|
||||
SURICATA_EXTRA_RULES_GLOB="${SURICATA_EXTRA_RULES_GLOB:-/data/suricata/*.rules}"
|
||||
SURICATA_THRESHOLD_CONFIG="${SURICATA_THRESHOLD_CONFIG:-/data/suricata/threshold.config}"
|
||||
|
||||
mkdir -p "$STATE_DIR" /var/lib/suricata/rules
|
||||
for name in disable.conf enable.conf modify.conf threshold.config; do
|
||||
[ -f "$STATE_DIR/$name" ] || : > "$STATE_DIR/$name"
|
||||
done
|
||||
|
||||
TMP_DIR="$(mktemp -d /tmp/suricata-rule-update.XXXXXX)"
|
||||
BACKUP="$TMP_DIR/suricata.rules.previous"
|
||||
VALIDATE_LOG="$TMP_DIR/validate-log"
|
||||
mkdir -p "$VALIDATE_LOG"
|
||||
HAD_RULES=false
|
||||
if [ -s "$RULES" ]; then
|
||||
cp -p "$RULES" "$BACKUP"
|
||||
HAD_RULES=true
|
||||
fi
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
|
||||
restore_previous_rules() {
|
||||
if [ "$HAD_RULES" = "true" ]; then
|
||||
cp -p "$BACKUP" "$RULES"
|
||||
else
|
||||
rm -f "$RULES"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "[rules] downloading enabled feeds with suricata-update"
|
||||
if ! suricata-update \
|
||||
--disable-conf="$STATE_DIR/disable.conf" \
|
||||
--enable-conf="$STATE_DIR/enable.conf" \
|
||||
--modify-conf="$STATE_DIR/modify.conf"; then
|
||||
echo "[rules] download/update failed; restoring previous rules" >&2
|
||||
restore_previous_rules
|
||||
exit 10
|
||||
fi
|
||||
|
||||
[ -f "$RULES" ] || : > "$RULES"
|
||||
|
||||
echo "[rules] validating downloaded rules before activation"
|
||||
if ! suricata -T \
|
||||
-c "$SURICATA_CONFIG" \
|
||||
-l "$VALIDATE_LOG" \
|
||||
-s "$SURICATA_EXTRA_RULES_GLOB" \
|
||||
--set "vars.address-groups.HOME_NET=$SURICATA_HOME_NET" \
|
||||
--set "threshold-file=$SURICATA_THRESHOLD_CONFIG"; then
|
||||
echo "[rules] validation failed; restoring previous known-good rules" >&2
|
||||
restore_previous_rules
|
||||
exit 11
|
||||
fi
|
||||
|
||||
chown suricata:suricata "$RULES"
|
||||
chmod 0644 "$RULES"
|
||||
|
||||
if [ "$NO_RELOAD" = "true" ]; then
|
||||
echo "Rules downloaded, validated and installed; reload skipped"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PID=""
|
||||
if [ -f /run/suricata.pid ]; then
|
||||
@@ -25,7 +87,7 @@ if [ -f /run/suricata.pid ]; then
|
||||
fi
|
||||
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
|
||||
kill -USR2 "$PID"
|
||||
echo "Rules updated; live reload requested for Suricata PID $PID"
|
||||
echo "Rules downloaded, validated and installed; live reload requested for Suricata PID $PID"
|
||||
else
|
||||
echo "Rules updated; Suricata is not currently running"
|
||||
echo "Rules downloaded, validated and installed; Suricata is not currently running"
|
||||
fi
|
||||
|
||||
Executable
+170
@@ -0,0 +1,170 @@
|
||||
#!/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
|
||||
|
||||
VERSION="$(tr -d '[:space:]' < VERSION)"
|
||||
[ -n "$VERSION" ] || { echo "VERSION is empty" >&2; exit 2; }
|
||||
case "$VERSION" in
|
||||
*[!A-Za-z0-9._-]*) echo "VERSION contains unsupported characters: $VERSION" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
CONTAINER_NAME="suricata_${VERSION}"
|
||||
ROOT_DIR="/containers/${CONTAINER_NAME}/root"
|
||||
|
||||
: "${ROUTER_HOST:=192.168.88.1}"
|
||||
: "${ROUTER_USER:=admin}"
|
||||
: "${ROUTER_PORT:=22}"
|
||||
: "${ROUTER_IDENTITY_FILE:=}"
|
||||
: "${CONTAINER_VETH:=veth-ids}"
|
||||
: "${CONTAINER_ENVLIST:=IDS_ENV}"
|
||||
: "${CONTAINER_MOUNTLIST:=IDS_MOUNTS}"
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $0 <TAR already uploaded to RouterOS>
|
||||
|
||||
This is an image-only container upgrade. It DOES NOT change:
|
||||
- bridge/IP/NAT/veth configuration,
|
||||
- TZSP/sniffer configuration,
|
||||
- firewall or REST configuration,
|
||||
- envlist or mount definitions.
|
||||
|
||||
It only disables/stops older suricata_* containers, creates:
|
||||
name=${CONTAINER_NAME}
|
||||
file=<TAR>
|
||||
root-dir=${ROOT_DIR}
|
||||
|
||||
and reuses:
|
||||
interface=${CONTAINER_VETH}
|
||||
envlist=${CONTAINER_ENVLIST}
|
||||
mountlists=${CONTAINER_MOUNTLIST}
|
||||
USAGE
|
||||
}
|
||||
|
||||
IMAGE_TAR_ROS="${1:-}"
|
||||
if [ -z "$IMAGE_TAR_ROS" ]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
case "$IMAGE_TAR_ROS" in
|
||||
*.tar) ;;
|
||||
*) echo "RouterOS image path must point to a .tar file: $IMAGE_TAR_ROS" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
command -v ssh >/dev/null 2>&1 || { echo "ssh is required" >&2; exit 2; }
|
||||
command -v scp >/dev/null 2>&1 || { echo "scp is required" >&2; exit 2; }
|
||||
case "$ROUTER_PORT" in
|
||||
*[!0-9]*|'') echo "ROUTER_PORT must be numeric" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
for value in "$IMAGE_TAR_ROS" "$CONTAINER_VETH" "$CONTAINER_ENVLIST" "$CONTAINER_MOUNTLIST"; do
|
||||
case "$value" in
|
||||
*'"'*|*'\\'*|*'$'*|*';'*|*'`'*) echo "Unsupported character in RouterOS value: $value" >&2; exit 3 ;;
|
||||
esac
|
||||
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
|
||||
}
|
||||
|
||||
printf '[upgrade] version: %s\n' "$VERSION"
|
||||
printf '[upgrade] new container: %s\n' "$CONTAINER_NAME"
|
||||
printf '[upgrade] image on RouterOS: %s\n' "$IMAGE_TAR_ROS"
|
||||
printf '[upgrade] root-dir: %s\n' "$ROOT_DIR"
|
||||
printf '[upgrade] reusing interface/env/mounts: %s / %s / %s\n' "$CONTAINER_VETH" "$CONTAINER_ENVLIST" "$CONTAINER_MOUNTLIST"
|
||||
|
||||
echo '[upgrade] read-only preflight'
|
||||
ssh_run '/container/print' >/dev/null
|
||||
ssh_run "/file/print without-paging where name=\"${IMAGE_TAR_ROS}\"" | grep -F "$IMAGE_TAR_ROS" >/dev/null 2>&1 || {
|
||||
echo "Image TAR not found on RouterOS: $IMAGE_TAR_ROS" >&2
|
||||
echo "Upload it first with scripts/upload-routeros-image.sh." >&2
|
||||
exit 4
|
||||
}
|
||||
ssh_run "/interface/veth/print without-paging where name=\"${CONTAINER_VETH}\"" | grep -F "$CONTAINER_VETH" >/dev/null 2>&1 || {
|
||||
echo "Existing veth not found: $CONTAINER_VETH" >&2
|
||||
echo "Run the normal deploy once before using image-only upgrades." >&2
|
||||
exit 5
|
||||
}
|
||||
if ! ssh_run "/container/envs/print without-paging where list=\"${CONTAINER_ENVLIST}\"" | grep -F "$CONTAINER_ENVLIST" >/dev/null 2>&1; then
|
||||
echo "Existing envlist not found or empty: $CONTAINER_ENVLIST" >&2
|
||||
exit 5
|
||||
fi
|
||||
if ! ssh_run "/container/mounts/print without-paging where list=\"${CONTAINER_MOUNTLIST}\"" | grep -F "$CONTAINER_MOUNTLIST" >/dev/null 2>&1; then
|
||||
echo "Existing mountlist not found or empty: $CONTAINER_MOUNTLIST" >&2
|
||||
exit 5
|
||||
fi
|
||||
if ssh_run "/container/print without-paging where name=\"${CONTAINER_NAME}\"" | grep -F "$CONTAINER_NAME" >/dev/null 2>&1; then
|
||||
echo "Container already exists: $CONTAINER_NAME" >&2
|
||||
echo "Bump VERSION or remove that container explicitly before retrying." >&2
|
||||
exit 6
|
||||
fi
|
||||
|
||||
DEPLOY_ID="$(date -u +%Y%m%d%H%M%S)"
|
||||
mkdir -p build
|
||||
LOCAL_RSC="build/upgrade-${CONTAINER_NAME}-${DEPLOY_ID}.rsc"
|
||||
REMOTE_RSC_NAME="upgrade-${CONTAINER_NAME}-${DEPLOY_ID}.rsc"
|
||||
|
||||
cat > "$LOCAL_RSC" <<RSC
|
||||
# Image-only Suricata container upgrade.
|
||||
# This script intentionally does not modify networking, sniffer, firewall,
|
||||
# envlist definitions or mount definitions.
|
||||
|
||||
:foreach c in=[/container/find where name~"^suricata_"] do={
|
||||
/container/set \$c start-on-boot=no
|
||||
}
|
||||
|
||||
:local running [/container/find where name~"^suricata_" and status="running"]
|
||||
:if ([:len \$running] > 0) do={
|
||||
/container/stop \$running
|
||||
:delay 3s
|
||||
}
|
||||
|
||||
/container/add name="${CONTAINER_NAME}" file="${IMAGE_TAR_ROS}" interface="${CONTAINER_VETH}" root-dir="${ROOT_DIR}" mountlists="${CONTAINER_MOUNTLIST}" envlist="${CONTAINER_ENVLIST}" start-on-boot=yes logging=yes
|
||||
|
||||
:local tries 0
|
||||
:while (\$tries < 180) do={
|
||||
:if ([:len [/container/find where name="${CONTAINER_NAME}" and status="stopped"]] > 0) do={
|
||||
:set tries 999
|
||||
} else={
|
||||
:delay 2s
|
||||
:set tries (\$tries + 1)
|
||||
}
|
||||
}
|
||||
:if ([:len [/container/find where name="${CONTAINER_NAME}" and status="stopped"]] = 0) do={
|
||||
:error "Container extraction did not reach stopped state"
|
||||
}
|
||||
|
||||
/container/start [find where name="${CONTAINER_NAME}"]
|
||||
:delay 5s
|
||||
/container/print detail where name="${CONTAINER_NAME}"
|
||||
RSC
|
||||
|
||||
if [ -n "$ROUTER_IDENTITY_FILE" ]; then
|
||||
scp -i "$ROUTER_IDENTITY_FILE" -P "$ROUTER_PORT" "$LOCAL_RSC" "${SSH_TARGET}:/${REMOTE_RSC_NAME}"
|
||||
else
|
||||
scp -P "$ROUTER_PORT" "$LOCAL_RSC" "${SSH_TARGET}:/${REMOTE_RSC_NAME}"
|
||||
fi
|
||||
|
||||
echo "[upgrade] creating ${CONTAINER_NAME} without touching existing network configuration"
|
||||
ssh_run "/import file-name=\"${REMOTE_RSC_NAME}\""
|
||||
ssh_run "/file/remove [find where name=\"${REMOTE_RSC_NAME}\"]" || true
|
||||
|
||||
echo '[upgrade] done'
|
||||
echo "New container: ${CONTAINER_NAME}"
|
||||
echo "Root dir: ${ROOT_DIR}"
|
||||
echo "Older suricata_* containers were left in place, stopped and start-on-boot=no."
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/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_SCP_DIR:=/}"
|
||||
: "${REMOTE_IMAGE_NAME:=}"
|
||||
|
||||
IMAGE_PATH="${1:-}"
|
||||
if [ -z "$IMAGE_PATH" ]; then
|
||||
echo "Usage: $0 /path/to/ready-image.tar" >&2
|
||||
echo "This script only uploads the TAR. It never builds or deploys it." >&2
|
||||
exit 2
|
||||
fi
|
||||
[ -f "$IMAGE_PATH" ] || { echo "Ready image TAR not found: $IMAGE_PATH" >&2; exit 2; }
|
||||
case "$IMAGE_PATH" in
|
||||
*.tar) ;;
|
||||
*) echo "Expected a .tar image archive: $IMAGE_PATH" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
command -v scp >/dev/null 2>&1 || { echo "scp is required" >&2; exit 2; }
|
||||
command -v ssh >/dev/null 2>&1 || { echo "ssh is required" >&2; exit 2; }
|
||||
|
||||
if [ -z "$REMOTE_IMAGE_NAME" ]; then
|
||||
REMOTE_IMAGE_NAME="$(basename "$IMAGE_PATH")"
|
||||
fi
|
||||
case "$REMOTE_IMAGE_NAME" in
|
||||
*'/'*|*'\\'*|*'"'*|*';'*|*'$'*|*'`'*)
|
||||
echo "REMOTE_IMAGE_NAME contains unsupported characters" >&2
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
|
||||
SSH_TARGET="${ROUTER_USER}@${ROUTER_HOST}"
|
||||
REMOTE_PATH="${ROUTER_SCP_DIR%/}/${REMOTE_IMAGE_NAME}"
|
||||
[ "$ROUTER_SCP_DIR" = "/" ] && REMOTE_PATH="/${REMOTE_IMAGE_NAME}"
|
||||
REMOTE_ROS_NAME="${REMOTE_PATH#/}"
|
||||
|
||||
printf '[upload] %s -> %s:%s\n' "$IMAGE_PATH" "$SSH_TARGET" "$REMOTE_PATH"
|
||||
if [ -n "$ROUTER_IDENTITY_FILE" ]; then
|
||||
scp -i "$ROUTER_IDENTITY_FILE" -P "$ROUTER_PORT" "$IMAGE_PATH" "${SSH_TARGET}:$REMOTE_PATH"
|
||||
ssh -i "$ROUTER_IDENTITY_FILE" -p "$ROUTER_PORT" "$SSH_TARGET" "/file/print without-paging where name=\"${REMOTE_ROS_NAME}\"" || true
|
||||
else
|
||||
scp -P "$ROUTER_PORT" "$IMAGE_PATH" "${SSH_TARGET}:$REMOTE_PATH"
|
||||
ssh -p "$ROUTER_PORT" "$SSH_TARGET" "/file/print without-paging where name=\"${REMOTE_ROS_NAME}\"" || true
|
||||
fi
|
||||
|
||||
echo "[upload] done"
|
||||
echo "RouterOS file: ${REMOTE_ROS_NAME}"
|
||||
echo "Nothing was built, imported, deployed or started."
|
||||
Reference in New Issue
Block a user