#!/usr/bin/env bash
# Real demos for P001 "The 7 Levels of Cybersecurity". Runs on render01 (Docker).
#  1 passwords: what a hash is, breach counts (Have I Been Pwned, k-anonymity), guessing speed
#  2 MFA: a time-based code (TOTP) changes every 30 s; the old one is refused
#  3 phishing: a lookalike domain with one Cyrillic letter
#  4 updates: known vulnerabilities in an old vs a current image (Trivy), CISA's KEV count
#  5 encryption: the same login sniffed over HTTP and over HTTPS (tcpdump)
#  6 least privilege: a read-only database role; a firewall closing ports (nmap)
#  7 detection: SSH password guessing, seen in the log and banned by fail2ban
# Every command + its real output goes to demo_output.json; terminal scenes show only that.
# Every number in video.json must match a value here. Re-run = new numbers = update the script.
set -euo pipefail
cd "$(dirname "$0")"
P=ce-p001
NETSHOOT=nicolaka/netshoot:v0.15
TRIVY=aquasec/trivy:${TRIVY_TAG:-0.75.0}
cleanup() {
  docker rm -f py web sniff client db server scanner ssh-server attacker >/dev/null 2>&1 || true
  docker network rm $P >/dev/null 2>&1 || true
}
cleanup
trap cleanup EXIT
docker network create $P >/dev/null
W=/tmp/ce-p001
rm -rf $W && mkdir -p $W

python3 - "$P" "$NETSHOOT" "$TRIVY" "$W" <<'PY'
import json, subprocess, sys, time
P, NETSHOOT, TRIVY, W = sys.argv[1:5]
steps = {}

def sh(cmd, check=False):
    r = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    if check and r.returncode:
        raise SystemExit(f"FAILED: {cmd}\n{r.stdout}\n{r.stderr}")
    return (r.stdout + r.stderr).rstrip()

def step(key, show, real=None, out=None):
    """Run `real` (default: `show`) and record the command as the viewer sees it."""
    steps[key] = {"cmd": show, "out": out if out is not None else sh(real or show)}
    print(f"== {key}\n$ {show}\n{steps[key]['out']}\n", flush=True)

def wait(cmd, tries=90):
    for _ in range(tries):
        if subprocess.run(cmd, shell=True, capture_output=True).returncode == 0:
            return
        time.sleep(1)
    raise SystemExit(f"timed out waiting for: {cmd}")

def put(name, text):
    open(f"{W}/{name}", "w").write(text)

# Python helper container for levels 1-3 (scripts are copied in, run with `python3 <name>`)
sh(f"docker run -d --name py --network {P} python:3.12-slim sleep infinity", check=True)
sh("docker exec py pip install -q 'bcrypt==4.*' 'pyotp==2.9.*'", check=True)
def py(name, src, args=""):
    put(name, src)
    sh(f"docker cp {W}/{name} py:/{name}", check=True)
    return f"docker exec -w / py python3 /{name} {args}".rstrip()

# ---------------------------------------------------------------- 1. passwords
step("hash", "echo -n sunshine | sha256sum")
step("hash2", "echo -n sunshinE | sha256sum")

sh("curl -sf -o /tmp/ce-p001/eff_large_wordlist.txt https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt", check=True)
sh(f"docker cp {W}/eff_large_wordlist.txt py:/eff_large_wordlist.txt", check=True)
passphrase = py("passphrase.py", r'''
import secrets
words = [line.split()[1] for line in open("/eff_large_wordlist.txt")]
print(f"{len(words)} words in the list")
print("-".join(secrets.choice(words) for _ in range(5)))
''')
step("passphrase", "python3 passphrase.py", passphrase)
phrase = steps["passphrase"]["out"].splitlines()[-1]

pwned = py("pwned.py", r'''
import hashlib, sys, urllib.request
print("times seen in breaches  password")
for pw in sys.argv[1:]:
    h = hashlib.sha1(pw.encode()).hexdigest().upper()
    prefix, suffix = h[:5], h[5:]                      # only the first 5 characters are sent
    req = urllib.request.Request(f"https://api.pwnedpasswords.com/range/{prefix}",
                                 headers={"User-Agent": "cloud-explainers-demo"})
    body = urllib.request.urlopen(req, timeout=20).read().decode()
    n = next((int(c) for s, c in (l.split(":") for l in body.splitlines()) if s == suffix), 0)
    print(f"{n:>10,}  {pw}")
''', f"password123 sunshine 'Summer2024!' {phrase}")
step("pwned", f"python3 pwned.py password123 sunshine 'Summer2024!' {phrase}", pwned)

