File size: 10,313 Bytes
49d1c75
 
58f7026
49d1c75
58f7026
 
 
 
7fedc25
58f7026
7fedc25
 
49d1c75
 
8b07a89
49d1c75
58f7026
49d1c75
 
 
 
 
 
 
 
 
 
 
 
 
 
1566173
 
 
 
49d1c75
7fedc25
49d1c75
 
58f7026
 
 
 
 
 
7fedc25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58f7026
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8691b5f
 
 
 
58f7026
8691b5f
 
49d1c75
8691b5f
 
49d1c75
58f7026
8691b5f
58f7026
 
8691b5f
 
 
58f7026
 
49d1c75
58f7026
 
 
 
8b07a89
58f7026
 
 
8b07a89
 
 
58f7026
 
49d1c75
58f7026
 
 
 
49d1c75
 
 
58f7026
49d1c75
 
 
58f7026
 
49d1c75
58f7026
 
 
49d1c75
58f7026
 
49d1c75
58f7026
 
 
 
 
49d1c75
 
58f7026
49d1c75
 
 
58f7026
 
49d1c75
58f7026
 
49d1c75
58f7026
 
49d1c75
58f7026
 
 
49d1c75
 
58f7026
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49d1c75
58f7026
 
 
 
49d1c75
 
58f7026
49d1c75
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
#!/usr/bin/env bash
# =============================================================================
# OpenRange β€” Snapshot-Driven Service Startup
# =============================================================================
# Called by RangeEnvironment.reset() to start services defined in a snapshot.
# NOT called at container boot β€” the Dockerfile starts only uvicorn.
#
# Usage:  start.sh <snapshot_dir>
#   snapshot_dir must contain a spec.json.
#
# If spec.json contains a "services" list (ServiceSpec entries), those are
# started generically.  Otherwise falls back to legacy host-name mapping.
# =============================================================================

set -uo pipefail

SNAPSHOT_DIR="${1:?Usage: start.sh <snapshot_dir>}"
LOGDIR="/var/log/siem"
CONSOLIDATED="${LOGDIR}/consolidated"

# Track background PIDs for cleanup
PIDS=()

cleanup() {
    echo "[start.sh] Shutting down services..."
    for pid in "${PIDS[@]}"; do
        kill "$pid" 2>/dev/null || true
    done
    wait 2>/dev/null || true
    echo "[start.sh] All services stopped."
}
# Only trap INT/TERM (not EXIT) -- services should survive script exit
# when called from RangeEnvironment.reset(). The environment manages
# service lifecycle via _stop_services() / _start_snapshot_services().
trap cleanup INT TERM

# ── Parse snapshot ────────────────────────────────────────────────────────────

mkdir -p "${CONSOLIDATED}"

if [ ! -f "${SNAPSHOT_DIR}/spec.json" ]; then
    echo "[start.sh] ERROR: No spec.json found in ${SNAPSHOT_DIR}"
    exit 1
fi

# ── Check for declarative services list ───────────────────────────────────────
# If spec.json contains "services" entries (ServiceSpec), start them generically
# via Python. This is the modern path populated by the Renderer.

