{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "initial_id",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-21T09:37:49.171093Z",
     "start_time": "2025-04-21T09:37:49.165832Z"
    }
   },
   "outputs": [],
   "source": [
    "import datetime\n",
    "from collections import Counter, defaultdict\n",
    "import json\n",
    "import numpy as np\n",
    "\n",
    "from common import *\n",
    "from vulnerability_database import VulnerabilityDatabase"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f61dd831a7861303",
   "metadata": {},
   "source": [
    "#### Resultset generated with\n",
    "```\n",
    "python analyze_bundle_dataset.py --total 100000 --worker $(nproc) --batch-per-file -s filter_source_map_sources $DATASETS/results-*\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "f4f962a27d9e4a44",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-21T09:37:49.984151Z",
     "start_time": "2025-04-21T09:37:49.234904Z"
    }
   },
   "outputs": [],
   "source": [
    "with open(os.path.join(DATASETS, \"update-behavior-pnpm.json\"), \"r\") as f:\n",
    "    data = json.load(f)\n",
    "\n",
    "vulndb = VulnerabilityDatabase(os.path.join(DATASETS, \"vulndb.json\"))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "8250aa41825433f7",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-14T14:01:24.634324Z",
     "start_time": "2025-04-14T14:01:24.630873Z"
    }
   },
   "outputs": [],
   "source": [
    "results = defaultdict(lambda: defaultdict(list))\n",
    "\n",
    "stats_to_plot = {}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "ba8f464d5efe7898",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-14T14:01:27.098664Z",
     "start_time": "2025-04-14T14:01:24.679408Z"
    }
   },
   "outputs": [],
   "source": [
    "for n, day in enumerate(data):\n",
    "    for d in day:\n",
    "        domain = next(iter(d))\n",
    "        urls = d[domain]\n",
    "        libraries = set()\n",
    "        for extracted in map(parse_full_pnpm_names, urls):\n",
    "            libraries.update([tuple(e.rsplit(\"@\", 1)) for e in extracted])\n",
    "        histories = defaultdict(list)\n",
    "        for lib, vers in libraries:\n",
    "            histories[lib].append(vers)\n",
    "        for lib, verss in histories.items():\n",
    "            results[domain][lib].append(sorted(verss))\n",
    "        for lib in results[domain]:\n",
    "            if len(results[domain][lib]) < n+1:\n",
    "                results[domain][lib].append([])  # Preserve one measurement per day\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "b32d0405-4480-426c-9669-0bbe389df1cb",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-14T14:01:27.342294Z",
     "start_time": "2025-04-14T14:01:27.123710Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "domain='dubclub.win' library='@sentry/browser' version='8.36.0' update_time_diff.days=5\n",
      "domain='app.destinyitemmanager.com' library='react-router' version='7.0.1' update_time_diff.days=16\n",
      "domain='community.spiceworks.com' library='nanoid' version='5.0.8' update_time_diff.days=4\n",
      "not_updated_domains=377 not_updated_libs=4888 not_monotonous=8\n"
     ]
    }
   ],
   "source": [
    "not_updated_libs = 0\n",
    "not_updated_domains = 0\n",
    "not_monotonous = 0\n",
    "\n",
    "base_date = datetime.datetime.fromisoformat(\"2024-10-31T00:00:00Z\")\n",
    "\n",
    "for domain, domain_data in results.items():\n",
    "    no = len(domain_data) > 0\n",
    "    for library, history in domain_data.items():\n",
    "        if all([n < 2 for n in set(map(len, history))]):  # we ignore multiple installed versions in parallel\n",
    "            if len(set([v[0] for v in history if len(v) > 0])) > 1:\n",
    "                no = False\n",
    "\n",
    "                parsed_history = list(map(coerce_version, [v[0] for v in history if len(v) > 0]))\n",
    "                non_empty_history = [(n, v[0]) for n, v in enumerate(history) if len(v) > 0]\n",
    "                \n",
    "                if all(v1 <= v2 for v1, v2 in zip(parsed_history, parsed_history[1:])):\n",
    "                    updates = [(base_date + datetime.timedelta(days=n), v2) for (n, v1), (_, v2) in zip(non_empty_history, non_empty_history[1:]) if v1[0] != v2[0]]\n",
    "                    if library in vulndb.releases:\n",
    "                        for update_time, version in updates:\n",
    "                            if version in vulndb.releases[library]:\n",
    "                                update_time_diff = update_time - datetime.datetime.fromisoformat(vulndb.releases[library][version])\n",
    "                                print(f\"{domain=} {library=} {version=} {update_time_diff.days=}\")\n",
    "                            else:\n",
    "                                print(f\"WARNING: Missing version {version} for library {library}\")\n",
    "                    else:\n",
    "                        # This is expected, as we have private packages in here\n",
    "                        # print(f\"WARNING: No release info for {library}\")\n",
    "                        pass\n",
    "                else:\n",
    "                    not_monotonous += 1\n",
    "            else:\n",
    "                not_updated_libs += 1\n",
    "    if no:\n",
    "        not_updated_domains += 1\n",
    "\n",
    "print(f\"{not_updated_domains=} {not_updated_libs=} {not_monotonous=}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e6c492fe-f277-4d2f-8bd3-8ca0e68bfdc6",
   "metadata": {},
   "source": [
    "## When a new library version gets released, what percentage of domains uses the updates within 1/4/16 weeks?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "48ee44cc-ba63-4d5a-ac44-40f5d93e7f14",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-14T14:01:27.400285Z",
     "start_time": "2025-04-14T14:01:27.391510Z"
    }
   },
   "outputs": [],
   "source": [
    "intervals = [datetime.timedelta(days=i) for i in [7, 4*7, 16*7]]\n",
    "libraries = set(library for domain_data in results.values() for library in domain_data.keys())\n",
    "libraries_not_indexed = set(library for library in libraries if library not in vulndb.releases)\n",
    "libraries_with_recent_updates = set(library for library in libraries.difference(libraries_not_indexed) if len(vulndb.releases[library]) > 0 and base_date - datetime.datetime.fromisoformat(vulndb.releases[library][\"modified\"]) < max(intervals))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "10d5b177-2e39-4d58-901b-06daae70c838",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-14T14:02:23.683029Z",
     "start_time": "2025-04-14T14:01:27.417211Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1 week: [0.0, 0.0, 0.007014852176067933, 0.75]\n",
      "4 week: [0.0, 0.0, 0.02599053742848358, 1.0]\n",
      "16 week: [0.0, 0.0, 0.10613958960513507, 1.0]\n",
      "1 week (normalized): [0.0, 0.0, 0.006650501583260177, 0.43499999999999994]\n",
      "4 week (normalized): [0.0, 0.0, 0.020830420088195, 0.8050271739130435]\n",
      "16 week (normalized): [0.0, 0.0, 0.09697951765206159, 2.989038555925179]\n"
     ]
    }
   ],
   "source": [
    "stats = [[], [], []]\n",
    "prevalences = [[], [], []]\n",
    "\n",
    "log = []\n",
    "\n",
    "for library in libraries_with_recent_updates:\n",
    "    release_dates = {v: datetime.datetime.fromisoformat(vulndb.releases[library][v]) for v in vulndb.releases[library]}\n",
    "    release_order = list([k for k in vulndb.releases[library].keys() if k not in [\"created\", \"modified\"]])  # npm already has release order\n",
    "    \n",
    "    for i, interval in enumerate(intervals):\n",
    "        count = [0, 0]\n",
    "        prevalence = 0\n",
    "        for domain, domain_data in results.items():\n",
    "            domain_hit = False\n",
    "            if library in domain_data:\n",
    "                for version in release_order:\n",
    "                    if base_date - release_dates[version] < interval:  # must have a chance of overlap\n",
    "                        history = domain_data[library]\n",
    "                        found_this_or_later_in_interval = False\n",
    "                        for day, versions in enumerate(history):\n",
    "                            date = base_date + datetime.timedelta(days=day)\n",
    "                            if date < release_dates[version]:\n",
    "                                continue\n",
    "                            if date > release_dates[version] + interval:\n",
    "                                break\n",
    "                                \n",
    "                            for v in versions:\n",
    "                                if v in release_dates and release_order.index(v) >= release_order.index(version):\n",
    "                                    found_this_or_later_in_interval = True\n",
    "                                    break\n",
    "                        count[0 if found_this_or_later_in_interval else 1] += 1\n",
    "                        # if library == \"@uppy/core\": log.append(f\"{library=} {interval.days=} {domain=} {found_this_or_later_in_interval=} {version=} {v=}\")\n",
    "                        domain_hit = True\n",
    "                if domain_hit:\n",
    "                    prevalence += 1\n",
    "        if sum(count) > 0:\n",
    "            stats[i].append(count[0] / sum(count))\n",
    "            if count[0] > 0: log.append(f\"{count=} {prevalence=} {library=} {interval.days=}\")\n",
    "            prevalences[i].append(prevalence)\n",
    "                                    \n",
    "\n",
    "print(f\"1 week: {compute_statistics(stats[0])}\")\n",
    "print(f\"4 week: {compute_statistics(stats[1])}\")\n",
    "print(f\"16 week: {compute_statistics(stats[2])}\")\n",
    "\n",
    "stats_normalized = [np.multiply(np.multiply(stat, prevalence), len(prevalence) / np.array(prevalence).sum()) for stat, prevalence in zip(stats, prevalences)]\n",
    "\n",
    "print(f\"1 week (normalized): {list(map(float, compute_statistics(stats_normalized[0])))}\")\n",
    "print(f\"4 week (normalized): {list(map(float, compute_statistics(stats_normalized[1])))}\")\n",
    "print(f\"16 week (normalized): {list(map(float, compute_statistics(stats_normalized[2])))}\")\n",
    "\n",
    "stats_to_plot[\"library_stats\"] = stats\n",
    "stats_to_plot[\"library_instances_stats\"] = [list(s) for s in stats_normalized]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b030bb26-f909-4298-919e-1835d7fead7c",
   "metadata": {},
   "source": [
    "## How many domains update their dependencies?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "5a73f0c7-8d7d-457a-b6e4-b7468caaab6f",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-14T14:03:21.257633Z",
     "start_time": "2025-04-14T14:02:23.733483Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1 week: [0.0, 0.0, 0.00212430874976802, 0.15]\n",
      "4 week: [0.0, 0.0, 0.00574210958739292, 0.5130434782608696]\n",
      "16 week: [0.0, 0.0, 0.077297906638259, 1.0]\n"
     ]
    }
   ],
   "source": [
    "stats = [[], [], []]\n",
    "\n",
    "for domain, domain_data in results.items():\n",
    "    \n",
    "    for i, interval in enumerate(intervals):\n",
    "        count = [0, 0]\n",
    "        prevalence = 0\n",
    "        for library in libraries_with_recent_updates:\n",
    "            if library in domain_data:\n",
    "                release_dates = {v: datetime.datetime.fromisoformat(vulndb.releases[library][v]) for v in vulndb.releases[library]}\n",
    "                release_order = list([k for k in vulndb.releases[library].keys() if k not in [\"created\", \"modified\"]])  # npm already has release order\n",
    "                for version in release_order:\n",
    "                    if base_date - release_dates[version] < interval:  # must have a chance of overlap\n",
    "                        history = domain_data[library]\n",
    "                        found_this_or_later_in_interval = False\n",
    "                        for day, versions in enumerate(history):\n",
    "                            date = base_date + datetime.timedelta(days=day)\n",
    "                            if date < release_dates[version]:\n",
    "                                continue\n",
    "                            if date > release_dates[version] + interval:\n",
    "                                break\n",
    "                                \n",
    "                            for v in versions:\n",
    "                                if v in release_dates and release_order.index(v) >= release_order.index(version):\n",
    "                                    found_this_or_later_in_interval = True\n",
    "                                    break\n",
    "                        count[0 if found_this_or_later_in_interval else 1] += 1\n",
    "        if sum(count) > 0:\n",
    "            stats[i].append(count[0] / sum(count))\n",
    "            if count[0] > 0: log.append(f\"{count=} {prevalence=} {library=} {interval.days=}\")\n",
    "                                    \n",
    "\n",
    "print(f\"1 week: {compute_statistics(stats[0])}\")\n",
    "print(f\"4 week: {compute_statistics(stats[1])}\")\n",
    "print(f\"16 week: {compute_statistics(stats[2])}\")\n",
    "\n",
    "stats_to_plot[\"domain_stats\"] = stats"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "acb14e8a-86cc-4c41-aff4-3d6d6ec06317",
   "metadata": {},
   "source": [
    "## How many vulnerable libraries are included per domain?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "ee1f49e7-77b7-4000-92a6-ea961249ada3",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-21T09:38:07.091691Z",
     "start_time": "2025-04-21T09:38:03.292893Z"
    },
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Most common vulnerable libraries:\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[('webpack', 1342),\n",
       " ('axios', 135),\n",
       " ('svelte', 131),\n",
       " ('nuxt', 130),\n",
       " ('cookie', 122),\n",
       " ('bootstrap', 106),\n",
       " ('vue', 86),\n",
       " ('url-parse', 76),\n",
       " ('dompurify', 37),\n",
       " ('next', 31)]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Stats (vulnerable vs safe) for each day:\n",
      "mean: 0.04865334063537752\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[0.03372007292800274,\n",
       " 0.04495276322179056,\n",
       " 0.03667578074134805,\n",
       " 0.038449005571181166,\n",
       " 0.038599260643951055,\n",
       " 0.041748634453523795,\n",
       " 0.037414443019384194,\n",
       " 0.043237796897277934,\n",
       " 0.038390670031622595,\n",
       " 0.04099244599654449,\n",
       " 0.04276463656497769,\n",
       " 0.041207545854613004,\n",
       " 0.04283819100588487,\n",
       " 0.04296195387008352,\n",
       " 0.04462435617429964,\n",
       " 0.04650304537997382,\n",
       " 0.04284842999156158,\n",
       " 0.04516684681508887,\n",
       " 0.040661025012956244,\n",
       " 0.04868642502715165,\n",
       " 0.0529732915225688,\n",
       " 0.045275536423978996,\n",
       " 0.04901033771471635,\n",
       " 0.044895765117445106,\n",
       " 0.0535136890893124,\n",
       " 0.04543646751830908,\n",
       " 0.0558324285644705,\n",
       " 0.043985402144033146,\n",
       " 0.05356995248716605,\n",
       " 0.052946948257281495,\n",
       " 0.050645478359376,\n",
       " 0.054364765450295206,\n",
       " 0.05138533749046605,\n",
       " 0.05596423295106347,\n",
       " 0.05963890252361591,\n",
       " 0.06541718582928148,\n",
       " 0.06382762223781394,\n",
       " 0.06848110086596501,\n",
       " 0.055195152521441686,\n",
       " 0.06084889926864974,\n",
       " 0.06626930576517696,\n",
       " 0.05200497707286288,\n",
       " 0.05816753894472569]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "what_libraries = Counter()\n",
    "n_days = len(data)\n",
    "\n",
    "stats = []\n",
    "\n",
    "for day in range(n_days):\n",
    "    stats.append([0, 0])\n",
    "    for domain, domain_data in results.items():\n",
    "        domainstats = [0, 0]\n",
    "        for library, history in domain_data.items():\n",
    "            if day >= len(history): continue\n",
    "            \n",
    "            versions = history[day]\n",
    "            if len(versions) > 0:\n",
    "                version = versions[0]  # todo ignore multiple versions\n",
    "                if vulndb.is_vulnerable(library, str(coerce_version(version))):\n",
    "                    domainstats[0] += 1\n",
    "                    what_libraries.update({library: 1})\n",
    "                else:\n",
    "                    domainstats[1] += 1\n",
    "        if sum(domainstats) > 0:\n",
    "            stats[-1][0] += domainstats[0] / sum(domainstats)\n",
    "            stats[-1][1] += domainstats[1] / sum(domainstats)\n",
    "\n",
    "print(\"Most common vulnerable libraries:\")\n",
    "display(what_libraries.most_common(10))\n",
    "\n",
    "print(\"Stats (vulnerable vs safe) for each day:\")\n",
    "print(f\"mean: {np.average([s[0] / sum(s) for s in stats if sum(s) > 0])}\")\n",
    "display([s[0] / sum(s) for s in stats if sum(s) > 0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "45234eb7-d685-49b6-a087-c914fbd2c591",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-14T14:03:24.695289Z",
     "start_time": "2025-04-14T14:03:24.690127Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2685"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sum(what_libraries.values())"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