speed = py("guess_speed.py", r'''
import hashlib, time, bcrypt
def rate(fn, seconds):
    n, t0 = 0, time.perf_counter()
    while time.perf_counter() - t0 < seconds:
        fn(f"guess{n}".encode()); n += 1
    return n / (time.perf_counter() - t0)
fast = rate(lambda g: hashlib.sha256(g).digest(), 3)
salt = bcrypt.gensalt(rounds=12)
slow = rate(lambda g: bcrypt.hashpw(g, salt), 6)
print(f"SHA-256:          {fast:>12,.0f} guesses per second")
print(f"bcrypt (cost 12): {slow:>12,.1f} guesses per second")
def human(s):
    for unit, size in (("years", 31_557_600), ("days", 86_400), ("hours", 3_600), ("minutes", 60)):
        if s >= size:
            v = s / size
            if unit == "years" and v >= 1e6:
                return f"{v / 1e6:,.0f} million years"
            return f"{v:,.0f} {unit}"
    return f"{s:,.1f} seconds"
print("time to try every password, at the SHA-256 speed:")
for label, n in (("8 lowercase letters", 26**8), ("8 characters, any type", 95**8),
                 ("15 lowercase letters", 26**15), ("5 random words", 7776**5)):
    print(f"  {label:<24} {human(n / fast)}")
''')
step("speed", "python3 guess_speed.py", speed)

# ---------------------------------------------------------------- 2. MFA (TOTP)
mfa = py("mfa.py", r'''
import time, pyotp
totp = pyotp.TOTP(pyotp.random_base32())          # the secret your phone and the server share
time.sleep(31 - time.time() % 30)                 # start just after a new 30-second window
code = totp.now()
print(f"{time.strftime('%H:%M:%S')}  phone shows {code}")
print(f"{time.strftime('%H:%M:%S')}  server checks {code}: {'accepted' if totp.verify(code) else 'refused'}")
time.sleep(30)
print(f"{time.strftime('%H:%M:%S')}  phone shows {totp.now()}")
print(f"{time.strftime('%H:%M:%S')}  server checks {code}: {'accepted' if totp.verify(code) else 'refused'}")
''')
step("mfa", "python3 mfa.py", mfa)

# ---------------------------------------------------------------- 3. phishing (lookalike domain)
look = py("lookalike.py", r'''
import unicodedata
real = "apple.com"
fake = "аpple.com"                            # first letter: Cyrillic, not Latin
print(f"real: {real}")
print(f"fake: {fake}")
print(f"same? {real == fake}")
print(f"first letter of fake: {unicodedata.name(fake[0])}")
print(f"address really visited: {fake.encode('idna').decode()}")
''')
step("lookalike", "python3 lookalike.py", look)

# ---------------------------------------------------------------- 4. updates (known vulnerabilities)
trivy = f"docker run --rm -v {W}/trivy-cache:/root/.cache {TRIVY}"
sh(f"{trivy} image --download-db-only -q", check=True)
for image in ("python:3.9.0-slim", "python:3.13-slim"):
    sh(f"docker pull -q {image}", check=True)
for key, image, flags in (("vuln_old", "python:3.9.0-slim", ""), ("vuln_new", "python:3.13-slim", ""),
                          ("fix_old", "python:3.9.0-slim", "--ignore-unfixed "), ("fix_new", "python:3.13-slim", "--ignore-unfixed ")):
    step(key, f"trivy image -q {flags}--severity HIGH,CRITICAL {image} | grep -B2 Total",
         f"docker run --rm -v {W}/trivy-cache:/root/.cache -v /var/run/docker.sock:/var/run/docker.sock {TRIVY} "
         f"image -q --skip-db-update {flags}--severity HIGH,CRITICAL {image} | grep -B2 Total")
