{
 "nbformat": 4,
 "nbformat_minor": 5,
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.x"
  },
  "colab": {
   "provenance": []
  }
 },
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# One Pixel Too Far - CTF Challenge\n",
    "### CIS 350: AI for Cybersecurity  |  Week 10: Adversarial Examples\n",
    "\n",
    "**Category:** Adversarial ML  |  **Points:** 20  |  **Difficulty:** Hard\n",
    "\n",
    "## Scenario\n",
    "A small business deployed a toy digit classifier to auto-read scanned form fields. You are the security analyst. The vendor claims the model is reliable because it is very confident. Your job is to show that a change too small for a human to notice can still flip the model's answer - the core idea behind **adversarial examples**.\n",
    "\n",
    "## Objective\n",
    "Starting from a clean image `x` that the model correctly reads as a **3**, craft an **adversarial image `adv`** that the model reads as something else, while keeping the change tiny.\n",
    "\n",
    "## Success condition (stated plainly)\n",
    "`check(adv)` reveals the flag only when BOTH are true:\n",
    "1. **Misclassified:** `predict(adv) != true_label` (the model no longer reads 3), and\n",
    "2. **Small change:** `max(abs(adv - x)) <= EPS_CAP` (L-infinity within the budget of 4.0 on the 0-16 pixel scale).\n",
    "\n",
    "## Ethics and sandbox\n",
    "Authorized, sandboxed educational exercise only: this is a synthetic toy model inside this notebook - never attack a real system, network, or service.\n",
    "\n",
    "Run cells top to bottom with **Shift + Enter**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 1 - Build the sandbox model\n",
    "This cell trains the toy classifier, picks the clean image `x`, exposes the weight matrix `coef`, and provides `predict()`, `confidence()`, and the grader `check()`. Do not edit it - just run it."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings(\"ignore\")   # keep output clean\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from sklearn.datasets import load_digits\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "\n",
    "# --- Build the toy classifier (fully deterministic) ---\n",
    "digits = load_digits()                 # 8x8 images of digits 0-9, pixels 0-16\n",
    "X, y = digits.data, digits.target\n",
    "X_train, X_test, y_train, y_test = train_test_split(\n",
    "        X, y, test_size=0.3, random_state=0, stratify=y)\n",
    "clf = LogisticRegression(max_iter=5000, random_state=0).fit(X_train, y_train)\n",
    "\n",
    "coef = clf.coef_                       # 10 x 64 weight matrix (one row per digit)\n",
    "true_label, target = 3, 8              # attack a clean \"3\"; try to make it read \"8\"\n",
    "EPS_CAP = 4.0                          # perturbation budget: max |adv - x| (0-16 scale)\n",
    "\n",
    "# The clean image we attack: first correctly-classified test \"3\".\n",
    "idx = [i for i in range(len(X_test))\n",
    "       if y_test[i] == true_label and clf.predict([X_test[i]])[0] == true_label][0]\n",
    "x = X_test[idx].copy().astype(float)\n",
    "\n",
    "def predict(img):\n",
    "    \"Model's predicted digit for one 8x8 image (length-64 vector).\"\n",
    "    return int(clf.predict([np.asarray(img, dtype=float)])[0])\n",
    "\n",
    "def confidence(img):\n",
    "    \"Model's top-class probability for one image (0-1).\"\n",
    "    return float(clf.predict_proba([np.asarray(img, dtype=float)])[0].max())\n",
    "\n",
    "print(\"clean image index:\", idx)\n",
    "print(\"model prediction :\", predict(x), \" confidence:\", round(confidence(x), 3))\n",
    "print(\"true label       :\", true_label, \" target class:\", target)\n",
    "\n",
    "def check(adv):\n",
    "    \"Reveal the flag only if adv is a SMALL, MISCLASSIFYING perturbation of x.\"\n",
    "    adv = np.asarray(adv, dtype=float)\n",
    "    if adv.shape != x.shape:\n",
    "        print(\"HINT: adv must be a length-64 image vector, same shape as x.\")\n",
    "        return\n",
    "    if np.min(adv) < 0 or np.max(adv) > 16:\n",
    "        print(\"HINT: pixels must stay in the valid 0-16 range (use np.clip).\")\n",
    "        return\n",
    "    linf = float(np.abs(adv - x).max())\n",
    "    flipped = predict(adv) != true_label\n",
    "    small = linf <= EPS_CAP\n",
    "    print(\"prediction on adv:\", predict(adv),\n",
    "          \"| L-inf change:\", round(linf, 3), \"| budget:\", EPS_CAP)\n",
    "    if flipped and small:\n",
    "        print(\"SUCCESS! Flag: CIS350{tiny_perturbation_big_lie}\")\n",
    "    elif not flipped:\n",
    "        print(\"HINT: the model still reads\", true_label,\n",
    "              \"- push a little harder toward the target class.\")\n",
    "    else:\n",
    "        print(\"HINT: label flipped but the change is too big (L-inf\",\n",
    "              round(linf, 3), \"> budget\", EPS_CAP, \") - shrink eps.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 2 - Look at the target\n",
    "Confirm the model reads `x` as a 3, and note how confident it is."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "plt.imshow(x.reshape(8, 8), cmap=\"gray_r\")\n",
    "plt.title(\"clean: predicted \" + str(predict(x)))\n",
    "plt.axis(\"off\"); plt.show()\n",
    "print(\"confidence on clean image:\", round(confidence(x), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 3 - Craft your attack (TODO)\n",
    "The model is **linear**, so its full weight matrix is in `coef` (shape 10 x 64). That is a gift to an attacker: you can compute the most damaging direction directly, with no gradient library."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# TODO: craft an adversarial image `adv` from the clean image `x`.\n",
    "#\n",
    "# Facts you may use:\n",
    "#   * The model is LINEAR; its weights are in `coef` (shape 10 x 64).\n",
    "#   * To push the image AWAY from the true class and TOWARD the target\n",
    "#     class, move along the weight difference:\n",
    "#         coef[target] - coef[true_label]\n",
    "#   * A Fast Gradient SIGN step changes every pixel by the same amount\n",
    "#     `eps` in the +/- direction of that difference (hint: np.sign(...)).\n",
    "#   * Keep pixels valid with np.clip(..., 0, 16).\n",
    "#   * Stay within budget: max |adv - x| must be <= EPS_CAP.\n",
    "#\n",
    "# Fill in eps and the formula for adv, then run the cell.\n",
    "\n",
    "eps = ____          # something around 3.5 is enough\n",
    "adv = ____          # np.clip(x + eps * np.sign(...), 0, 16)\n",
    "\n",
    "check(adv)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 4 - Compare the images (optional)\n",
    "If you succeeded, plot both. They should look almost identical to your eye - that is the whole point of an adversarial example."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots(1, 2, figsize=(6, 3))\n",
    "ax[0].imshow(x.reshape(8, 8), cmap=\"gray_r\");   ax[0].set_title(\"orig: \" + str(predict(x)))\n",
    "ax[1].imshow(adv.reshape(8, 8), cmap=\"gray_r\"); ax[1].set_title(\"adv: \" + str(predict(adv)))\n",
    "for a in ax: a.axis(\"off\")\n",
    "plt.show()\n",
    "print(\"clean confidence:\", round(confidence(x), 3),\n",
    "      \"| adversarial confidence:\", round(confidence(adv), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## What to submit\n",
    "- This `.ipynb` run top to bottom (Runtime -> Run all) with `check(adv)` printing **SUCCESS! Flag: ...**.\n",
    "- Paste the flag string into the CTFd challenge box.\n",
    "\n",
    "**Troubleshooting:** `NameError` -> run Step 1 first. Label will not flip -> raise `eps` toward 3.5-4. Change too big -> lower `eps`."
   ]
  }
 ]
}