{
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python",
      "version": "3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5,
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# Citadelle Bank \u2014 Fraud Analysis\n",
        "\n",
        "This notebook was recovered from the compromised data-science workstation.\n",
        "The security team believes the attacker left a trace inside one of the flagged transactions.\n",
        "\n",
        "**Your objective:** identify the suspicious transaction and decode the message hidden in its note field.\n",
        "\n",
        "| Column | Description |\n",
        "|--------|-------------|\n",
        "| `failed_login_count` | Login failures on the account in the 24 h before the transaction |\n",
        "| `device_trust_score` | 0 \u2013 unknown device \u00a0\u00a0 100 \u2013 trusted managed device |\n",
        "| `ip_risk_score` | 0 \u2013 clean IP \u00a0\u00a0 100 \u2013 known-malicious range |\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "import base64\n",
        "import pandas as pd\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "columns = [\"transaction_id\", \"account_id\", \"timestamp\", \"amount\", \"country\", \"failed_login_count\", \"device_trust_score\", \"transaction_note\", \"ip_risk_score\"]\n",
        "records = [[\"TX-1041\", \"AC-220\", \"2026-07-27 08:41\", 8200, \"GB\", 1, 82, \"Supplier settlement\", 18], [\"TX-1042\", \"AC-118\", \"2026-07-27 08:57\", 14950, \"AE\", 5, 22, \"New device \\u2014 client confirmed travel\", 76], [\"TX-1043\", \"AC-731\", \"2026-07-27 09:11\", 42000, \"RO\", 9, 8, \"ADMIN-RECOVERY:SSBhbSBhIGZyYXVkdWxlbnQgbWVzc2FnZSBkZXNpZ25lZCB0byB0cmljayBwZW9wbGUgaW50byByZXZlYWxpbmcgcGFzc3dvcmRzIG9yIHNlbnNpdGl2ZSBpbmZvcm1hdGlvbi4KCkNvbGxlY3QgdGhlIGFuc3dlcnMgZnJvbSBhbGwgZml2ZSBjaGFyYWRlcywgYXJyYW5nZSB0aGVtIGluIHRoZSBvcmRlciBpbiB3aGljaCB5b3UgZGlzY292ZXJlZCB0aGVtLCByZW1vdmUgc3BhY2VzIGFuZCBwdW5jdHVhdGlvbiwgYW5kIGVudGVyIHRoZSByZXN1bHQgYXMgdGhlIGFkbWluaXN0cmF0b3IgcGFzc3dvcmQu\", 97], [\"TX-1044\", \"AC-304\", \"2026-07-27 09:18\", 127, \"GB\", 0, 91, \"Card purchase\", 4], [\"TX-1045\", \"AC-515\", \"2026-07-27 09:23\", 3750, \"FR\", 2, 74, \"Investment transfer\", 19], [\"TX-1046\", \"AC-089\", \"2026-07-27 09:31\", 9100, \"US\", 0, 95, \"Wire \\u2014 confirmed by client\", 6], [\"TX-1047\", \"AC-622\", \"2026-07-27 09:44\", 6600, \"NG\", 7, 31, \"Account top-up\", 84], [\"TX-1048\", \"AC-411\", \"2026-07-27 09:55\", 520, \"GB\", 0, 88, \"Standing order\", 11]]\n",
        "df = pd.DataFrame(records, columns=columns)\n",
        "df\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# Quick overview: check how many rows and what types we have.\n",
        "print(f\"Shape: {df.shape[0]} transactions, {df.shape[1]} columns\\n\")\n",
        "print(df[[\"transaction_id\", \"country\", \"amount\",\n",
        "          \"failed_login_count\", \"device_trust_score\", \"ip_risk_score\"]].to_string(index=False))\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Step 1 \u2014 Filter by risk country\n",
        "\n",
        "The risk team has flagged a watchlist of elevated-risk jurisdictions.\n",
        "Start by narrowing the dataset to transactions from these countries only.\n",
        "\n",
        "> **Note:** A high country-risk score alone is not proof of malicious activity.\n",
        "> One of the candidates has a legitimate explanation in its note field.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# Countries flagged by the risk team as elevated-risk jurisdictions.\n",
        "# Fill in the variable name inside isin(___) to apply the watchlist filter.\n",
        "watchlist_countries = [\"RO\", \"AE\", \"NG\", \"UA\"]\n",
        "candidates = df[df[\"country\"].isin(___)]  # \u2190 fill in the variable name\n",
        "candidates[[\"transaction_id\", \"country\", \"failed_login_count\",\n",
        "            \"device_trust_score\", \"ip_risk_score\", \"transaction_note\"]]\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Step 2 \u2014 Apply multi-signal risk thresholds\n",
        "\n",
        "Multiple weak signals together are much stronger evidence than any single indicator.\n",
        "Apply **all three** thresholds simultaneously to narrow down to the most critical record:\n",
        "\n",
        "- **failed_login_count \u2265 8** \u2014 nine or more consecutive failures suggest a brute-force attempt\n",
        "- **device_trust_score < 15** \u2014 below 15 means an unrecognised, unmanaged device\n",
        "- **ip_risk_score > 90** \u2014 above 90 places the source in a known-bad IP range\n",
        "\n",
        "> **Verification tip:** After filtering, look at which candidates were cleared and why.\n",
        "> Do not decode anything until you can explain why each ruled-out record was legitimate.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# Apply all three risk thresholds \u2014 fill in each numeric value.\n",
        "# Hint: failed logins \u2265 8, device trust < 15, IP risk > 90\n",
        "suspicious = candidates[\n",
        "    (candidates[\"failed_login_count\"] >= ___) &\n",
        "    (candidates[\"device_trust_score\"] < ___) &\n",
        "    (candidates[\"ip_risk_score\"] > ___)\n",
        "]\n",
        "suspicious\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Step 3 \u2014 Decode the embedded clue\n",
        "\n",
        "The suspicious transaction note contains a prefixed Base64 message.\n",
        "Strip the prefix (`ADMIN-RECOVERY:`) and decode the remainder to recover\n",
        "the final investigation clue.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# The note field contains \"ADMIN-RECOVERY:\" followed by a Base64 string.\n",
        "# Split on \":\" and decode the second part.\n",
        "note = suspicious.iloc[0][\"transaction_note\"]\n",
        "encoded = note.split(\":\", 1)[1]  # text after \"ADMIN-RECOVERY:\"\n",
        "print(base64.b64decode(encoded).decode())\n"
      ]
    }
  ]
}