kev = "curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json | jq .count"
step("kev", kev, f"docker run --rm {NETSHOOT} sh -c '{kev}'")

# ---------------------------------------------------------------- 5. encryption (HTTP vs HTTPS)
sh(f"""cd {W} && openssl req -x509 -newkey rsa:2048 -nodes -keyout ca.key -out ca.pem -days 2 -subj '/CN=Demo CA' 2>/dev/null &&
openssl req -newkey rsa:2048 -nodes -keyout web.key -out web.csr -subj '/CN=web' 2>/dev/null &&
printf 'subjectAltName=DNS:web' > ext.cnf &&
openssl x509 -req -in web.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out web.pem -days 2 -extfile ext.cnf 2>/dev/null""", check=True)
put("server.py", r'''
import http.server, ssl, threading
class Login(http.server.BaseHTTPRequestHandler):
    def do_POST(self):
        self.rfile.read(int(self.headers["Content-Length"]))
        self.send_response(200); self.end_headers(); self.wfile.write(b"welcome\n")
    def log_message(self, *a): pass
plain = http.server.HTTPServer(("", 80), Login)
tls = http.server.HTTPServer(("", 443), Login)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER); ctx.load_cert_chain("/certs/web.pem", "/certs/web.key")
tls.socket = ctx.wrap_socket(tls.socket, server_side=True)
threading.Thread(target=plain.serve_forever, daemon=True).start()
tls.serve_forever()
''')
sh(f"docker run -d --name web --hostname web --network {P} -v {W}:/certs:ro python:3.12-slim python3 /certs/server.py", check=True)
sh(f"docker run -d --name client --network {P} -v {W}:/certs:ro {NETSHOOT} sleep infinity", check=True)
wait("docker exec client curl -sf -X POST -d x=1 http://web/login")
login = "curl -s -d 'user=ana&password=hunter2' {url}"
for proto, port, url, extra in (("http", 80, "http://web/login", ""), ("https", 443, "https://web/login", "--cacert /certs/ca.pem ")):
    sh(f"docker rm -f sniff >/dev/null 2>&1; docker run -d --name sniff --network container:web -v {W}:/cap {NETSHOOT} "
       f"tcpdump -i eth0 -s0 -U -w /cap/{proto}.pcap port {port}", check=True)
    time.sleep(3)
    step(f"login_{proto}", login.format(url=extra + url), f"docker exec client {login.format(url=extra + url)}")
    time.sleep(2)
    sh("docker stop sniff >/dev/null")
for proto, grep in (("http", "grep -a password"), ("https", "grep -a -c password")):
    show = f"tcpdump -A -r {proto}.pcap 2>/dev/null | {grep}"
    step(f"sniff_{proto}", show, f"docker run --rm -w /cap -v {W}:/cap {NETSHOOT} sh -c '{show}; true'")
tls = "curl -sv --cacert /certs/ca.pem -d x=1 https://web/login 2>&1 | grep 'SSL connection'"
step("tls_version", tls, f"docker exec client sh -c \"{tls}\"")

# ---------------------------------------------------------------- 6. least privilege
sh(f"docker run -d --name db --network {P} -e POSTGRES_PASSWORD=demo postgres:18", check=True)
wait("docker exec db pg_isready -U postgres -q")
time.sleep(2)
psql = "docker exec -i db psql -U postgres -X -q -P pager=off"
sh(f"""{psql} <<'SQL'
CREATE TABLE customers (id int PRIMARY KEY, name text, email text);
INSERT INTO customers SELECT g, 'Customer ' || g, 'c' || g || '@example.com' FROM generate_series(1, 5000) g;
CREATE ROLE reporting LOGIN PASSWORD 'demo';
SQL""", check=True)
rep = "docker exec -i db psql -U reporting -d postgres -X -P pager=off"
step("grant", "GRANT SELECT ON customers TO reporting;", "docker exec -i db psql -U postgres -X -c 'GRANT SELECT ON customers TO reporting;'")
step("rep_select", "reporting=> SELECT count(*) FROM customers;", f"{rep} -c 'SELECT count(*) FROM customers;'")
step("rep_delete", "reporting=> DELETE FROM customers;", f"{rep} -c 'DELETE FROM customers;'")