HAS_SERVICES=$(python3 -c "
import json
with open('${SNAPSHOT_DIR}/spec.json') as f:
    spec = json.load(f)
svcs = spec.get('services', [])
print(len(svcs))
" 2>/dev/null || echo "0")

if [ "$HAS_SERVICES" -gt 0 ] 2>/dev/null; then
    echo "[start.sh] Found $HAS_SERVICES declared service(s) β€” using spec-driven startup"

    python3 -c "
import json, subprocess, sys, time, os, socket

with open('${SNAPSHOT_DIR}/spec.json') as f:
    spec = json.load(f)

pids = []
for svc in spec.get('services', []):
    daemon = svc.get('daemon', '')
    host = svc.get('host', '')
    print(f'[start.sh] Starting service: {daemon} (host={host})')

    env = os.environ.copy()
    env.update(svc.get('env_vars', {}))

    log_dir = svc.get('log_dir', '')
    if log_dir:
        os.makedirs(log_dir, exist_ok=True)

    # Init commands
    for cmd in svc.get('init_commands', []):
        try:
            subprocess.run(['bash', '-c', cmd], capture_output=True, timeout=30, env=env)
        except Exception as e:
            print(f'[start.sh]   init warning: {e}', file=sys.stderr)

    # Start command
    start_cmd = svc.get('start_command', '')
    if start_cmd:
        try:
            subprocess.run(['bash', '-c', start_cmd], capture_output=True, timeout=30, env=env)
        except Exception as e:
            print(f'[start.sh]   start warning: {e}', file=sys.stderr)

    # Readiness
    readiness = svc.get('readiness', {})
    rtype = readiness.get('type', 'tcp')
    timeout_s = readiness.get('timeout_s', 30)
    interval_s = readiness.get('interval_s', 1.0)
    port = readiness.get('port', 0)
    url = readiness.get('url', '')
    command = readiness.get('command', '')

    if (rtype == 'tcp' and port == 0 and not url and not command):
        print(f'[start.sh]   {daemon}: started (no readiness check)')
        continue

    max_attempts = int(timeout_s / max(interval_s, 0.1))
    ready = False
    for attempt in range(max_attempts):
        try:
            if rtype == 'tcp' and port > 0:
                s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                s.settimeout(2)
                s.connect(('127.0.0.1', port))
                s.close()
                ready = True
            elif rtype == 'http' and url:
                r = subprocess.run(['curl', '-sf', url], capture_output=True, timeout=3)
                ready = (r.returncode == 0)
            elif rtype == 'command' and command:
                r = subprocess.run(['bash', '-c', command], capture_output=True, timeout=5)
                ready = (r.returncode == 0)
        except Exception:
            pass
        if ready:
            print(f'[start.sh]   {daemon}: ready ({attempt + 1}s)')
            break
        time.sleep(interval_s)
    else:
        if not ready:
            print(f'[start.sh]   {daemon}: readiness timeout after {timeout_s}s')
"

    echo "============================================================"
    echo "[start.sh] Spec-driven services started."
    echo "[start.sh] Logs at: ${LOGDIR}/"
    echo "============================================================"
    exit 0
fi

# ── Legacy fallback: host-name-based service mapping ──────────────────────────
# Used when spec.json has no "services" list (old snapshots).

echo "[start.sh] No declared services β€” falling back to legacy host mapping"

# Extract host list from topology
HOSTS=$(python3 -c "
import json, sys
with open('${SNAPSHOT_DIR}/spec.json') as f:
    spec = json.load(f)
hosts = spec.get('topology', {}).get('hosts', [])
print(' '.join(hosts))
" 2>/dev/null || echo "")

echo "[start.sh] Snapshot hosts: ${HOSTS:-none}"

# ── Service starters (called only if snapshot needs them) ─────────────────────

start_mysql() {
    local MYSQLD=$(command -v mariadbd || command -v mysqld || echo "")
    if [ -z "$MYSQLD" ]; then echo "[start.sh]   mysql: not installed"; return; fi

    mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld 2>/dev/null || true
    mkdir -p /var/log/mysql && chown mysql:mysql /var/log/mysql 2>/dev/null || true

    if [ ! -d /var/lib/mysql/mysql ]; then
        if command -v mariadb-install-db >/dev/null 2>&1; then
            mariadb-install-db --user=mysql 2>&1 | tee "${LOGDIR}/mysql.log"
        else
            $MYSQLD --initialize-insecure --user=mysql 2>&1 | tee "${LOGDIR}/mysql.log"
        fi
    fi

    $MYSQLD --user=mysql --log-error="${LOGDIR}/mysql.log" &
    PIDS+=($!)

    local ADMIN=$(command -v mariadb-admin || command -v mysqladmin || echo "")
    for i in $(seq 1 30); do
        if [ -n "$ADMIN" ] && $ADMIN ping --silent 2>/dev/null; then
            echo "[start.sh]   mysql: ready (${i}s)"; return
        fi
        sleep 1
    done
    echo "[start.sh]   mysql: timeout"
}

start_nginx() {
    if ! command -v nginx >/dev/null 2>&1; then echo "[start.sh]   nginx: not installed"; return; fi
    mkdir -p /var/log/nginx
    nginx -g "daemon off;" > "${LOGDIR}/nginx.log" 2>&1 &
    PIDS+=($!)
    for i in $(seq 1 10); do
        if curl -sf http://localhost:80/ >/dev/null 2>&1; then
            echo "[start.sh]   nginx: ready (${i}s)"; return
        fi
        sleep 1
    done
    echo "[start.sh]   nginx: timeout"
}

start_slapd() {
    if ! command -v slapd >/dev/null 2>&1; then echo "[start.sh]   slapd: not installed"; return; fi
    mkdir -p /var/run/slapd
    slapd -h "ldap:/// ldapi:///" -u openldap -g openldap > "${LOGDIR}/slapd.log" 2>&1 &
    PIDS+=($!)
    for i in $(seq 1 10); do
        if ldapsearch -x -H ldap://localhost -b "" -s base namingContexts >/dev/null 2>&1; then
            echo "[start.sh]   slapd: ready (${i}s)"; return
        fi
        sleep 1
    done
    echo "[start.sh]   slapd: timeout"
}

start_rsyslog() {
    if ! command -v rsyslogd >/dev/null 2>&1; then echo "[start.sh]   rsyslog: not installed"; return; fi
    rsyslogd -n > "${LOGDIR}/rsyslog.log" 2>&1 &
    PIDS+=($!)
    echo "[start.sh]   rsyslog: started"
}

start_samba() {
    if ! command -v smbd >/dev/null 2>&1; then echo "[start.sh]   samba: not installed"; return; fi
    mkdir -p /var/lib/samba/private
    smbd --foreground --no-process-group > "${LOGDIR}/smbd.log" 2>&1 &
    PIDS+=($!)
    for i in $(seq 1 10); do
        if smbclient -L localhost -N >/dev/null 2>&1; then
            echo "[start.sh]   samba: ready (${i}s)"; return
        fi
        sleep 1
    done
    echo "[start.sh]   samba: timeout"
}

start_postfix() {
    if ! command -v postfix >/dev/null 2>&1; then echo "[start.sh]   postfix: not installed"; return; fi
    postfix start > "${LOGDIR}/postfix.log" 2>&1 || true
    echo "[start.sh]   postfix: started"
}

start_sshd() {
    if ! command -v sshd >/dev/null 2>&1; then echo "[start.sh]   sshd: not installed"; return; fi
    mkdir -p /var/run/sshd
    /usr/sbin/sshd -E "${LOGDIR}/sshd.log" &
    PIDS+=($!)
    echo "[start.sh]   sshd: started"
}

# ── Map host names to services ────────────────────────────────────────────────
# The manifest topology uses logical host names. Map them to service starters.

declare -A HOST_SERVICE_MAP=(
    [web]=start_nginx
    [db]=start_mysql
    [ldap]=start_slapd
    [siem]=start_rsyslog
    [files]=start_samba
    [mail]=start_postfix
    [firewall]=start_rsyslog  # firewall host uses rsyslog for logging
)

# SSH is started if any host needs remote access
SSH_NEEDED=false

for host in $HOSTS; do
    starter="${HOST_SERVICE_MAP[$host]:-}"
    if [ -n "$starter" ]; then
        echo "[start.sh] Starting service for host: $host"
        $starter
    else
        echo "[start.sh] Host '$host' has no mapped service (may be agent-only)"
    fi
    # Any host beyond attacker/siem might need SSH
    if [ "$host" != "attacker" ] && [ "$host" != "siem" ]; then
        SSH_NEEDED=true
    fi
done

if $SSH_NEEDED; then
    echo "[start.sh] Starting SSH (needed for host access)"
    start_sshd
fi

echo "============================================================"
echo "[start.sh] Services started for snapshot. PIDs: ${PIDS[*]:-none}"
echo "[start.sh] Logs at: ${LOGDIR}/"
echo "============================================================"