[
  {
    "path": "CONTRIBUTING.md",
    "content": "# How to Contribute\n\nWe'd love to accept your patches and contributions to this project. There are\njust a few small guidelines you need to follow.\n\n## Contributor License Agreement\n\nContributions to this project must be accompanied by a Contributor License\nAgreement. You (or your employer) retain the copyright to your contribution;\nthis simply gives us permission to use and redistribute your contributions as\npart of the project. Head over to <https://cla.developers.google.com/> to see\nyour current agreements on file or to sign a new one.\n\nYou generally only need to submit a CLA once, so if you've already submitted one\n(even if it was for a different project), you probably don't need to do it\nagain.\n\n## Code reviews\n\nAll submissions, including submissions by project members, require review. We\nuse GitHub pull requests for this purpose. Consult\n[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more\ninformation on using pull requests.\n\n## Community Guidelines\n\nThis project follows\n[Google's Open Source Community Guidelines](https://opensource.google/conduct/).\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# USB Keystroke Injection Protection\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n\n## Overview\nThis tool is a daemon for blocking USB keystroke injection devices on Linux systems.\n\nIt supports two different modes of operation: **monitoring** and **hardening**. In\nmonitor mode, information about a potentially attacking USB device is collected\nand logged to syslog. In hardening mode, the attacking USB device is ejected\nfrom the operating system by unbinding the driver.\n\n### Installation Prerequisites\nThe installation is mainly handled by `setup.sh`, however, there are some prerequisites \nthat need to be adjusted before running the script:\n\n1) Install Python3.7 or later, python dev package, virtualenv (`python3-venv`) and PIP3 (`python3-pip`) if not already \navailable on the system.\n\n1) Adjust the `KEYSTROKE_WINDOW` variable on top of the `setup.sh` file. This is the \nnumber of keystrokes the daemon looks at to determine whether its dealing with an attack or not. \nThe lower the number, the higher the false positives will be (e.g., if the number is 2, the tool \nlooks at only 1 interarrival time between those two keystrokes to determine whether it's an \nattack or not. Obviously, users sometimes hit two keys almost at the same time, which leads \nto the aforementioned false positive). Based on our internal observations, 5 is a value that \nis effective. However, it should be adjusted based on specific users' experiences and typing \nbehaviour.\n\n1) Adjust the `ABNORMAL_TYPING` variable on top of the `setup.sh` file. This variable \nspecifies what interarrival time (between two keystrokes) should be classified as malicious. \nThe higher the number, the more false-positives will arise (normal typing speed will be \nclassified as malicious), where more false-negatives will arise with a lower number (even very \nfast typing attacks will be classified as benign). That said, the preset `50000` after initial \ninstallation is a safe default but should be changed to a number reflecting the typing speed of \nthe user using the tool.\n\n1) Set the mode the daemon should run in by adjusting the `RUN_MODE` variable on top of the \n`setup.sh` file. Setting it to `MONITOR` will send information about the USB device to a logging \ninstance without blocking the device. Setting the variable to `HARDENING` will remove an \nattacking device from the system by unbinding the driver.\n\n1) Adjust the `DEBIAN` variable on top of the `setup.sh` file. This variable indicates \nwhether the system the tool is installed on is a Debian derivate or something else. This determination \nis important for the installation of the systemd service later on (the path, the service will be \ncopied to).\n\n1) Adjust the allowlist file in `data/allowlist`. This file will be installed to `/etc/ukip/` \non your system and taken as source of truth for allowed devices, in case a device is \nexceeding the preset `ABNORMAL_TYPING` speed. As described in the file, the allowed device \ncan be narrowed down with a specific set of characters to allow to even more minimize the attack \nsurface. For example, if your keyboard uses a macro that sends `rm -rf /` allow those characters, \nand even an attacking device spoofing your keyboards product ID and vendor ID couldn't inject an \nattack (except an attack using those specific characters obviously :D ). For other cases, the \n`any` keyword allows all possible characters for a specified device and `none` disallows \nall characters. Please keep in mind that this allowlist will only be taken into consideration, if\na device is exceeding the set threshold.  \n\n1) Adjust the keycodes file in `data/keycodes`. This file stores the relation between scancodes \nsent by the keyboard and keycodes you see on the keyboard. The default keycodes file as it is now \nhas the scancode<->keycode layout for the US keyboard layout. If you are using a different layout, \nplease adjust the file to fit your needs.\n\n### Installation\nOnce all of the above prerequisites are fulfilled, `setup.sh` should do the rest. It will install \ndepending libraries into your users home directory (`$HOME/.ukip/`) so you don't have to install \nthem system wide:\n```\nchmod +x setup.sh\n./setup.sh\n```\nThat's it: The daemon will be automatically started at boot time.  \n\nFor interaction with the service, the systemd interface is probably the most convenient one.\nTo check the status:\n```\nsystemctl status ukip.service\n```\n\nTo stop the service:\n```\nsudo systemctl stop ukip.service\n```\n\nAlternatively, to disable the service and prevent it from being started at boot time:\n```\nsudo systemctl disable ukip.service\n```\n\n## Terms of use\n\n### USB Keystroke Injection Protection\nThis project provides code that can be run on Linux systems to harden those systems against keystroke injection attacks, delivered via USB.\nThe terms of use apply to data provided by Google or implicitly through code in this repository.\n\n```\nThis tool hereby grants you a perpetual, worldwide, non-exclusive,\nno-charge, royalty-free, irrevocable copyright license to reproduce, prepare\nderivative works of, publicly display, publicly perform, sublicense, and\ndistribute code in this repository related to this tool. Any copy you make for\nsuch purposes is authorized provided that you reproduce this tool's copyright\ndesignation and this license in any such copy.\n```\n\n### Third-party Libraries\nThis project builds upon several open source libraries.  \nPlease see each projects' Terms of use when using the provided code in this repository.\n\n## Disclaimer\n**This is not an officially supported Google product.**\n"
  },
  {
    "path": "data/allowlist",
    "content": "# This is the allowlist for UKIP: USB Keystroke Injection Protection.\n#\n# Devices are added manually by a user in the following form, one rule per line:\n# <product ID in hex>:<vendor ID in hex> <allowed characters, comma separated>\n# An example for the Yubikey:\n0x10:0x1050 c,b,d,e,f,g,h,i,j,k,l,n,r,t,u,v\n\n# If every character should be allowed, the product ID and vendor ID, followed\n# by the keyword any is sufficient.\n#\n# The following would be an example for the product ID 0x1234 and the vendor ID\n# 0x1337 (without the starting hashtag):\n# 0x1234:0x1337 any\n\n# If no character should be allowed, the approach is similar, but the keyword is\n# none.\n#\n# The following would be an example for the product ID 0x1337 and the vendor ID\n# 0x1234 (without the starting hashtag):\n# 0x1337:0x1234 none\n"
  },
  {
    "path": "data/keycodes",
    "content": "{\n  \"lowcodes\":\n  [{\n    \"1\": \"ESC\",\n    \"2\": \"1\",\n    \"3\": \"2\",\n    \"4\": \"3\",\n    \"5\": \"4\",\n    \"6\": \"5\",\n    \"7\": \"6\",\n    \"8\": \"7\",\n    \"9\": \"8\",\n    \"10\": \"9\",\n    \"11\": \"0\",\n    \"12\": \"-\",\n    \"13\": \"=\",\n    \"14\": \"BKSP\",\n    \"15\": \"TAB\",\n    \"16\": \"q\",\n    \"17\": \"w\",\n    \"18\": \"e\",\n    \"19\": \"r\",\n    \"20\": \"t\",\n    \"21\": \"y\",\n    \"22\": \"u\",\n    \"23\": \"i\",\n    \"24\": \"o\",\n    \"25\": \"p\",\n    \"26\": \"[\",\n    \"27\": \"]\",\n    \"28\": \"CRLF\",\n    \"29\": \"LCTRL\",\n    \"30\": \"a\",\n    \"31\": \"s\",\n    \"32\": \"d\",\n    \"33\": \"f\",\n    \"34\": \"g\",\n    \"35\": \"h\",\n    \"36\": \"j\",\n    \"37\": \"k\",\n    \"38\": \"l\",\n    \"39\": \";\",\n    \"40\": \"\\\"\",\n    \"41\": \"`\",\n    \"42\": \"LSHFT\",\n    \"43\": \"\\\\\",\n    \"44\": \"z\",\n    \"45\": \"x\",\n    \"46\": \"c\",\n    \"47\": \"v\",\n    \"48\": \"b\",\n    \"49\": \"n\",\n    \"50\": \"m\",\n    \"51\": \",\",\n    \"52\": \".\",\n    \"53\": \"/\",\n    \"54\": \"RSHFT\",\n    \"56\": \"LALT\",\n    \"57\": \" \",\n    \"100\": \"RALT\"\n  }],\n\n  \"capscodes\":\n  [{\n    \"1\": \"ESC\",\n    \"2\": \"!\",\n    \"3\": \"@\",\n    \"4\": \"#\",\n    \"5\": \"$\",\n    \"6\": \"%\",\n    \"7\": \"^\",\n    \"8\": \"&\",\n    \"9\": \"*\",\n    \"10\": \"(\",\n    \"11\": \")\",\n    \"12\": \"_\",\n    \"13\": \"+\",\n    \"14\": \"BKSP\",\n    \"15\": \"TAB\",\n    \"16\": \"Q\",\n    \"17\": \"W\",\n    \"18\": \"E\",\n    \"19\": \"R\",\n    \"20\": \"T\",\n    \"21\": \"Y\",\n    \"22\": \"U\",\n    \"23\": \"I\",\n    \"24\": \"O\",\n    \"25\": \"P\",\n    \"26\": \"{\",\n    \"27\": \"}\",\n    \"28\": \"CRLF\",\n    \"29\": \"LCTRL\",\n    \"30\": \"A\",\n    \"31\": \"S\",\n    \"32\": \"D\",\n    \"33\": \"F\",\n    \"34\": \"G\",\n    \"35\": \"H\",\n    \"36\": \"J\",\n    \"37\": \"K\",\n    \"38\": \"L\",\n    \"39\": \":\",\n    \"40\": \"'\",\n    \"41\": \"~\",\n    \"42\": \"LSHFT\",\n    \"43\": \"|\",\n    \"44\": \"Z\",\n    \"45\": \"X\",\n    \"46\": \"C\",\n    \"47\": \"V\",\n    \"48\": \"B\",\n    \"49\": \"N\",\n    \"50\": \"M\",\n    \"51\": \"<\",\n    \"52\": \">\",\n    \"53\": \"?\",\n    \"54\": \"RSHFT\",\n    \"56\": \"LALT\",\n    \"57\": \" \",\n    \"100\": \"RALT\"\n  }]\n}\n"
  },
  {
    "path": "data/ukip.service",
    "content": "[Unit]\nDescription=UKIP\nRequires=systemd-udevd.service\nAfter=systemd-udevd.service\n\n[Service]\nExecStart=/usr/sbin/ukip\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "requirements.txt",
    "content": "attrs==18.1.0\npyudev==0.21.0\nattr==0.3.1\nevdev==1.3.0\npyusb==1.0.2\n"
  },
  {
    "path": "setup.sh",
    "content": "#!/bin/bash\n# Copyright 2019 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# Replace those variables to fit your needs.\nNEW_KEYSTROKE_WINDOW=5\nNEW_ABNORMAL_TYPING=50000\n# Set either MONITOR or HARDENING.\nRUN_MODE=MONITOR\n\n# For systemd it's important to know which Linux flavor.\nDEBIAN=true\n\n# Path to virtual environment (.ukip/ in the user's home).\nVENV_PATH=$HOME'/.ukip/'\n\n\nfunction info() {\n  echo -e \"[\\e[94m*\\e[0m]\" \"$@\"\n}\n\nfunction error() {\n  echo -e \"[\\e[91m!\\e[0m]\" \"$@\"\n}\n\nfunction success() {\n  echo -e \"[\\e[92m+\\e[0m]\" \"$@\"\n}\n\nfunction fatal() {\n  error \"$@\"\n  exit 1\n}\n\nfunction install_virtual_env() {\n  # Replace the shebang line.\n  sed -i 's@#!/usr/bin/env python3@#!'$VENV_PATH'bin/python3@g' src/ukip.py\n\n  # Install the needed virtual environemt.\n  /usr/bin/env python3 -m venv $VENV_PATH\n\n  # Activate the venv.\n  source $VENV_PATH'bin/activate'\n\n  # Install wheel before requirements.\n  /usr/bin/env pip3 -q install wheel\n\n  # Install the required packages.\n  /usr/bin/env pip3 -q install -r requirements.txt\n\n  success \"Successfully prepared and installed the virtual environment.\"\n}\n\nfunction replace_variables() {\n  sed -i 's/ABNORMAL_TYPING = [^0-9]*\\([0-9]\\+\\)/ABNORMAL_TYPING = '$NEW_ABNORMAL_TYPING'/g' src/ukip.py\n  sed -i 's/KEYSTROKE_WINDOW = [^0-9]*\\([0-9]\\+\\)/KEYSTROKE_WINDOW = '$NEW_KEYSTROKE_WINDOW'/g' src/ukip.py\n  sed -i 's/_UKIP_RUN_MODE = UKIP_AVAILABLE_MODES\\.\\(MONITOR\\|HARDENING\\)/_UKIP_RUN_MODE = UKIP_AVAILABLE_MODES\\.'$RUN_MODE'/g' src/ukip.py\n\n\n  success \"Successfully replaced abnormal typing and keystroke window variables in UKIP.\"\n  success \"Successfully set the run mode for UKIP.\"\n}\n\nfunction prepare_metadata() {\n  ALLOWLIST_FILE=/etc/ukip/allowlist\n  KEYCODES_FILE=/etc/ukip/keycodes\n\n  sudo mkdir /etc/ukip/\n\n  sudo cp data/allowlist $ALLOWLIST_FILE\n  sudo chmod 0755 $ALLOWLIST_FILE\n  sudo chown root:root $ALLOWLIST_FILE\n\n  sudo cp data/keycodes $KEYCODES_FILE\n  sudo chmod 0755 $KEYCODES_FILE\n  sudo chown root:root $KEYCODES_FILE\n\n  success \"Installed the allowlist and the keycodes file in /etc/ukip/.\"\n}\n\nfunction install_ukip() {\n  UKIP_BINARY=/usr/sbin/ukip\n\n  sudo cp src/ukip.py $UKIP_BINARY\n  sudo chmod 0755 $UKIP_BINARY\n  sudo chown root:root $UKIP_BINARY\n\n  success \"Installed UKIP in /usr/sbin/.\"\n}\n\nfunction install_systemd_service() {\n  if $DEBIAN; then\n    # For Debian based OSs.\n    SYSTEMD_PATH=/lib/systemd/system/ukip.service\n  else\n    # For Fedora based OSs.\n    SYSTEMD_PATH=/usr/lib/systemd/system/ukip.service\n  fi\n\n  sudo cp data/ukip.service $SYSTEMD_PATH\n  sudo chmod 0644 $SYSTEMD_PATH\n  sudo chown root:root $SYSTEMD_PATH\n\n  sudo systemctl start ukip.service\n\n  # The start and enabling sometimes race.\n  sleep 1\n\n  sudo systemctl enable ukip.service\n\n  success \"Installed and started systemd service.\"\n}\n\ninfo \"Preparing and installing the virtual environment...\"\ninstall_virtual_env\n\ninfo \"Replacing keystroke window, abnormal typing speed and run mode...\"\nreplace_variables\n\ninfo \"Preparing UKIP metadata...\"\nprepare_metadata\n\ninfo \"Installing UKIP...\"\ninstall_ukip\n\ninfo \"Installing and starting systemd service...\"\ninstall_systemd_service\n\nsuccess \"UKIP is now installed and enabled on startup!\"\n"
  },
  {
    "path": "src/__init__.py",
    "content": ""
  },
  {
    "path": "src/ukip.py",
    "content": "#!/usr/bin/env python3\n# Copyright 2020 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrom __future__ import absolute_import\nfrom __future__ import division\nfrom __future__ import print_function\nfrom __future__ import unicode_literals\n\nimport collections\nimport gc\nimport json\nimport logging\nimport logging.handlers\nimport sys\nimport threading\nimport attr\nimport enum\nimport evdev\nimport pyudev\nfrom typing import Text\nimport usb\n\n\n# Modes, available for UKIP to run in. Constant enum:\n# 1) MONITOR: Sends information about the usb device to a logging instance.\n# 2) HARDENING: The device gets removed from the system (drivers are unbound\n#    from every device interface).\nclass UKIP_AVAILABLE_MODES(enum.Enum):\n  MONITOR = 'MONITOR'\n  HARDENING = 'HARDENING'\n\n\n# The current mode, UKIP is running in.\n_UKIP_RUN_MODE = UKIP_AVAILABLE_MODES.HARDENING\n\n# A dict with ringbuffers as values (holding the most recent 5 keystroke times):\n# keys are paths to the event devices.\n_event_devices_timings = {}\n\n# A dict with ringbuffers as values (holding the most recent 5 keystrokes):\n# keys are paths to the event devices.\n_event_devices_keystrokes = {}\n\n# Window of keystrokes to look at.\nKEYSTROKE_WINDOW = 5\n\n# Abnormal typing threshold in milliseconds (Linux emits keystroke timings in\n# microsecond precision).\n# Lower: More True Positives.\n# Higher: More False Positives.\nABNORMAL_TYPING = 50000\n\n# 1 equals KEY_DOWN in evdev.\nKEY_DOWN = evdev.KeyEvent.key_down\n\n# Shifts as constants for better readability.\nLSHIFT = 42\nRSHIFT = 54\n\n# Turn off duplicate logging to syslog, that would happen with the root logger.\nlogging.basicConfig(filename='/dev/null', level=logging.DEBUG)\n# Now, turn on logging to syslog.\nlog = logging.getLogger(__name__)\nlog.setLevel(logging.DEBUG)\nhandler = logging.handlers.SysLogHandler(address='/dev/log')\nlog.addHandler(handler)\n\n# Global lock for _event_devices_timings and _event_devices_keystrokes dicts.\n_event_devices_lock = threading.Lock()\n\n\n@attr.s\nclass AllowlistConfigReturn(object):\n  \"\"\"Class to represent the return value of the allowlist Config.\n\n  The following return combinations are valid:\n  1) allowlist is a list with characters, device_present is true: the returned\n  characters are not blocked by UKIP for the given device.\n  2) allowlist is an empty list, device_present is true: for the given device,\n  any character is allowed by UKIP.\n  3) allowlist is an empty list, device_present is false: for the given device,\n  no character is allowed by UKIP (either the device is not in the config\n  file, or a user specifically marked that device with 'none' for the allowed\n  characters).\n\n  Attributes:\n   allowlist: The returned allowlist, or empty if all characters are allowed.\n   device_present: A boolean, whether the device was found in the config file.\n  \"\"\"\n  allowlist = attr.ib()  # type: list\n  device_present = attr.ib()  # type: boolean\n\n\n@attr.s\nclass KeycodesReturn(object):\n  \"\"\"Class to represent the return value of the keycode file read.\n\n  The keycode file in /etc/ukip/keycodes contains the scancodes and ASCII\n  codes for the selected keyboard layout. It is parsed once and read into two\n  dicts for further processing: lower_codes and capped_codes.\n  \"\"\"\n  lower_codes = attr.ib()  # type: dict\n  capped_codes = attr.ib()  # type: dict\n\n\nclass DeviceError(Exception):\n  \"\"\"Generic error class for device processing.\"\"\"\n\n\nclass AllowlistFileError(Exception):\n  \"\"\"Generic error class for allowlist processing.\"\"\"\n\n\nclass KeycodesFileError(Exception):\n  \"\"\"Generic error class for keycode file processing.\"\"\"\n\n\ndef add_to_ring_buffer(event_device_path: Text, key_down_time: int,\n                       keystroke: Text, device: usb.core.Device):\n  \"\"\"Add time in milliseconds to global ringbuffer.\n\n  Locates the event device (/dev/input/*) in the dict of ringbuffers and adds\n  the KEY_DOWN time in milliseconds to it. Then calls the check_for_attack\n  function on the event device and the usb core device.\n\n  Args:\n    event_device_path: The path to the event device (/dev/input/*).\n    key_down_time: The KEY_DOWN time in milliseconds.\n    keystroke: The actual key typed.\n    device: A USB device (usb.core.Device).\n  \"\"\"\n  with _event_devices_lock:\n    if event_device_path not in _event_devices_timings:\n      _event_devices_timings[event_device_path] = collections.deque(\n          maxlen=KEYSTROKE_WINDOW)\n      _event_devices_keystrokes[event_device_path] = collections.deque(\n          maxlen=KEYSTROKE_WINDOW)\n\n    _event_devices_timings[event_device_path].append(key_down_time)\n    _event_devices_keystrokes[event_device_path].append(keystroke)\n\n  check_for_attack(event_device_path, device)\n\n\ndef check_local_allowlist(product_id: Text,\n                          vendor_id: Text) -> AllowlistConfigReturn:\n  \"\"\"Check local (user-based) allowlist for specifically allowed devices.\n\n  UKIP users are able to specify USB devices they want to allow in a local\n  file. This allowlist is checked, when a device is found attacking (timing\n  threshold is exceeded) and whether that device is listed in here. If so, only\n  the characters listed in the corresponding allowlist are allowed, the others\n  are denied (in case of 'any' and 'none' all or no characters are allowed\n  respectively). If the device is not listed in the allowlist, it is denied per\n  default.\n\n  Args:\n    product_id: The required product ID to look up in the local allowlist.\n    vendor_id: The required vendor ID to look up in the local allowlist.\n\n  Raises:\n    AllowlistFileError: When there were errors with the allowlist config file.\n\n  Returns:\n    A AllowlistConfigReturn object, with the following variations:\n    1) allowlist is a list with characters, device_present is true: the returned\n    characters are not blocked by UKIP for the given device.\n    2) allowlist is an empty list, device_present is true: for the given device\n    any character is allowed by UKIP.\n    3) allowlist is an empty list, device_present is false: for the given device\n    no character is allowed by UKIP (either the device is not in the config\n    file, or a user specifically marked that device with 'none' for the allowed\n    characters).\n  \"\"\"\n  device = '%s:%s' % (product_id, vendor_id)\n\n  try:\n    with open('/etc/ukip/allowlist', 'r') as f:\n      for line in f:\n        # Comments start with '#'.\n        if line[0] == '#':\n          continue\n        # Ignore empty lines.\n        if not line.strip():\n          continue\n\n        try:\n          (key, val) = line.split()\n          int(key.split(':')[0], 16)\n          int(key.split(':')[1], 16)\n\n          allowlist = val.split(',')\n\n          if key != device:\n            continue\n          if allowlist[0] == 'any':\n            return AllowlistConfigReturn(allowlist=[], device_present=True)\n          if allowlist[0] == 'none':\n            return AllowlistConfigReturn(allowlist=[], device_present=False)\n\n          # If all of the checks succeed, return the allowlist (but only if it\n          # is an allowlist, and not a word).\n          if len(allowlist[0]) == 1:\n            return AllowlistConfigReturn(\n                allowlist=val.split(','), device_present=True)\n        except (ValueError, IndexError) as vi:\n          raise AllowlistFileError(\n              'The format of the config file /etc/ukip/allowlist seems to be'\n              ' incorrect: %s' % vi)\n\n      # If the device wasn't found in the file, return False.\n      return AllowlistConfigReturn(allowlist=[], device_present=False)\n  except FileNotFoundError as fnfe:\n    raise AllowlistFileError(\n        'The config file /etc/ukip/allowlist could not be found: %s' % fnfe)\n\n\ndef check_for_attack(event_device_path: Text, device: usb.core.Device) -> bool:\n  \"\"\"Check a ringbuffer of KEY_DOWN timings for attacks.\n\n  Locates the event device (/dev/input/*) in the dict of ringbuffers and checks\n  the correct ringbuffer for attacks (keystroke injection attack). In case of\n  an attack, two actions can be taken, depending on the mode UKIP is running in.\n  Those modes are specified in the UKIP_AVAILABLE_MODES enum.\n\n  Args:\n    event_device_path: The path to the event device (/dev/input/*).\n    device: A USB device (usb.core.Device).\n\n  Returns:\n    False: If the check failed (not enough times, mode not set). None otherwise.\n  \"\"\"\n  with _event_devices_lock:\n    if len(_event_devices_timings[event_device_path]) < KEYSTROKE_WINDOW:\n      return False\n\n    attack_counter = 0\n\n    # Count the number of adjacent keystrokes below (or equal) the\n    # ABNORMAL_TYPING.\n    reversed_buffer = reversed(_event_devices_timings[event_device_path])\n    for value in reversed_buffer:\n      for prev in reversed_buffer:\n        if value - prev <= ABNORMAL_TYPING:\n          attack_counter += 1\n        value = prev\n      break  # Exit after the first backward iteratation.\n\n  # If all the timings in the ringbuffer are within the ABNORMAL_TYPING timing.\n  if attack_counter == KEYSTROKE_WINDOW - 1:\n    if _UKIP_RUN_MODE == UKIP_AVAILABLE_MODES.MONITOR:\n      enforce_monitor_mode(device, event_device_path)\n    elif _UKIP_RUN_MODE == UKIP_AVAILABLE_MODES.HARDENING:\n      enforce_hardening_mode(device, event_device_path)\n    else:\n      log.error('No run mode was specified for UKIP. Exiting...')\n      return False\n\n\ndef enforce_monitor_mode(device: usb.core.Device, event_device_path: Text):\n  \"\"\"Enforce the MONITOR mode on a given device.\n\n  Information about devices, that would have been blocked in HARDENING mode\n  is logged to /dev/log.\n\n  Args:\n    device: A USB device (usb.core.Device).\n    event_device_path: The path to the event device (/dev/input/*).\n  \"\"\"\n  log.warning(\n      '[UKIP] The device %s with the vendor id %s and the product id'\n      ' %s would have been blocked. The causing timings are: %s.',\n      device.product if device.product else 'UNKNOWN', hex(device.idVendor),\n      hex(device.idProduct), _event_devices_timings[event_device_path])\n\n\ndef enforce_hardening_mode(device: usb.core.Device, event_device_path: Text):\n  \"\"\"Enforce the HARDENING mode on a given device.\n\n  When enforcing the HARDENING mode, a device gets removed from the operating\n  system when the keystrokes exceed the typing speed threshold\n  (ABNORMAL_TYPING). This is done by unbinding the drivers from every device\n  interface. Before the device is removed, the allowlist is checked. If the\n  product and vendor ids are in there, the function will return and the device\n  will continue working (possibly with a reduced allowed character set, as\n  described in the function check_local_allowlist).\n\n  Args:\n    device: A USB device (usb.core.Device).\n    event_device_path: The path to the event device (/dev/input/*).\n  \"\"\"\n\n  product_id = hex(device.idProduct)\n  vendor_id = hex(device.idVendor)\n\n  local_allowlist = check_local_allowlist(\n      hex(device.idProduct), hex(device.idVendor))\n\n  # Device is present in the allowlist and all characters are allowed.\n  if local_allowlist.device_present and not local_allowlist.allowlist:\n    return\n  # Device is present and an allowlist is specified.\n  elif local_allowlist.device_present and local_allowlist.allowlist:\n    allowlist = local_allowlist.allowlist\n  # Device is not in the allowlist or keyword is 'none'.\n  # i.e.: not local_allowlist.device_present and not local_allowlist.allowlist\n  else:\n    allowlist = []\n\n  # If all typed characters are in the allowlist, return. Otherwise run through\n  # the rest of the function.\n  if not set(_event_devices_keystrokes[event_device_path]).difference(\n      set(allowlist)):\n    return\n\n  pid_and_vid = '%s:%s' % (product_id, vendor_id)\n\n  for config in device:\n    for interface in range(config.bNumInterfaces):\n      if device.is_kernel_driver_active(interface):\n        try:\n          device.detach_kernel_driver(interface)\n\n          if device.product:\n            log.warning(\n                '[UKIP] The device %s with the vendor id %s and the '\n                'product id %s was blocked. The causing timings were: '\n                '%s.', device.product, vendor_id, product_id,\n                _event_devices_timings[event_device_path])\n          else:\n            log.warning(\n                '[UKIP] The device with the vendor id %s and the '\n                'product id %s was blocked. The causing timings were: '\n                '%s.', vendor_id, product_id,\n                _event_devices_timings[event_device_path])\n\n        except (IOError, OSError, ValueError, usb.core.USBError) as e:\n          log.warning(\n              'There was an error in unbinding the interface for the USB device'\n              ' %s: %s', pid_and_vid, e)\n          # In case of an error we still need to continue to the next interface.\n          continue\n\n  # The device was removed, so clear the dicts. Most importantly, clear the\n  # keystroke dict.\n  del _event_devices_timings[event_device_path]\n  del _event_devices_keystrokes[event_device_path]\n  gc.collect()\n\n\ndef load_keycodes_from_file() -> KeycodesReturn:\n  \"\"\"Helper function to load the keycodes file into memory.\n\n  Returns:\n    The lowcodes and capscodes as dicts in a KeycodesReturn attribute.\n  Raises:\n    KeycodesFileError: If there is a problem with the keycodes file.\n  \"\"\"\n  lowcodes = {}\n  capscodes = {}\n\n  try:\n    with open('/etc/ukip/keycodes', 'r') as keycode_file:\n      try:\n        keycodes = json.load(keycode_file)\n      except (OverflowError, ValueError, TypeError) as je:\n        raise KeycodesFileError('The keycodes file could not be read: %s' % je)\n  except FileNotFoundError as fnfe:\n    raise KeycodesFileError(\n        'The keycode file /etc/ukip/keycodes could not be found: %s' % fnfe)\n\n  if not keycodes.get('lowcodes') or not keycodes.get('capscodes'):\n    log.error(\n        'The keycodes file is missing either the lowcodes or capscodes keyword.'\n    )\n    return KeycodesReturn(lower_codes=lowcodes, capped_codes=capscodes)\n\n  for keycode in keycodes['lowcodes']:\n    for scancode, lowcode in keycode.items():\n      lowcodes[int(scancode)] = lowcode\n\n  for keycode in keycodes['capscodes']:\n    for scancode, capcode in keycode.items():\n      capscodes[int(scancode)] = capcode\n\n  return KeycodesReturn(lower_codes=lowcodes, capped_codes=capscodes)\n\n\ndef monitor_device_thread(device: pyudev.Device, vendor_id: int,\n                          product_id: int) -> None:\n  \"\"\"Monitor a given USB device for occurring KEY_DOWN events.\n\n  Creates a passive reading loop over a given event device and waits for\n  KEY_DOWN events to occour. Then extracts the time in milliseconds of the event\n  and adds it to the ringbuffer.\n\n  Args:\n    device: The event device in (/dev/input/*).\n    vendor_id: The vendor ID of the device.\n    product_id: The product ID of the device.\n\n  Raises:\n    OSError: If the given USB device cannot be found or if the OS receives\n             keyboard events, after the device was unbound. Both originate from\n             the evdev lib.\n    StopIteration: If the iteration of the usb device tree breaks.\n  \"\"\"\n  keycodes = load_keycodes_from_file()\n  lowcodes = keycodes.lower_codes\n  capscodes = keycodes.capped_codes\n\n  try:\n    try:\n      inputdevice = evdev.InputDevice(device.device_node)\n      dev = usb.core.find(idVendor=vendor_id, idProduct=product_id)\n    except (OSError, StopIteration) as mex:\n      log.warning(\n          'There was an error while starting the thread for device monitoring:'\n          ' %s', mex)\n\n      # Bail the function and with that, end the thread.\n      return\n\n    log.info(\n        f'Start monitoring {device.device_node} with the VID {hex(vendor_id)} and the PID {hex(product_id)}'\n    )\n\n    try:\n      # The default behaviour of evdev.InputDevice is a non-exclusive access,\n      # so each reader gets a copy of each event.\n      for event in inputdevice.read_loop():\n        caps = False\n\n        for led in inputdevice.leds(verbose=True):\n          # Check if CapsLock is turned on.\n          if 'LED_CAPSL' in led:\n            caps = True\n\n        # LShift or RShift is either pressed or held.\n        if LSHIFT in inputdevice.active_keys(\n        ) or RSHIFT in inputdevice.active_keys():\n          caps = True\n\n        if event.value == KEY_DOWN and event.type == evdev.ecodes.EV_KEY:\n          keystroke_in_ms = (event.sec * 1000000) + event.usec\n\n          if caps:\n            keystroke = capscodes.get(evdev.categorize(event).scancode)\n          else:\n            keystroke = lowcodes.get(evdev.categorize(event).scancode)\n\n          add_to_ring_buffer(device.device_node, keystroke_in_ms, keystroke,\n                             dev)\n\n    except OSError as ose:\n      log.warning('Events found for unbound device: %s', ose)\n  except:\n    log.exception('Error monitoring device.')\n\n\ndef init_device_list() -> int:\n  \"\"\"Adds all current event devices to the global dict of event devices.\n\n  Returns:\n    The number of event devices connected, at the time UKIP was started.\n  Raises:\n    TypeError: If there is an error in converting the PID/VID of a USB device.\n    ValueError: If there is an error in converting the PID/VID of a USB device.\n    RuntimeError: If there is an error in launching the thread.\n    DeviceError: If there is an error in creating the device list.\n  \"\"\"\n\n  device_count = 0\n\n  try:\n    local_device_context = pyudev.Context()\n    local_device_monitor = pyudev.Monitor.from_netlink(local_device_context)\n    local_device_monitor.filter_by(subsystem='input')\n  except (ValueError, EnvironmentError, DeviceError) as mex:\n    log.warning(\n        'There was an error creating the initial list of USB devices: %s', mex)\n    raise DeviceError('The device context and monitor could not be created.')\n\n  for device in local_device_context.list_devices():\n    if device.device_node and device.device_node.startswith(\n        '/dev/input/event') and (device.get('ID_VENDOR_ID') and\n                                 device.get('ID_MODEL_ID')):\n\n      try:\n        vendor_id = int(device.get('ID_VENDOR_ID'), 16)\n        product_id = int(device.get('ID_MODEL_ID'), 16)\n      except (TypeError, ValueError) as mex:\n        log.error(\n            'There was an error in converting the PID and VID of a USB device: '\n            '%s', mex)\n        continue\n\n      try:\n        threading.Thread(\n            target=monitor_device_thread,\n            args=(device, vendor_id, product_id)).start()\n        device_count += 1\n      except RuntimeError as e:\n        log.error(\n            'There was an runtime error in starting the monitoring thread %s',\n            e)\n\n  return device_count\n\n\ndef main(argv):\n  if len(argv) > 1:\n    sys.exit('Too many command-line arguments.')\n\n  device_count = init_device_list()\n\n  if not device_count:\n    log.warning('No HID devices connected to this machine yet')\n\n  #####################\n  # Hotplug detection #\n  #####################\n  context = pyudev.Context()\n  monitor = pyudev.Monitor.from_netlink(context)\n  monitor.filter_by(subsystem='input')\n\n  for device in iter(monitor.poll, None):\n    try:\n      if device.action == 'add':\n        if device.device_node and '/dev/input/event' in device.device_node and (\n            device.get('ID_VENDOR_ID') and device.get('ID_MODEL_ID')):\n\n          try:\n            vendor_id = int(device.get('ID_VENDOR_ID'), 16)\n            product_id = int(device.get('ID_MODEL_ID'), 16)\n          except (TypeError, ValueError) as mex:\n            log.error(\n                'There was an error in converting the PID and VID of a USB'\n                ' device: %s', mex)\n            continue\n\n          threading.Thread(\n              target=monitor_device_thread,\n              args=(device, vendor_id, product_id)).start()\n    except:\n      log.exception('Error adding new device to monitoring.')\n\n\nif __name__ == '__main__':\n  sys.exit(main(sys.argv))\n"
  },
  {
    "path": "src/ukip_test.py",
    "content": "#!/usr/bin/env python3\n# Copyright 2020 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrom __future__ import absolute_import\nfrom __future__ import division\nfrom __future__ import print_function\n\nimport builtins\nimport collections\nimport gc\nimport json\nimport unittest.mock as mock\nimport sys\nimport threading\nimport unittest\nimport ukip\nimport evdev\nimport pyudev\nimport usb\n\n\nsys.modules['evdev'] = mock.MagicMock()\nsys.modules['pyudev'] = mock.MagicMock()\nsys.modules['usb'] = mock.MagicMock()\n\n\n# This is needed, because the whole library is (Magic)mocked.\n# Therefore, without this an error is thrown, that usb.core.USBError is not\n# inheriting from BaseException.\nclass USBError(IOError):\n  pass\n\n\nclass UkipTest(unittest.TestCase):\n\n  def setUp(self):\n    super(UkipTest, self).setUp()\n\n    usb.core.USBError = USBError\n\n    ukip._event_devices_timings = {}\n    ukip._event_devices_keystrokes = {}\n\n    class FakePyudevDevice(object):\n      product = None\n      device_node = None\n      action = None\n      ID_VENDOR_ID = None\n      ID_MODEL_ID = None\n\n      def get(self, attribute):\n        return getattr(self, attribute)\n\n    class FakeEvent(object):\n      value = None\n      type = None\n      sec = None\n      usec = None\n      scancode = None\n\n    self.pyudev_device = FakePyudevDevice()\n    self.pyudev_device.product = 'FakeProduct'\n    self.pyudev_device.device_node = '/dev/input/event1337'\n    self.pyudev_device.action = 'add'\n    # Pyudev devices emit the PID and VID as strings (hex values, but str).\n    # Also, the PID (product ID) is called model ID (ID_MODEL_ID).\n    self.pyudev_device.ID_VENDOR_ID = '123'\n    self.pyudev_device.ID_MODEL_ID = '456'\n\n    self.fake_event = FakeEvent()\n    self.fake_event.value = evdev.KeyEvent.key_down\n    self.fake_event.type = evdev.ecodes.EV_KEY\n    self.fake_event.sec = 13\n    self.fake_event.usec = 477827\n    self.fake_event.scancode = 45\n\n    self.mock_inputdevice = mock.create_autospec(evdev.InputDevice)\n\n    self.mock_pyusb_device = mock.MagicMock()\n    self.mock_pyusb_device.product = 'SomeVendor Keyboard'\n    # PyUSB devices emit the PID and VID as integers.\n    self.mock_pyusb_device.idVendor = 123\n    self.mock_pyusb_device.idProduct = 456\n    self.mock_pyusb_device.is_kernel_driver_active.return_value = True\n\n    self.mock_usb_config = mock.create_autospec(usb.core.Configuration)\n    self.mock_usb_config.bNumInterfaces = 1\n\n    self.event_device_path = '/dev/input/event1337'\n\n    evdev.InputDevice.side_effect = None\n\n  @mock.patch.object(ukip, 'enforce_monitor_mode', autospec=True)\n  def test_check_for_attack_trigger_monitor(self, monitor_mode_mock):\n    \"\"\"Tests if the monitor mode is triggered for attacking device times.\"\"\"\n\n    ukip._UKIP_RUN_MODE = ukip.UKIP_AVAILABLE_MODES.MONITOR\n\n    # Need to access the global variable.\n    ukip._event_devices_timings[self.event_device_path] = collections.deque(\n        maxlen=ukip.KEYSTROKE_WINDOW)\n    ukip._event_devices_keystrokes[self.event_device_path] = collections.deque(\n        maxlen=ukip.KEYSTROKE_WINDOW)\n\n    # Push amount of KEYSTROKE_WINDOW times into the ringbuffer, that trigger\n    # the monitor mode.\n    ukip._event_devices_timings[self.event_device_path].append(1555146977759524)\n    ukip._event_devices_timings[self.event_device_path].append(1555146977759525)\n    ukip._event_devices_timings[self.event_device_path].append(1555146977759526)\n    ukip._event_devices_timings[self.event_device_path].append(1555146977759527)\n    ukip._event_devices_timings[self.event_device_path].append(1555146977759528)\n\n    ukip.check_for_attack(self.event_device_path, self.mock_pyusb_device)\n\n    # The timings trigger, so call the monitor mode.\n    monitor_mode_mock.assert_called_once_with(self.mock_pyusb_device,\n                                              self.event_device_path)\n\n  @mock.patch.object(ukip, 'enforce_monitor_mode', autospec=True)\n  def test_check_for_attack_not_trigger_monitor(self, monitor_mode_mock):\n    \"\"\"Tests if the monitor mode is NOT triggered for benign device times.\"\"\"\n\n    ukip._UKIP_RUN_MODE = ukip.UKIP_AVAILABLE_MODES.MONITOR\n\n    # Need to access the global variable.\n    ukip._event_devices_timings[self.event_device_path] = collections.deque(\n        maxlen=ukip.KEYSTROKE_WINDOW)\n\n    # Normal typing, that doesn't trigger the monitor mode.\n    ukip._event_devices_timings[self.event_device_path].append(1555146977759524)\n    ukip._event_devices_timings[self.event_device_path].append(1555146980127487)\n    ukip._event_devices_timings[self.event_device_path].append(1555146982271470)\n    ukip._event_devices_timings[self.event_device_path].append(1555146984415453)\n    ukip._event_devices_timings[self.event_device_path].append(1555146986559436)\n\n    ukip.check_for_attack(self.event_device_path, self.mock_pyusb_device)\n\n    # Since normal typing, the monitor mode was not called.\n    self.assertFalse(monitor_mode_mock.called)\n\n  @mock.patch.object(ukip, 'enforce_monitor_mode', autospec=True)\n  def test_check_for_attack_no_times(self, monitor_mode_mock):\n    \"\"\"Checks if function returns early, if no times are provided.\"\"\"\n\n    ukip._UKIP_RUN_MODE = ukip.UKIP_AVAILABLE_MODES.MONITOR\n\n    ukip._event_devices_timings[self.event_device_path] = collections.deque(\n        maxlen=ukip.KEYSTROKE_WINDOW)\n    not_enough_timings = ukip.check_for_attack(self.event_device_path,\n                                               self.mock_pyusb_device)\n\n    # Not enough times, so bail out of the function call early (return False).\n    self.assertIs(not_enough_timings, False)\n\n    # When not enough times, return value is None and monitor mode is not\n    # called.\n    self.assertFalse(monitor_mode_mock.called)\n\n  @mock.patch.object(ukip, 'enforce_hardening_mode', autospec=True)\n  @mock.patch.object(ukip, 'enforce_monitor_mode', autospec=True)\n  def test_check_for_attack_proper_run_mode(self, monitor_mode_mock,\n                                            hardening_mode_mock):\n    \"\"\"Tests if the proper mode is executed based on global selection.\"\"\"\n\n    # Need to access the global variable.\n    ukip._event_devices_timings[self.event_device_path] = collections.deque(\n        maxlen=ukip.KEYSTROKE_WINDOW)\n\n    # Push amount of KEYSTROKE_WINDOW times into the ringbuffer, that triggers\n    # the chosen mode.\n    ukip._event_devices_timings[self.event_device_path].append(1555146977759524)\n    ukip._event_devices_timings[self.event_device_path].append(1555146977759525)\n    ukip._event_devices_timings[self.event_device_path].append(1555146977759526)\n    ukip._event_devices_timings[self.event_device_path].append(1555146977759527)\n    ukip._event_devices_timings[self.event_device_path].append(1555146977759528)\n\n    # First test with the MONITOR mode.\n    ukip._UKIP_RUN_MODE = ukip.UKIP_AVAILABLE_MODES.MONITOR\n    ukip.check_for_attack(self.event_device_path, self.mock_pyusb_device)\n    monitor_mode_mock.assert_called_once_with(self.mock_pyusb_device,\n                                              self.event_device_path)\n\n    # Finally, test with the HARDENING mode.\n    ukip._UKIP_RUN_MODE = ukip.UKIP_AVAILABLE_MODES.HARDENING\n    ukip.check_for_attack(self.event_device_path, self.mock_pyusb_device)\n    hardening_mode_mock.assert_called_once_with(self.mock_pyusb_device,\n                                                self.event_device_path)\n\n  @mock.patch.object(ukip, 'log', autospec=True)\n  @mock.patch.object(ukip, 'enforce_hardening_mode', autospec=True)\n  @mock.patch.object(ukip, 'enforce_monitor_mode', autospec=True)\n  def test_check_for_attack_no_run_mode(self, monitor_mode_mock,\n                                        hardening_mode_mock, logging_mock):\n    \"\"\"Tests when no run mode is set.\"\"\"\n\n    # Need to access the global variable.\n    ukip._event_devices_timings[self.event_device_path] = collections.deque(\n        maxlen=ukip.KEYSTROKE_WINDOW)\n\n    # Push amount of KEYSTROKE_WINDOW times into the ringbuffer, that would\n    # trigger a chosen mode.\n    ukip._event_devices_timings[self.event_device_path].append(1555146977759524)\n    ukip._event_devices_timings[self.event_device_path].append(1555146977759525)\n    ukip._event_devices_timings[self.event_device_path].append(1555146977759526)\n    ukip._event_devices_timings[self.event_device_path].append(1555146977759527)\n    ukip._event_devices_timings[self.event_device_path].append(1555146977759528)\n\n    # Set the run mode to None.\n    ukip._UKIP_RUN_MODE = None\n    ukip.check_for_attack(self.event_device_path, self.mock_pyusb_device)\n\n    # No mode should trigger.\n    self.assertFalse(monitor_mode_mock.called)\n    self.assertFalse(hardening_mode_mock.called)\n\n    # But the error should be logged.\n    logging_mock.error.assert_called_once()\n\n  @mock.patch.object(ukip, 'check_for_attack', autospec=True)\n  def test_add_to_ring_buffer_create_key_time(self, check_for_attack_mock):\n    \"\"\"Tests the ringbuffer key creation on adding a time for the first time.\"\"\"\n\n    # At the beginning the global dict is empty.\n    self.assertFalse(ukip._event_devices_timings)\n\n    # The event_device_path wasn't present, but should be created now.\n    ukip.add_to_ring_buffer(self.event_device_path, 1555146977759524, 'x',\n                            self.mock_pyusb_device)\n\n    # Check if the key was successfully created.\n    self.assertTrue(ukip._event_devices_timings.get(self.event_device_path))\n\n    # Check if the check_for_attack function was called on the created key.\n    check_for_attack_mock.assert_called_once_with(self.event_device_path,\n                                                  self.mock_pyusb_device)\n\n  @mock.patch.object(ukip, 'check_for_attack', autospec=True)\n  def test_add_to_ring_buffer_create_key_keystroke(self, check_for_attack_mock):\n    \"\"\"Tests the ringbuffer key creation on adding an initial keystroke.\"\"\"\n\n    # At the beginning the global dict is empty.\n    self.assertFalse(ukip._event_devices_keystrokes)\n\n    # The event_device_path wasn't present, but should be created now.\n    ukip.add_to_ring_buffer(self.event_device_path, 1555146977759524, 'x',\n                            self.mock_pyusb_device)\n\n    # Check if the key was successfully created.\n    self.assertTrue(ukip._event_devices_keystrokes.get(self.event_device_path))\n\n    # Check if the check_for_attack function was called on the created key.\n    check_for_attack_mock.assert_called_once_with(self.event_device_path,\n                                                  self.mock_pyusb_device)\n\n  @mock.patch.object(ukip, 'check_for_attack', autospec=True)\n  def test_add_to_ring_buffer_multiple_values(self, check_for_attack_mock):\n    \"\"\"Tests if the ringbuffer is working correctly with the set window.\"\"\"\n\n    ukip.add_to_ring_buffer(self.event_device_path, 1555146977759524, 'a',\n                            self.mock_pyusb_device)\n\n    self.assertEqual(\n        len(ukip._event_devices_timings.get(self.event_device_path)), 1)\n\n    ukip.add_to_ring_buffer(self.event_device_path, 1555146980127487, 'b',\n                            self.mock_pyusb_device)\n\n    self.assertEqual(\n        len(ukip._event_devices_timings.get(self.event_device_path)), 2)\n\n    ukip.add_to_ring_buffer(self.event_device_path, 1555146980303490, 'c',\n                            self.mock_pyusb_device)\n\n    self.assertEqual(\n        len(ukip._event_devices_timings.get(self.event_device_path)), 3)\n\n    ukip.add_to_ring_buffer(self.event_device_path, 1555146982271470, 'd',\n                            self.mock_pyusb_device)\n\n    self.assertEqual(\n        len(ukip._event_devices_timings.get(self.event_device_path)), 4)\n\n    ukip.add_to_ring_buffer(self.event_device_path, 1555146984271470, 'e',\n                            self.mock_pyusb_device)\n\n    self.assertEqual(\n        len(ukip._event_devices_timings.get(self.event_device_path)), 5)\n\n    ukip.add_to_ring_buffer(self.event_device_path, 1555147982271470, 'f',\n                            self.mock_pyusb_device)\n\n    # Since it's a ringbuffer, the length for both dicts is still\n    # KEYSTROKE_WINDOW.\n    self.assertEqual(\n        len(ukip._event_devices_timings.get(self.event_device_path)),\n        ukip.KEYSTROKE_WINDOW)\n    self.assertEqual(\n        len(ukip._event_devices_timings.get(self.event_device_path)),\n        ukip.KEYSTROKE_WINDOW)\n\n    # The check_for_attack function was called KEYSTROKE_WINDOW + 1 times.\n    self.assertEqual(check_for_attack_mock.call_count,\n                     ukip.KEYSTROKE_WINDOW + 1)\n\n  @mock.patch.object(ukip, 'log', autospec=True)\n  def test_enforce_monitor_mode_with_product(self, logging_mock):\n    \"\"\"Tests which logging message is emitted when device has a product set.\"\"\"\n\n    self.fill_test_ringbuffer_with_data()\n\n    ukip.enforce_monitor_mode(self.mock_pyusb_device, self.event_device_path)\n\n    logging_mock.warning.assert_called_with(\n        '[UKIP] The device %s with the vendor id %s and the product'\n        ' id %s would have been blocked. The causing timings are: %s.',\n        self.mock_pyusb_device.product, hex(self.mock_pyusb_device.idVendor),\n        hex(self.mock_pyusb_device.idProduct),\n        ukip._event_devices_timings[self.event_device_path])\n\n  @mock.patch.object(ukip, 'log', autospec=True)\n  def test_enforce_monitor_mode_no_product(self, logging_mock):\n    \"\"\"Tests which logging message is emitted when device has NO product set.\"\"\"\n\n    self.fill_test_ringbuffer_with_data()\n    self.mock_pyusb_device.product = None\n\n    ukip.enforce_monitor_mode(self.mock_pyusb_device, self.event_device_path)\n\n    logging_mock.warning.assert_called_with(\n        '[UKIP] The device %s with the vendor id %s and the product'\n        ' id %s would have been blocked. The causing timings are: %s.',\n        'UNKNOWN', hex(self.mock_pyusb_device.idVendor),\n        hex(self.mock_pyusb_device.idProduct),\n        ukip._event_devices_timings[self.event_device_path])\n\n  @mock.patch.object(ukip, 'load_keycodes_from_file', autospec=True)\n  @mock.patch.object(evdev, 'InputDevice', autospec=True)\n  @mock.patch.object(usb.core, 'find', autospec=True)\n  def test_monitor_device_thread_library_calls(self, usb_core_find_mock,\n                                               input_device_mock,\n                                               load_keycodes_from_file_mock):\n    \"\"\"Tests if all the calls to the libraries are made.\"\"\"\n\n    vendor_id = int(self.pyudev_device.ID_VENDOR_ID, 16)\n    product_id = int(self.pyudev_device.ID_MODEL_ID, 16)\n\n    ukip.monitor_device_thread(self.pyudev_device, vendor_id, product_id)\n\n    load_keycodes_from_file_mock.assert_called()\n\n    input_device_mock.assert_called_once_with(self.pyudev_device.device_node)\n    usb_core_find_mock.assert_called_once_with(\n        idVendor=vendor_id, idProduct=product_id)\n\n  def test_monitor_device_thread_logging(self):\n    \"\"\"Tests the initial logging of the thread starting function.\"\"\"\n    # TODO Implement this test.\n\n  @mock.patch.object(ukip, 'load_keycodes_from_file', autospec=True)\n  @mock.patch.object(ukip, 'log', autospec=True)\n  def test_monitor_device_thread_exception_inputdevice(\n      self, logging_mock, load_keycodes_from_file_mock):\n    \"\"\"Tests exception and log message for the InputDevice creation.\"\"\"\n    log_message = ('There was an error while starting the thread for device '\n                   'monitoring: %s')\n    exception_message = '[Errno 19] No such device'\n    exception_object = OSError(exception_message)\n\n    evdev.InputDevice.side_effect = exception_object\n\n    vendor_id = int(self.pyudev_device.ID_VENDOR_ID, 16)\n    product_id = int(self.pyudev_device.ID_MODEL_ID, 16)\n\n    ukip.monitor_device_thread(self.pyudev_device, vendor_id, product_id)\n\n    load_keycodes_from_file_mock.assert_called()\n\n    logging_mock.warning.assert_called()\n\n  @mock.patch.object(ukip, 'load_keycodes_from_file', autospec=True)\n  @mock.patch.object(ukip, 'log', autospec=True)\n  def test_monitor_device_thread_exception_read_loop(\n      self, logging_mock, load_keycodes_from_file_mock):\n    \"\"\"Tests exception and log message in read_loop.\"\"\"\n    log_message = 'Events found for unbound device: %s'\n    exception_message = '[Errno 19] No such device'\n    exception_object = OSError(exception_message)\n\n    local_mock_inputdevice = mock.MagicMock()\n    evdev.InputDevice.return_value = local_mock_inputdevice\n\n    local_mock_inputdevice.read_loop.side_effect = exception_object\n\n    vendor_id = int(self.pyudev_device.ID_VENDOR_ID, 16)\n    product_id = int(self.pyudev_device.ID_MODEL_ID, 16)\n\n    ukip.monitor_device_thread(self.pyudev_device, vendor_id, product_id)\n\n    load_keycodes_from_file_mock.assert_called()\n\n    logging_mock.warning.assert_called()\n\n  def test_monitor_device_thread_keystroke_in_ms(self):\n    \"\"\"Tests if add_to_ringbuffer was called with the keystroke time in ms.\"\"\"\n    # TODO Implement this test.\n\n  def test_monitor_device_thread_keystroke_shift(self):\n    \"\"\"Tests if add_to_ringbuffer was called with the upper case keystroke.\"\"\"\n    # TODO Implement this test.\n\n  def test_monitor_device_thread_keystroke_capslock(self):\n    \"\"\"Tests if add_to_ringbuffer was called with the upper case keystroke.\"\"\"\n    # TODO Implement this test.\n\n  @mock.patch.object(pyudev, 'Context', autospec=True)\n  @mock.patch.object(pyudev.Monitor, 'from_netlink', autospec=True)\n  def test_init_device_list_library_calls(self, netlink_mock, context_mock):\n    \"\"\"Tests if the initial library calls are made.\"\"\"\n\n    ukip.init_device_list()\n\n    self.assertEqual(context_mock.call_count, 1)\n    self.assertEqual(netlink_mock.call_count, 1)\n\n  def test_init_device_list_exceptions(self):\n    \"\"\"Tests if exceptions were raised (ValueError and DeviceError).\"\"\"\n    # TODO Implement this test.\n\n  def test_init_device_list_device_count(self):\n    \"\"\"Tests if the number of devices is increased when iterating.\"\"\"\n    # TODO Implement this test.\n\n  def test_init_device_list_invalid_pid_vid(self):\n    \"\"\"Tests if a ValueError is raised, when the VID/PID cannot be converted.\"\"\"\n    # TODO Implement this test.\n\n  def test_init_device_list_runtimeerror(self):\n    \"\"\"Tests if the RuntimeError is thrown, when the thread failed to start.\"\"\"\n    # TODO Implement this test.\n\n  def test_main_threading(self):\n    \"\"\"Tests if the thread was started.\"\"\"\n    # TODO Implement this test.\n\n  def test_main_too_many_arguments(self):\n    \"\"\"Tests if no arguments were provided to main.\"\"\"\n    # TODO Implement this test.\n\n  @mock.patch.object(pyudev.Monitor, 'from_netlink', autospec=True)\n  def test_main_filter_by(self, netlink_mock):\n    \"\"\"Tests if the monitor filter_by was actually called.\"\"\"\n\n    monitor_mock = mock.MagicMock()\n    pyudev.Monitor.from_netlink.return_value = monitor_mock\n    monitor_mock.poll.side_effect = [self.pyudev_device, None]\n    netlink_mock.return_value = monitor_mock\n\n    ukip.main(['ukip.py'])\n\n    calls = [mock.call(subsystem='input'), mock.call(subsystem='input')]\n    monitor_mock.filter_by.assert_has_calls(calls)\n\n  @mock.patch.object(builtins, 'open', autospec=True)\n  def test_check_local_allowlist(self, open_mock):\n    \"\"\"Tests if the local allowlist check returns the allowlist on success.\"\"\"\n\n    open_mock.return_value.__enter__ = open_mock\n\n    # Prepare a fake file, that looks similar to the actual file.\n    open_mock.return_value.__iter__.return_value = iter([\n        '# This is the config file\\n', '# for UKIP.\\n',\n        '0x3784:0x3472 a,b,c\\n'\n    ])\n\n    # Call with a PID and VID that will be found.\n    allowlist = ukip.check_local_allowlist('0x3784', '0x3472')\n\n    # If the PID and VID are found, the function returns the allowlist.\n    self.assertEqual(\n        allowlist,\n        ukip.AllowlistConfigReturn(\n            allowlist=['a', 'b', 'c'], device_present=True))\n\n  @mock.patch.object(builtins, 'open', autospec=True)\n  def test_check_local_allowlist_two_devices(self, open_mock):\n    \"\"\"Tests if the local allowlist with two devices, where one matches.\"\"\"\n\n    open_mock.return_value.__enter__ = open_mock\n\n    # Prepare a fake file, that looks similar to the actual file.\n    open_mock.return_value.__iter__.return_value = iter([\n        '# This is the config file\\n', '# for UKIP.\\n',\n        '0x1337:0x1234 x,y,z\\n', '0x3784:0x3472 a,b,c\\n'\n    ])\n\n    # Call with a PID and VID that will be found.\n    allowlist = ukip.check_local_allowlist('0x3784', '0x3472')\n\n    # If the PID and VID are found, the function returns the allowlist.\n    self.assertEqual(\n        allowlist,\n        ukip.AllowlistConfigReturn(\n            allowlist=['a', 'b', 'c'], device_present=True))\n\n  @mock.patch.object(builtins, 'open', autospec=True)\n  def test_check_local_allowlist_only_comments(self, open_mock):\n    \"\"\"Tests if the local allowlist check returns False when only comments.\"\"\"\n\n    open_mock.return_value.__enter__ = open_mock\n\n    # Prepare a fake file, with only comments.\n    open_mock.return_value.__iter__.return_value = iter([\n        '# This is the config file\\n', '# for UKIP.\\n',\n        '# One more comment line.\\n'\n    ])\n\n    # Lookup for a PID and VID.\n    allowlist = ukip.check_local_allowlist('0x3784', '0x3472')\n\n    # If there are only comment in the config file, return False.\n    self.assertEqual(\n        allowlist,\n        ukip.AllowlistConfigReturn(allowlist=[], device_present=False))\n\n  @mock.patch.object(builtins, 'open', autospec=True)\n  def test_check_local_allowlist_no_device(self, open_mock):\n    \"\"\"Tests if the allowlist check returns False when device not in file.\"\"\"\n\n    open_mock.return_value.__enter__ = open_mock\n\n    open_mock.return_value.__iter__.return_value = iter([\n        '# This is the config file\\n', '# for UKIP.\\n',\n        '0x3784:0x3472 a,b,c\\n'\n    ])\n\n    # Lookup for a PID and VID which are not in the config file.\n    allowlist = ukip.check_local_allowlist('0x1234', '0x3472')\n\n    # If the device cannot be found in the config file, return False.\n    self.assertEqual(\n        allowlist,\n        ukip.AllowlistConfigReturn(allowlist=[], device_present=False))\n\n  @mock.patch.object(builtins, 'open', autospec=True)\n  def test_check_local_allowlist_key_val_parsing(self, open_mock):\n    \"\"\"Tests if the config file could be parsed into keys and values.\"\"\"\n\n    open_mock.return_value.__enter__ = open_mock\n    open_mock.return_value.__iter__.return_value = iter([\n        '# This is the config file\\n', '# for UKIP.\\n',\n        'cannotparse\\n'\n    ])\n\n    # Check if the exception was raised.\n    self.assertRaises(ukip.AllowlistFileError, ukip.check_local_allowlist,\n                      '0x1234', '0x3472')\n\n  @mock.patch.object(builtins, 'open', autospec=True)\n  def test_check_local_allowlist_device_parsing(self, open_mock):\n    \"\"\"Tests if the device in the config file can be parsed.\"\"\"\n\n    open_mock.return_value.__enter__ = open_mock\n    open_mock.return_value.__iter__.return_value = iter([\n        '# This is the config file\\n', '# for UKIP.\\n',\n        '37843472 a,b,c\\n'\n    ])\n\n    self.assertRaises(ukip.AllowlistFileError, ukip.check_local_allowlist,\n                      '0x3784', '0x3472')\n\n  @mock.patch.object(builtins, 'open', autospec=True)\n  def test_check_local_allowlist_parsing(self, open_mock):\n    \"\"\"Tests if allowlist could be parsed from the config file.\"\"\"\n\n    open_mock.return_value.__enter__ = open_mock\n    open_mock.return_value.__iter__.return_value = iter([\n        '# This is the config file\\n', '# for UKIP.\\n',\n        '0x3784:0x3472 cannotparse\\n'\n    ])\n\n    # The device will be found, but the allowlist cannot be parsed.\n    allowlist = ukip.check_local_allowlist('0x3784', '0x3472')\n\n    # If the allowlist is a word, that is not 'any' or 'none', return False.\n    self.assertEqual(\n        allowlist,\n        ukip.AllowlistConfigReturn(allowlist=[], device_present=False))\n\n  @mock.patch.object(builtins, 'open', autospec=True)\n  def test_check_local_allowlist_file_not_found(self, open_mock):\n    \"\"\"Tests if the config file could be found.\"\"\"\n\n    open_mock.side_effect = ukip.AllowlistFileError(\n        'The config file /etc/ukip/allowlist could not be found: %s')\n\n    self.assertRaises(ukip.AllowlistFileError, ukip.check_local_allowlist,\n                      '0x3784', '0x3472')\n\n  @mock.patch.object(builtins, 'open', autospec=True)\n  def test_check_local_allowlist_empty_lines(self, open_mock):\n    \"\"\"Tests if the allowlist check returns False when only empty lines.\"\"\"\n\n    open_mock.return_value.__enter__ = open_mock\n\n    # Prepare a fake file, with only empty lines.\n    open_mock.return_value.__iter__.return_value = iter(\n        ['\\n', '    \\n', '        \\n'])\n\n    # Lookup for a PID and VID.\n    allowlist = ukip.check_local_allowlist('0x3784', '0x3472')\n\n    # If there are only empty lines in the config file, return False.\n    self.assertEqual(\n        allowlist,\n        ukip.AllowlistConfigReturn(allowlist=[], device_present=False))\n\n  @mock.patch.object(builtins, 'open', autospec=True)\n  def test_check_local_allowlist_allow_all(self, open_mock):\n    \"\"\"Tests if the allowlist check returns True for \"allow all characters\".\"\"\"\n\n    open_mock.return_value.__enter__ = open_mock\n\n    # Prepare a fake file, with only empty lines.\n    open_mock.return_value.__iter__.return_value = iter([\n        '0x1234:0x1337 any\\n',\n    ])\n\n    # Lookup for a PID and VID.\n    allowlist = ukip.check_local_allowlist('0x1234', '0x1337')\n\n    # If all possible characters are allowed for a device, return an empty list\n    # and True.\n    self.assertEqual(\n        allowlist,\n        ukip.AllowlistConfigReturn(allowlist=[], device_present=True))\n\n  @mock.patch.object(builtins, 'open', autospec=True)\n  def test_check_local_allowlist_deny_all(self, open_mock):\n    \"\"\"Tests if the allowlist is an empty list when denying all characters.\"\"\"\n\n    open_mock.return_value.__enter__ = open_mock\n\n    # Prepare a fake file, with only empty lines.\n    open_mock.return_value.__iter__.return_value = iter([\n        '0x1234:0x1337 none\\n',\n    ])\n\n    # Lookup for a PID and VID.\n    allowlist = ukip.check_local_allowlist('0x1234', '0x1337')\n\n    # If no characters are allowed for the given device, return an empty list.\n    self.assertEqual(\n        allowlist,\n        ukip.AllowlistConfigReturn(allowlist=[], device_present=False))\n\n  def fill_test_ringbuffer_with_data(self):\n    \"\"\"A helper function to add times and trigger the hardening mode.\"\"\"\n    ukip.add_to_ring_buffer(self.event_device_path, 1555146977759524, 'a',\n                            self.mock_pyusb_device)\n    ukip.add_to_ring_buffer(self.event_device_path, 1555146977859525, 'b',\n                            self.mock_pyusb_device)\n    ukip.add_to_ring_buffer(self.event_device_path, 1555146977959526, 'c',\n                            self.mock_pyusb_device)\n    ukip.add_to_ring_buffer(self.event_device_path, 1555146977959527, 'd',\n                            self.mock_pyusb_device)\n    ukip.add_to_ring_buffer(self.event_device_path, 1555146977959528, 'e',\n                            self.mock_pyusb_device)\n\n  @mock.patch.object(gc, 'collect', wraps=gc.collect)\n  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)\n  @mock.patch.object(ukip, 'log', autospec=True)\n  def test_enforce_hardening_mode_with_product(self, logging_mock,\n                                               check_allowlist_mock, gc_mock):\n    \"\"\"Tests which logging message is emitted when device has a product set.\"\"\"\n\n    self.fill_test_ringbuffer_with_data()\n\n    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])\n\n    # Need a link, because after the function is run, the dicts are deleted.\n    timings = ukip._event_devices_timings[self.event_device_path]\n\n    # Return the allowlist from /etc/ukip/allowlist.\n    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(\n        allowlist=['a', 'b', 'c'], device_present=True)\n\n    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)\n\n    check_allowlist_mock.assert_called_once_with(\n        hex(self.mock_pyusb_device.idProduct),\n        hex(self.mock_pyusb_device.idVendor))\n\n    # Only 1 interface, so the range is 0.\n    self.mock_pyusb_device.detach_kernel_driver.assert_called_once_with(0)\n\n    logging_mock.warning.assert_called_with(\n        '[UKIP] The device %s with the vendor id %s and the product id %s '\n        'was blocked. The causing timings were: %s.',\n        self.mock_pyusb_device.product, hex(self.mock_pyusb_device.idVendor),\n        hex(self.mock_pyusb_device.idProduct), timings)\n\n    # The error was not logged.\n    self.assertFalse(logging_mock.error.called)\n\n    # The dicts are deleted now.\n    self.assertFalse(ukip._event_devices_timings)\n    self.assertFalse(ukip._event_devices_keystrokes)\n\n    # And the garbage collector ran.\n    gc_mock.assert_called_once()\n\n  @mock.patch.object(gc, 'collect', wraps=gc.collect)\n  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)\n  @mock.patch.object(ukip, 'log', autospec=True)\n  def test_enforce_hardening_mode_no_product(self, logging_mock,\n                                             check_allowlist_mock, gc_mock):\n    \"\"\"Tests which logging message is emitted when device has no product set.\"\"\"\n\n    self.fill_test_ringbuffer_with_data()\n\n    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])\n    self.mock_pyusb_device.product = None\n\n    # Need a link, because after the function is run, the dicts are deleted.\n    timings = ukip._event_devices_timings[self.event_device_path]\n\n    # Return the allowlist from /etc/ukip/allowlist.\n    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(\n        allowlist=['a', 'b', 'c'], device_present=True)\n\n    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)\n\n    check_allowlist_mock.assert_called_once_with(\n        hex(self.mock_pyusb_device.idProduct),\n        hex(self.mock_pyusb_device.idVendor))\n\n    # Only 1 interface, so the range is 0.\n    self.mock_pyusb_device.detach_kernel_driver.assert_called_once_with(0)\n\n    logging_mock.warning.assert_called_with(\n        '[UKIP] The device with the vendor id %s and the product id %s was '\n        'blocked. The causing timings were: %s.',\n        hex(self.mock_pyusb_device.idVendor),\n        hex(self.mock_pyusb_device.idProduct), timings)\n\n    self.assertFalse(logging_mock.error.called)\n\n    # The dicts are deleted now.\n    self.assertFalse(ukip._event_devices_timings)\n    self.assertFalse(ukip._event_devices_keystrokes)\n\n    # And the garbage collector ran.\n    gc_mock.assert_called_once()\n\n  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)\n  @mock.patch.object(ukip, 'log', autospec=True)\n  def test_enforce_hardening_mode_no_active_driver(self, logging_mock,\n                                                   check_allowlist_mock):\n    \"\"\"Tests flow through function when no interface has an active driver.\"\"\"\n\n    self.fill_test_ringbuffer_with_data()\n\n    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])\n    self.mock_pyusb_device.is_kernel_driver_active.return_value = False\n\n    # Return the allowlist from /etc/ukip/allowlist.\n    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(\n        allowlist=['a', 'b', 'c'], device_present=True)\n\n    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)\n\n    check_allowlist_mock.assert_called_once_with(\n        hex(self.mock_pyusb_device.idProduct),\n        hex(self.mock_pyusb_device.idVendor))\n\n    self.assertFalse(self.mock_pyusb_device.detach_kernel_driver.called)\n    self.assertFalse(logging_mock.warning.called)\n    self.assertFalse(logging_mock.error.called)\n\n  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)\n  @mock.patch.object(ukip, 'log', autospec=True)\n  def test_enforce_hardening_mode_ioerror(self, logging_mock,\n                                          check_allowlist_mock):\n    \"\"\"Tests IOError/log message for unbinding a driver from an interface.\"\"\"\n\n    self.fill_test_ringbuffer_with_data()\n\n    log_message = ('There was an error in unbinding the interface for the USB '\n                   'device %s: %s')\n    exception_message = '[Errno 16] Device or resource busy'\n    exception_object = IOError(exception_message)\n\n    product_id = hex(self.mock_pyusb_device.idProduct)\n    vendor_id = hex(self.mock_pyusb_device.idVendor)\n    pid_and_vid = '%s:%s' % (product_id, vendor_id)\n\n    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])\n    self.mock_pyusb_device.detach_kernel_driver.side_effect = exception_object\n\n    # Return the allowlist from /etc/ukip/allowlist.\n    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(\n        allowlist=['a', 'b', 'c'], device_present=True)\n\n    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)\n\n    check_allowlist_mock.assert_called_once_with(\n        hex(self.mock_pyusb_device.idProduct),\n        hex(self.mock_pyusb_device.idVendor))\n\n    logging_mock.warning.assert_called()\n\n  @mock.patch.object(gc, 'collect', wraps=gc.collect)\n  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)\n  @mock.patch.object(ukip, 'log', autospec=True)\n  def test_enforce_hardening_mode_multiple_interfaces_error(\n      self, logging_mock, check_allowlist_mock, gc_mock):\n    \"\"\"Tests multiple interfaces, with one failing with an IOError.\"\"\"\n\n    self.fill_test_ringbuffer_with_data()\n\n    log_message = ('There was an error in unbinding the interface for the USB '\n                   'device %s: %s')\n    exception_message = '[Errno 16] Device or resource busy'\n    exception_object = IOError(exception_message)\n\n    product_id = hex(self.mock_pyusb_device.idProduct)\n    vendor_id = hex(self.mock_pyusb_device.idVendor)\n    pid_and_vid = '%s:%s' % (product_id, vendor_id)\n\n    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])\n    self.mock_usb_config.bNumInterfaces = 2\n\n    self.mock_pyusb_device.detach_kernel_driver.side_effect = [\n        exception_object, mock.DEFAULT\n    ]\n\n    # Need a link, because after the function is run, the dicts are deleted.\n    timings = ukip._event_devices_timings[self.event_device_path]\n\n    # Return the allowlist from /etc/ukip/allowlist.\n    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(\n        allowlist=['a', 'b', 'c'], device_present=True)\n\n    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)\n\n    check_allowlist_mock.assert_called_once_with(\n        hex(self.mock_pyusb_device.idProduct),\n        hex(self.mock_pyusb_device.idVendor))\n\n    call = [\n        mock.call(\n            '[UKIP] The device %s with the vendor id %s and the product id '\n            '%s was blocked. The causing timings were: %s.',\n            self.mock_pyusb_device.product,\n            hex(self.mock_pyusb_device.idVendor),\n            hex(self.mock_pyusb_device.idProduct), timings)\n    ]\n    logging_mock.warning.assert_has_calls(call)\n\n    # The dicts are deleted now.\n    self.assertFalse(ukip._event_devices_timings)\n    self.assertFalse(ukip._event_devices_keystrokes)\n\n    # And the garbage collector ran.\n    gc_mock.assert_called_once()\n\n  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)\n  @mock.patch.object(ukip, 'log', autospec=True)\n  def test_enforce_hardening_mode_oserror(self, logging_mock,\n                                          check_allowlist_mock):\n    \"\"\"Tests OSError/log message for unbinding a driver from an interface.\"\"\"\n\n    self.fill_test_ringbuffer_with_data()\n\n    log_message = ('There was an error in unbinding the interface for the USB '\n                   'device %s: %s')\n    exception_message = 'access violation'\n    exception_object = OSError(exception_message)\n\n    product_id = hex(self.mock_pyusb_device.idProduct)\n    vendor_id = hex(self.mock_pyusb_device.idVendor)\n    pid_and_vid = '%s:%s' % (product_id, vendor_id)\n\n    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])\n    self.mock_pyusb_device.detach_kernel_driver.side_effect = exception_object\n\n    # Return the allowlist from /etc/ukip/allowlist.\n    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(\n        allowlist=['a', 'b', 'c'], device_present=True)\n\n    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)\n\n    check_allowlist_mock.assert_called_once_with(\n        hex(self.mock_pyusb_device.idProduct),\n        hex(self.mock_pyusb_device.idVendor))\n\n    logging_mock.warning.assert_called()\n\n  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)\n  @mock.patch.object(ukip, 'log', autospec=True)\n  def test_enforce_hardening_mode_valueerror(self, logging_mock,\n                                             check_allowlist_mock):\n    \"\"\"Tests ValueError/log message for unbinding a driver from an interface.\"\"\"\n\n    self.fill_test_ringbuffer_with_data()\n\n    log_message = ('There was an error in unbinding the interface for the USB '\n                   'device %s: %s')\n    exception_message = 'Invalid configuration'\n    exception_object = ValueError(exception_message)\n\n    product_id = hex(self.mock_pyusb_device.idProduct)\n    vendor_id = hex(self.mock_pyusb_device.idVendor)\n    pid_and_vid = '%s:%s' % (product_id, vendor_id)\n\n    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])\n    self.mock_pyusb_device.detach_kernel_driver.side_effect = exception_object\n\n    # Return the allowlist from /etc/ukip/allowlist.\n    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(\n        allowlist=['a', 'b', 'c'], device_present=True)\n\n    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)\n\n    check_allowlist_mock.assert_called_once_with(\n        hex(self.mock_pyusb_device.idProduct),\n        hex(self.mock_pyusb_device.idVendor))\n\n    logging_mock.warning.assert_called()\n\n  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)\n  @mock.patch.object(ukip, 'log', autospec=True)\n  def test_enforce_hardening_mode_usberror(self, logging_mock,\n                                           check_allowlist_mock):\n    \"\"\"Tests USBError/log message for unbinding a driver from an interface.\"\"\"\n\n    self.fill_test_ringbuffer_with_data()\n\n    log_message = ('There was an error in unbinding the interface for the USB '\n                   'device %s: %s')\n    exception_message = 'USBError Accessing Configurations'\n    exception_object = usb.core.USBError(exception_message)\n\n    product_id = hex(self.mock_pyusb_device.idProduct)\n    vendor_id = hex(self.mock_pyusb_device.idVendor)\n    pid_and_vid = '%s:%s' % (product_id, vendor_id)\n\n    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])\n    self.mock_pyusb_device.detach_kernel_driver.side_effect = exception_object\n\n    # Return the allowlist from /etc/ukip/allowlist.\n    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(\n        allowlist=['a', 'b', 'c'], device_present=True)\n\n    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)\n\n    check_allowlist_mock.assert_called_once_with(\n        hex(self.mock_pyusb_device.idProduct),\n        hex(self.mock_pyusb_device.idVendor))\n\n    logging_mock.warning.assert_called()\n\n  @mock.patch.object(gc, 'collect', wraps=gc.collect)\n  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)\n  @mock.patch.object(ukip, 'log', autospec=True)\n  def test_enforce_hardening_mode_any_keyword(self, logging_mock,\n                                              check_allowlist_mock, gc_mock):\n    \"\"\"Tests an early return if the any keyword is set in the allowlist.\"\"\"\n\n    self.fill_test_ringbuffer_with_data()\n\n    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])\n\n    # Device present and empty allowlist -> any keyword was set.\n    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(\n        allowlist=[], device_present=True)\n\n    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)\n\n    check_allowlist_mock.assert_called_once_with(\n        hex(self.mock_pyusb_device.idProduct),\n        hex(self.mock_pyusb_device.idVendor))\n\n    # Due to the early return, none of the followup functions are called.\n    self.assertFalse(self.mock_pyusb_device.detach_kernel_driver.called)\n    self.assertFalse(logging_mock.called)\n    self.assertFalse(gc_mock.called)\n\n  @mock.patch.object(gc, 'collect', wraps=gc.collect)\n  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)\n  @mock.patch.object(ukip, 'log', autospec=True)\n  def test_enforce_hardening_mode_keystrokes_allowed(self, logging_mock,\n                                                     check_allowlist_mock,\n                                                     gc_mock):\n    \"\"\"Tests an early return if the typed keys are allowed in the allowlist.\"\"\"\n\n    # This sets the typed keys to [a,b,c,d,e]\n    self.fill_test_ringbuffer_with_data()\n\n    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])\n\n    # Device present and allowlist set to typed characters.\n    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(\n        allowlist=['a', 'b', 'c', 'd', 'e'], device_present=True)\n\n    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)\n\n    check_allowlist_mock.assert_called_once_with(\n        hex(self.mock_pyusb_device.idProduct),\n        hex(self.mock_pyusb_device.idVendor))\n\n    # Due to the early return, none of the followup functions are called.\n    self.assertFalse(self.mock_pyusb_device.detach_kernel_driver.called)\n    self.assertFalse(logging_mock.called)\n    self.assertFalse(gc_mock.called)\n\n  @mock.patch.object(gc, 'collect', wraps=gc.collect)\n  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)\n  @mock.patch.object(ukip, 'log', autospec=True)\n  def test_enforce_hardening_mode_keystrokes_allowed_subset(\n      self, logging_mock, check_allowlist_mock, gc_mock):\n    \"\"\"Tests an early return with a subset of allowed keys.\"\"\"\n\n    ukip.add_to_ring_buffer(self.event_device_path, 1555146977759524, 'a',\n                            self.mock_pyusb_device)\n    ukip.add_to_ring_buffer(self.event_device_path, 1555146977859525, 'b',\n                            self.mock_pyusb_device)\n    ukip.add_to_ring_buffer(self.event_device_path, 1555146977959526, 'c',\n                            self.mock_pyusb_device)\n\n    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])\n\n    # Device present and allowlist set to typed characters.\n    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(\n        allowlist=['a', 'b', 'c', 'd', 'e', 'f'], device_present=True)\n\n    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)\n\n    check_allowlist_mock.assert_called_once_with(\n        hex(self.mock_pyusb_device.idProduct),\n        hex(self.mock_pyusb_device.idVendor))\n\n    # Due to the early return, none of the followup functions are called.\n    self.assertFalse(self.mock_pyusb_device.detach_kernel_driver.called)\n    self.assertFalse(logging_mock.called)\n    self.assertFalse(gc_mock.called)\n\n  @mock.patch.object(gc, 'collect', wraps=gc.collect)\n  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)\n  @mock.patch.object(ukip, 'log', autospec=True)\n  def test_enforce_hardening_mode_device_not_present(self, logging_mock,\n                                                     check_allowlist_mock,\n                                                     gc_mock):\n    \"\"\"Tests function flow when the device is not present in the allowlist.\"\"\"\n\n    self.fill_test_ringbuffer_with_data()\n\n    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])\n\n    # Need a link, because after the function is run, the dicts are deleted.\n    timings = ukip._event_devices_timings[self.event_device_path]\n\n    # Return the allowlist from /etc/ukip/allowlist.\n    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(\n        allowlist=[], device_present=False)\n\n    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)\n\n    check_allowlist_mock.assert_called_once_with(\n        hex(self.mock_pyusb_device.idProduct),\n        hex(self.mock_pyusb_device.idVendor))\n\n    # Only 1 interface, so the range is 0.\n    self.mock_pyusb_device.detach_kernel_driver.assert_called_once_with(0)\n\n    logging_mock.warning.assert_called_with(\n        '[UKIP] The device %s with the vendor id %s and the product id %s '\n        'was blocked. The causing timings were: %s.',\n        self.mock_pyusb_device.product, hex(self.mock_pyusb_device.idVendor),\n        hex(self.mock_pyusb_device.idProduct), timings)\n\n    # The error was not logged.\n    self.assertFalse(logging_mock.error.called)\n\n    # The dicts are deleted now.\n    self.assertFalse(ukip._event_devices_timings)\n    self.assertFalse(ukip._event_devices_keystrokes)\n\n    # And the garbage collector ran.\n    gc_mock.assert_called_once()\n\n  @mock.patch.object(gc, 'collect', wraps=gc.collect)\n  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)\n  @mock.patch.object(ukip, 'log', autospec=True)\n  def test_enforce_hardening_mode_one_key_off(self, logging_mock,\n                                              check_allowlist_mock, gc_mock):\n    \"\"\"Tests the hardening mode when one typed key is not allowed.\"\"\"\n\n    # This sets the typed keys to [a,b,c,d,e]\n    self.fill_test_ringbuffer_with_data()\n\n    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])\n\n    # Need a link, because after the function is run, the dicts are deleted.\n    timings = ukip._event_devices_timings[self.event_device_path]\n\n    # Return the allowlist from /etc/ukip/allowlist. The 'e' from the typed\n    # keys is not allowed.\n    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(\n        allowlist=['a', 'b', 'c', 'd', 'f'], device_present=False)\n\n    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)\n\n    check_allowlist_mock.assert_called_once_with(\n        hex(self.mock_pyusb_device.idProduct),\n        hex(self.mock_pyusb_device.idVendor))\n\n    # Only 1 interface, so the range is 0.\n    self.mock_pyusb_device.detach_kernel_driver.assert_called_once_with(0)\n\n    logging_mock.warning.assert_called_with(\n        '[UKIP] The device %s with the vendor id %s and the product id %s '\n        'was blocked. The causing timings were: %s.',\n        self.mock_pyusb_device.product, hex(self.mock_pyusb_device.idVendor),\n        hex(self.mock_pyusb_device.idProduct), timings)\n\n    # The error was not logged.\n    self.assertFalse(logging_mock.error.called)\n\n    # The dicts are deleted now.\n    self.assertFalse(ukip._event_devices_timings)\n    self.assertFalse(ukip._event_devices_keystrokes)\n\n    # And the garbage collector ran.\n    gc_mock.assert_called_once()\n\n  @mock.patch.object(ukip, 'log', autospec=True)\n  @mock.patch.object(builtins, 'open')\n  def test_load_keycodes_from_file(self, open_mock, logging_mock):\n    \"\"\"Tests if the keycode file returns the KeycodesReturn class.\"\"\"\n\n    handle = open_mock().__enter__.return_value\n\n    keycode_file_content = [{\n        'lowcodes': [{\n            '1': 'ESC',\n            '2': '1'\n        }],\n        'capscodes': [{\n            '1': 'ESC',\n            '2': '!'\n        }]\n    }]\n\n    file_mock = mock.MagicMock(side_effect=keycode_file_content)\n    json_mock = mock.patch('json.load', file_mock)\n\n    with open_mock:\n      with json_mock as json_load_mock:\n        keycodes = ukip.load_keycodes_from_file()\n        json_load_mock.assert_called_with(handle)\n\n    self.assertEqual(keycodes.lower_codes, {1: 'ESC', 2: '1'})\n    self.assertEqual(keycodes.capped_codes, {1: 'ESC', 2: '!'})\n    logging_mock.assert_not_called()\n\n  @mock.patch.object(ukip, 'log', autospec=True)\n  @mock.patch.object(builtins, 'open')\n  def test_load_keycodes_from_file_missing_keyword(self, open_mock,\n                                                   logging_mock):\n    \"\"\"Tests the keycode file returns when a keyword is missing.\"\"\"\n\n    handle = open_mock().__enter__.return_value\n\n    keycode_file_content = [{\n        'not_low_codes': [{\n            '1': 'ESC',\n            '2': '1'\n        }],\n        'capscodes': [{\n            '1': 'ESC',\n            '2': '!'\n        }]\n    }]\n\n    file_mock = mock.MagicMock(side_effect=keycode_file_content)\n    json_mock = mock.patch('json.load', file_mock)\n\n    with open_mock:\n      with json_mock as json_load_mock:\n        keycodes = ukip.load_keycodes_from_file()\n        json_load_mock.assert_called_with(handle)\n\n    # The lowcodes keyword is missing in the keycodes file.\n    self.assertEqual(keycodes.lower_codes, {})\n    self.assertEqual(keycodes.capped_codes, {})\n    logging_mock.error.assert_called()\n\n  @mock.patch.object(ukip, 'log', autospec=True)\n  @mock.patch.object(json, 'load', autospec=True)\n  @mock.patch.object(builtins, 'open', autospec=True)\n  def test_load_keycodes_from_file_overflowerror(self, open_mock, json_mock,\n                                                 logging_mock):\n    \"\"\"Tests if KeycodesFileError is raised on an OverflowError.\"\"\"\n\n    json_mock.side_effect = OverflowError\n    self.assertRaises(ukip.KeycodesFileError, ukip.load_keycodes_from_file)\n    open_mock.assert_called()\n    json_mock.assert_called()\n    logging_mock.assert_not_called()\n\n  @mock.patch.object(ukip, 'log', autospec=True)\n  @mock.patch.object(json, 'load', autospec=True)\n  @mock.patch.object(builtins, 'open', autospec=True)\n  def test_load_keycodes_from_file_valueerror(self, open_mock, json_mock,\n                                              logging_mock):\n    \"\"\"Tests if KeycodesFileError is raised on a ValueError.\"\"\"\n\n    json_mock.side_effect = ValueError\n    self.assertRaises(ukip.KeycodesFileError, ukip.load_keycodes_from_file)\n    open_mock.assert_called()\n    json_mock.assert_called()\n    logging_mock.assert_not_called()\n\n  @mock.patch.object(ukip, 'log', autospec=True)\n  @mock.patch.object(json, 'load', autospec=True)\n  @mock.patch.object(builtins, 'open', autospec=True)\n  def test_load_keycodes_from_file_typeerror(self, open_mock, json_mock,\n                                             logging_mock):\n    \"\"\"Tests if KeycodesFileError is raised on a TypeError.\"\"\"\n\n    json_mock.side_effect = TypeError\n    self.assertRaises(ukip.KeycodesFileError, ukip.load_keycodes_from_file)\n    open_mock.assert_called()\n    json_mock.assert_called()\n    logging_mock.assert_not_called()\n\n  @mock.patch.object(ukip, 'log', autospec=True)\n  @mock.patch.object(json, 'load', autospec=True)\n  @mock.patch.object(builtins, 'open', autospec=True)\n  def test_load_keycodes_from_file_not_found(self, open_mock, json_mock,\n                                             logging_mock):\n    \"\"\"Tests if KeycodesFileError is raised on a FileNotFoundError.\"\"\"\n\n    json_mock.side_effect = FileNotFoundError\n    self.assertRaises(ukip.KeycodesFileError, ukip.load_keycodes_from_file)\n    open_mock.assert_called()\n    json_mock.assert_called()\n    logging_mock.assert_not_called()\n\n\nif __name__ == '__main__':\n  unittest.main()\n"
  }
]