sh(f"docker run -d --name server --hostname server --network {P} --cap-add NET_ADMIN {NETSHOOT} sh -c "
   "'socat TCP-LISTEN:80,fork,reuseaddr SYSTEM:\"echo ok\" & socat TCP-LISTEN:5432,fork,reuseaddr SYSTEM:\"echo db\" & "
   "socat TCP-LISTEN:6379,fork,reuseaddr SYSTEM:\"echo cache\" & sleep infinity'", check=True)
sh(f"docker run -d --name scanner --network {P} {NETSHOOT} sleep infinity", check=True)
time.sleep(2)
scan = "nmap -Pn -p 80,5432,6379 server"
nmap = lambda: sh(f"docker exec scanner {scan} | grep -E '^[0-9]+/tcp|PORT'")
step("scan_before", scan, out=nmap())
fw = "iptables -A INPUT -p tcp --dport 80 -j ACCEPT && iptables -A INPUT -p tcp -m multiport --dports 5432,6379 -j DROP && iptables -S INPUT"
step("firewall", fw, f"docker exec server sh -c '{fw}'")
step("scan_after", scan, out=nmap())

# ---------------------------------------------------------------- 7. detection (SSH guessing + fail2ban)
import os; os.makedirs(f"{W}/ssh", exist_ok=True)
put("ssh/Dockerfile", r'''
FROM debian:12-slim
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openssh-server fail2ban iptables rsyslog >/dev/null \
 && mkdir -p /run/sshd && useradd -m ana
RUN printf '[sshd]\nenabled = true\nbackend = polling\nlogpath = /var/log/auth.log\nmaxretry = 5\nfindtime = 10m\nbantime = 10m\n' > /etc/fail2ban/jail.local
CMD rsyslogd && touch /var/log/auth.log && /usr/sbin/sshd && fail2ban-server -b && sleep infinity
''')
sh(f"docker build -q -t ce-p001-ssh {W}/ssh", check=True)
sh(f"docker run -d --name ssh-server --hostname ssh-server --network {P} --cap-add NET_ADMIN ce-p001-ssh", check=True)
sh(f"docker exec ssh-server sh -c 'echo ana:$(head -c 18 /dev/urandom | base64) | chpasswd'", check=True)
sh(f"docker run -d --name attacker --hostname attacker --network {P} debian:12-slim sleep infinity", check=True)
sh("docker exec attacker sh -c 'apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq sshpass openssh-client >/dev/null'", check=True)
wait("docker exec ssh-server fail2ban-client ping")
time.sleep(3)
guess = ("for pw in 123456 password qwerty letmein admin iloveyou welcome monkey; do "
         "sshpass -p $pw ssh -o StrictHostKeyChecking=no -o ConnectTimeout=3 ana@ssh-server true 2>&1 | tail -1 | sed \"s/^/$pw: /\"; "
         "sleep 1; done")
step("guess", guess, f"docker exec attacker bash -c '{guess}'")
step("authlog", "grep 'Failed password' /var/log/auth.log | tail -3",
     "docker exec ssh-server sh -c \"grep 'Failed password' /var/log/auth.log | tail -3\"")
step("authlog_count", "grep -c 'Failed password' /var/log/auth.log",
     "docker exec ssh-server grep -c 'Failed password' /var/log/auth.log")
step("banned", "fail2ban-client status sshd", "docker exec ssh-server fail2ban-client status sshd")
steps["attacker_ip"] = {"cmd": "attacker ip", "out": sh("docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' attacker")}

steps["version"] = {"cmd": "versions", "out": sh(
    f"echo postgres $(docker exec db postgres --version | awk '{{print $3}}'); "
    f"echo trivy $(docker run --rm {TRIVY} --version | head -1 | awk '{{print $2}}'); "
    f"echo openssh $(docker exec ssh-server dpkg-query -W -f '${{Version}}' openssh-server); "
    f"echo fail2ban $(docker exec ssh-server fail2ban-client --version | head -1); date -u +%Y-%m-%dT%H:%MZ")}
json.dump(steps, open("demo_output.json", "w"), indent=1)
print("saved demo_output.json")
PY
