{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# COAE Practical 01 — Phishing Classifier Evasion\n",
        "**Track:** AI Evasion Foundations → Sparsity Attacks  \n",
        "**Level:** Medium | **Environment:** Mobile JupyterLite / laptop\n",
        "\n",
        "## Mission\n",
        "You are assessing a **synthetic phishing detection model** in an authorised offline lab. The input is truly malicious. Your task is to make the fixed classifier label it **benign** by modifying only permitted input features.\n",
        "\n",
        "The scorer is a small logistic decision function with illustrative fixed coefficients. It is **not** a production classifier or an actual HTB exam question. The point is to practise feature obfuscation, attack constraints, verification, and reporting.\n",
        "\n",
        "**Rules:** Modify at most **2 features**, only positions **1, 2, or 3**, and by at most **0.45 per feature**. Values must stay between **0 and 1**. No model weights, labels or thresholds may be changed."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "from math import exp\n",
        "FEATURE_NAMES = (\"url_entropy\", \"display_name_oddity\", \"urgent_language\",\n",
        "                 \"attachment_metadata\", \"domain_reputation\")\n",
        "WEIGHTS = (1.5, 1.8, 1.3, 1.6, 1.2)\n",
        "BIAS = -3.1\n",
        "THRESHOLD = 0.72\n",
        "ORIGINAL = [0.8, 0.7, 0.6, 0.7, 0.8]\n",
        "EDITABLE = (1, 2, 3)\n",
        "MAX_CHANGED = 2\n",
        "MAX_DELTA = 0.45\n",
        "\n",
        "def phishing_probability(features):\n",
        "    z = sum(w * x for w, x in zip(WEIGHTS, features)) + BIAS\n",
        "    return 1 / (1 + exp(-z))\n",
        "\n",
        "def label(features):\n",
        "    return \"MALICIOUS\" if phishing_probability(features) >= THRESHOLD else \"BENIGN\"\n",
        "\n",
        "print(\"Original probability:\", round(phishing_probability(ORIGINAL), 4))\n",
        "print(\"Original prediction:\", label(ORIGINAL))\n",
        "assert label(ORIGINAL) == \"MALICIOUS\""
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Task A — Construct the adversarial sample\n",
        "Fill in the attack function. You may use a manual choice or write a search algorithm. Do **not** modify the original example, the score function or the allowed feature constraints.\n",
        "\n",
        "Think about the relationship between **feature weights**, change magnitude and the decision threshold."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "def craft_attack(original):\n",
        "    candidate = original.copy()\n",
        "    # TODO: Change up to 2 allowed features (indices 1, 2, 3).\n",
        "    # Each value can change by at most 0.45.\n",
        "    # Example: candidate[index] = candidate[index] + change\n",
        "    return candidate\n",
        "\n",
        "candidate = craft_attack(ORIGINAL)\n",
        "print(\"Original:\", ORIGINAL)\n",
        "print(\"Candidate:\", candidate)\n",
        "print(\"Classifier decision:\", label(candidate))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Task B — Validate the attack\n",
        "Execute this checker after editing `craft_attack`. It gives a clear reason when an attack violates its constraints. It will **not pass** until you produce an effective candidate."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "def verify_attack(original, candidate):\n",
        "    assert isinstance(candidate, (list, tuple)), \"Return a list or tuple.\"\n",
        "    assert len(candidate) == len(original), \"Feature vector length changed.\"\n",
        "    assert all(isinstance(v, (int, float)) and 0 <= v <= 1 for v in candidate), \"Feature value out of [0, 1].\"\n",
        "    changed = [i for i, (a, b) in enumerate(zip(original, candidate)) if abs(a-b) > 1e-9]\n",
        "    assert 1 <= len(changed) <= MAX_CHANGED, \"Change at least 1 and no more than 2 features.\"\n",
        "    assert all(i in EDITABLE for i in changed), \"Changed a non-editable feature.\"\n",
        "    assert all(abs(original[i] - candidate[i]) <= MAX_DELTA + 1e-9 for i in changed), \"Exceeded the per-feature budget.\"\n",
        "    assert label(original) == \"MALICIOUS\", \"Baseline is not malicious.\"\n",
        "    assert label(candidate) == \"BENIGN\", \"Attack did not cross the decision threshold.\"\n",
        "    return changed\n",
        "\n",
        "changed = verify_attack(ORIGINAL, candidate)\n",
        "print(\"PASS — admissible classifier evasion\")\n",
        "print(\"Modified features:\", [FEATURE_NAMES[i] for i in changed])\n",
        "print(\"Original risk:\", round(phishing_probability(ORIGINAL), 4))\n",
        "print(\"Adversarial risk:\", round(phishing_probability(candidate), 4))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Task C — Analyst notes (mandatory)\n",
        "Write down:\n",
        "1. Why this is **inference-time evasion** and not training-data poisoning.\n",
        "2. Which features you controlled, and why those modifications have high influence.\n",
        "3. The precise change in classifier score.\n",
        "4. What a defender should evaluate (robustness tests, domain-valid transformations, retraining/monitoring).\n",
        "\n",
        "**Bonus:** Implement a `for`-loop or gradient-based function that finds an admissible bypass automatically, without hard-coding the final vector.\n",
        "\n",
        "**Deliverable:** Your completed `craft_attack` function, the checker output, and a five-line finding including impact and remediation."
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python (Pyodide)",
      "language": "python",
      "name": "python"
    },
    "language_info": {
      "name": "python"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}