Full Code of google/ukip for AI

master c88410eacfe8 cached
11 files
94.3 KB
23.2k tokens
74 symbols
1 requests
Download .txt
Repository: google/ukip
Branch: master
Commit: c88410eacfe8
Files: 11
Total size: 94.3 KB

Directory structure:
gitextract_f7kggwew/

├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── data/
│   ├── allowlist
│   ├── keycodes
│   └── ukip.service
├── requirements.txt
├── setup.sh
└── src/
    ├── __init__.py
    ├── ukip.py
    └── ukip_test.py

================================================
FILE CONTENTS
================================================

================================================
FILE: CONTRIBUTING.md
================================================
# How to Contribute

We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.

## Contributor License Agreement

Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution;
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to <https://cla.developers.google.com/> to see
your current agreements on file or to sign a new one.

You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.

## Code reviews

All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.

## Community Guidelines

This project follows
[Google's Open Source Community Guidelines](https://opensource.google/conduct/).


================================================
FILE: LICENSE
================================================

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
# USB Keystroke Injection Protection
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)

## Overview
This tool is a daemon for blocking USB keystroke injection devices on Linux systems.

It supports two different modes of operation: **monitoring** and **hardening**. In
monitor mode, information about a potentially attacking USB device is collected
and logged to syslog. In hardening mode, the attacking USB device is ejected
from the operating system by unbinding the driver.

### Installation Prerequisites
The installation is mainly handled by `setup.sh`, however, there are some prerequisites 
that need to be adjusted before running the script:

1) Install Python3.7 or later, python dev package, virtualenv (`python3-venv`) and PIP3 (`python3-pip`) if not already 
available on the system.

1) Adjust the `KEYSTROKE_WINDOW` variable on top of the `setup.sh` file. This is the 
number of keystrokes the daemon looks at to determine whether its dealing with an attack or not. 
The lower the number, the higher the false positives will be (e.g., if the number is 2, the tool 
looks at only 1 interarrival time between those two keystrokes to determine whether it's an 
attack or not. Obviously, users sometimes hit two keys almost at the same time, which leads 
to the aforementioned false positive). Based on our internal observations, 5 is a value that 
is effective. However, it should be adjusted based on specific users' experiences and typing 
behaviour.

1) Adjust the `ABNORMAL_TYPING` variable on top of the `setup.sh` file. This variable 
specifies what interarrival time (between two keystrokes) should be classified as malicious. 
The higher the number, the more false-positives will arise (normal typing speed will be 
classified as malicious), where more false-negatives will arise with a lower number (even very 
fast typing attacks will be classified as benign). That said, the preset `50000` after initial 
installation is a safe default but should be changed to a number reflecting the typing speed of 
the user using the tool.

1) Set the mode the daemon should run in by adjusting the `RUN_MODE` variable on top of the 
`setup.sh` file. Setting it to `MONITOR` will send information about the USB device to a logging 
instance without blocking the device. Setting the variable to `HARDENING` will remove an 
attacking device from the system by unbinding the driver.

1) Adjust the `DEBIAN` variable on top of the `setup.sh` file. This variable indicates 
whether the system the tool is installed on is a Debian derivate or something else. This determination 
is important for the installation of the systemd service later on (the path, the service will be 
copied to).

1) Adjust the allowlist file in `data/allowlist`. This file will be installed to `/etc/ukip/` 
on your system and taken as source of truth for allowed devices, in case a device is 
exceeding the preset `ABNORMAL_TYPING` speed. As described in the file, the allowed device 
can be narrowed down with a specific set of characters to allow to even more minimize the attack 
surface. For example, if your keyboard uses a macro that sends `rm -rf /` allow those characters, 
and even an attacking device spoofing your keyboards product ID and vendor ID couldn't inject an 
attack (except an attack using those specific characters obviously :D ). For other cases, the 
`any` keyword allows all possible characters for a specified device and `none` disallows 
all characters. Please keep in mind that this allowlist will only be taken into consideration, if
a device is exceeding the set threshold.  

1) Adjust the keycodes file in `data/keycodes`. This file stores the relation between scancodes 
sent by the keyboard and keycodes you see on the keyboard. The default keycodes file as it is now 
has the scancode<->keycode layout for the US keyboard layout. If you are using a different layout, 
please adjust the file to fit your needs.

### Installation
Once all of the above prerequisites are fulfilled, `setup.sh` should do the rest. It will install 
depending libraries into your users home directory (`$HOME/.ukip/`) so you don't have to install 
them system wide:
```
chmod +x setup.sh
./setup.sh
```
That's it: The daemon will be automatically started at boot time.  

For interaction with the service, the systemd interface is probably the most convenient one.
To check the status:
```
systemctl status ukip.service
```

To stop the service:
```
sudo systemctl stop ukip.service
```

Alternatively, to disable the service and prevent it from being started at boot time:
```
sudo systemctl disable ukip.service
```

## Terms of use

### USB Keystroke Injection Protection
This project provides code that can be run on Linux systems to harden those systems against keystroke injection attacks, delivered via USB.
The terms of use apply to data provided by Google or implicitly through code in this repository.

```
This tool hereby grants you a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable copyright license to reproduce, prepare
derivative works of, publicly display, publicly perform, sublicense, and
distribute code in this repository related to this tool. Any copy you make for
such purposes is authorized provided that you reproduce this tool's copyright
designation and this license in any such copy.
```

### Third-party Libraries
This project builds upon several open source libraries.  
Please see each projects' Terms of use when using the provided code in this repository.

## Disclaimer
**This is not an officially supported Google product.**


================================================
FILE: data/allowlist
================================================
# This is the allowlist for UKIP: USB Keystroke Injection Protection.
#
# Devices are added manually by a user in the following form, one rule per line:
# <product ID in hex>:<vendor ID in hex> <allowed characters, comma separated>
# An example for the Yubikey:
0x10:0x1050 c,b,d,e,f,g,h,i,j,k,l,n,r,t,u,v

# If every character should be allowed, the product ID and vendor ID, followed
# by the keyword any is sufficient.
#
# The following would be an example for the product ID 0x1234 and the vendor ID
# 0x1337 (without the starting hashtag):
# 0x1234:0x1337 any

# If no character should be allowed, the approach is similar, but the keyword is
# none.
#
# The following would be an example for the product ID 0x1337 and the vendor ID
# 0x1234 (without the starting hashtag):
# 0x1337:0x1234 none


================================================
FILE: data/keycodes
================================================
{
  "lowcodes":
  [{
    "1": "ESC",
    "2": "1",
    "3": "2",
    "4": "3",
    "5": "4",
    "6": "5",
    "7": "6",
    "8": "7",
    "9": "8",
    "10": "9",
    "11": "0",
    "12": "-",
    "13": "=",
    "14": "BKSP",
    "15": "TAB",
    "16": "q",
    "17": "w",
    "18": "e",
    "19": "r",
    "20": "t",
    "21": "y",
    "22": "u",
    "23": "i",
    "24": "o",
    "25": "p",
    "26": "[",
    "27": "]",
    "28": "CRLF",
    "29": "LCTRL",
    "30": "a",
    "31": "s",
    "32": "d",
    "33": "f",
    "34": "g",
    "35": "h",
    "36": "j",
    "37": "k",
    "38": "l",
    "39": ";",
    "40": "\"",
    "41": "`",
    "42": "LSHFT",
    "43": "\\",
    "44": "z",
    "45": "x",
    "46": "c",
    "47": "v",
    "48": "b",
    "49": "n",
    "50": "m",
    "51": ",",
    "52": ".",
    "53": "/",
    "54": "RSHFT",
    "56": "LALT",
    "57": " ",
    "100": "RALT"
  }],

  "capscodes":
  [{
    "1": "ESC",
    "2": "!",
    "3": "@",
    "4": "#",
    "5": "$",
    "6": "%",
    "7": "^",
    "8": "&",
    "9": "*",
    "10": "(",
    "11": ")",
    "12": "_",
    "13": "+",
    "14": "BKSP",
    "15": "TAB",
    "16": "Q",
    "17": "W",
    "18": "E",
    "19": "R",
    "20": "T",
    "21": "Y",
    "22": "U",
    "23": "I",
    "24": "O",
    "25": "P",
    "26": "{",
    "27": "}",
    "28": "CRLF",
    "29": "LCTRL",
    "30": "A",
    "31": "S",
    "32": "D",
    "33": "F",
    "34": "G",
    "35": "H",
    "36": "J",
    "37": "K",
    "38": "L",
    "39": ":",
    "40": "'",
    "41": "~",
    "42": "LSHFT",
    "43": "|",
    "44": "Z",
    "45": "X",
    "46": "C",
    "47": "V",
    "48": "B",
    "49": "N",
    "50": "M",
    "51": "<",
    "52": ">",
    "53": "?",
    "54": "RSHFT",
    "56": "LALT",
    "57": " ",
    "100": "RALT"
  }]
}


================================================
FILE: data/ukip.service
================================================
[Unit]
Description=UKIP
Requires=systemd-udevd.service
After=systemd-udevd.service

[Service]
ExecStart=/usr/sbin/ukip

[Install]
WantedBy=multi-user.target


================================================
FILE: requirements.txt
================================================
attrs==18.1.0
pyudev==0.21.0
attr==0.3.1
evdev==1.3.0
pyusb==1.0.2


================================================
FILE: setup.sh
================================================
#!/bin/bash
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Replace those variables to fit your needs.
NEW_KEYSTROKE_WINDOW=5
NEW_ABNORMAL_TYPING=50000
# Set either MONITOR or HARDENING.
RUN_MODE=MONITOR

# For systemd it's important to know which Linux flavor.
DEBIAN=true

# Path to virtual environment (.ukip/ in the user's home).
VENV_PATH=$HOME'/.ukip/'


function info() {
  echo -e "[\e[94m*\e[0m]" "$@"
}

function error() {
  echo -e "[\e[91m!\e[0m]" "$@"
}

function success() {
  echo -e "[\e[92m+\e[0m]" "$@"
}

function fatal() {
  error "$@"
  exit 1
}

function install_virtual_env() {
  # Replace the shebang line.
  sed -i 's@#!/usr/bin/env python3@#!'$VENV_PATH'bin/python3@g' src/ukip.py

  # Install the needed virtual environemt.
  /usr/bin/env python3 -m venv $VENV_PATH

  # Activate the venv.
  source $VENV_PATH'bin/activate'

  # Install wheel before requirements.
  /usr/bin/env pip3 -q install wheel

  # Install the required packages.
  /usr/bin/env pip3 -q install -r requirements.txt

  success "Successfully prepared and installed the virtual environment."
}

function replace_variables() {
  sed -i 's/ABNORMAL_TYPING = [^0-9]*\([0-9]\+\)/ABNORMAL_TYPING = '$NEW_ABNORMAL_TYPING'/g' src/ukip.py
  sed -i 's/KEYSTROKE_WINDOW = [^0-9]*\([0-9]\+\)/KEYSTROKE_WINDOW = '$NEW_KEYSTROKE_WINDOW'/g' src/ukip.py
  sed -i 's/_UKIP_RUN_MODE = UKIP_AVAILABLE_MODES\.\(MONITOR\|HARDENING\)/_UKIP_RUN_MODE = UKIP_AVAILABLE_MODES\.'$RUN_MODE'/g' src/ukip.py


  success "Successfully replaced abnormal typing and keystroke window variables in UKIP."
  success "Successfully set the run mode for UKIP."
}

function prepare_metadata() {
  ALLOWLIST_FILE=/etc/ukip/allowlist
  KEYCODES_FILE=/etc/ukip/keycodes

  sudo mkdir /etc/ukip/

  sudo cp data/allowlist $ALLOWLIST_FILE
  sudo chmod 0755 $ALLOWLIST_FILE
  sudo chown root:root $ALLOWLIST_FILE

  sudo cp data/keycodes $KEYCODES_FILE
  sudo chmod 0755 $KEYCODES_FILE
  sudo chown root:root $KEYCODES_FILE

  success "Installed the allowlist and the keycodes file in /etc/ukip/."
}

function install_ukip() {
  UKIP_BINARY=/usr/sbin/ukip

  sudo cp src/ukip.py $UKIP_BINARY
  sudo chmod 0755 $UKIP_BINARY
  sudo chown root:root $UKIP_BINARY

  success "Installed UKIP in /usr/sbin/."
}

function install_systemd_service() {
  if $DEBIAN; then
    # For Debian based OSs.
    SYSTEMD_PATH=/lib/systemd/system/ukip.service
  else
    # For Fedora based OSs.
    SYSTEMD_PATH=/usr/lib/systemd/system/ukip.service
  fi

  sudo cp data/ukip.service $SYSTEMD_PATH
  sudo chmod 0644 $SYSTEMD_PATH
  sudo chown root:root $SYSTEMD_PATH

  sudo systemctl start ukip.service

  # The start and enabling sometimes race.
  sleep 1

  sudo systemctl enable ukip.service

  success "Installed and started systemd service."
}

info "Preparing and installing the virtual environment..."
install_virtual_env

info "Replacing keystroke window, abnormal typing speed and run mode..."
replace_variables

info "Preparing UKIP metadata..."
prepare_metadata

info "Installing UKIP..."
install_ukip

info "Installing and starting systemd service..."
install_systemd_service

success "UKIP is now installed and enabled on startup!"


================================================
FILE: src/__init__.py
================================================


================================================
FILE: src/ukip.py
================================================
#!/usr/bin/env python3
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import collections
import gc
import json
import logging
import logging.handlers
import sys
import threading
import attr
import enum
import evdev
import pyudev
from typing import Text
import usb


# Modes, available for UKIP to run in. Constant enum:
# 1) MONITOR: Sends information about the usb device to a logging instance.
# 2) HARDENING: The device gets removed from the system (drivers are unbound
#    from every device interface).
class UKIP_AVAILABLE_MODES(enum.Enum):
  MONITOR = 'MONITOR'
  HARDENING = 'HARDENING'


# The current mode, UKIP is running in.
_UKIP_RUN_MODE = UKIP_AVAILABLE_MODES.HARDENING

# A dict with ringbuffers as values (holding the most recent 5 keystroke times):
# keys are paths to the event devices.
_event_devices_timings = {}

# A dict with ringbuffers as values (holding the most recent 5 keystrokes):
# keys are paths to the event devices.
_event_devices_keystrokes = {}

# Window of keystrokes to look at.
KEYSTROKE_WINDOW = 5

# Abnormal typing threshold in milliseconds (Linux emits keystroke timings in
# microsecond precision).
# Lower: More True Positives.
# Higher: More False Positives.
ABNORMAL_TYPING = 50000

# 1 equals KEY_DOWN in evdev.
KEY_DOWN = evdev.KeyEvent.key_down

# Shifts as constants for better readability.
LSHIFT = 42
RSHIFT = 54

# Turn off duplicate logging to syslog, that would happen with the root logger.
logging.basicConfig(filename='/dev/null', level=logging.DEBUG)
# Now, turn on logging to syslog.
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
handler = logging.handlers.SysLogHandler(address='/dev/log')
log.addHandler(handler)

# Global lock for _event_devices_timings and _event_devices_keystrokes dicts.
_event_devices_lock = threading.Lock()


@attr.s
class AllowlistConfigReturn(object):
  """Class to represent the return value of the allowlist Config.

  The following return combinations are valid:
  1) allowlist is a list with characters, device_present is true: the returned
  characters are not blocked by UKIP for the given device.
  2) allowlist is an empty list, device_present is true: for the given device,
  any character is allowed by UKIP.
  3) allowlist is an empty list, device_present is false: for the given device,
  no character is allowed by UKIP (either the device is not in the config
  file, or a user specifically marked that device with 'none' for the allowed
  characters).

  Attributes:
   allowlist: The returned allowlist, or empty if all characters are allowed.
   device_present: A boolean, whether the device was found in the config file.
  """
  allowlist = attr.ib()  # type: list
  device_present = attr.ib()  # type: boolean


@attr.s
class KeycodesReturn(object):
  """Class to represent the return value of the keycode file read.

  The keycode file in /etc/ukip/keycodes contains the scancodes and ASCII
  codes for the selected keyboard layout. It is parsed once and read into two
  dicts for further processing: lower_codes and capped_codes.
  """
  lower_codes = attr.ib()  # type: dict
  capped_codes = attr.ib()  # type: dict


class DeviceError(Exception):
  """Generic error class for device processing."""


class AllowlistFileError(Exception):
  """Generic error class for allowlist processing."""


class KeycodesFileError(Exception):
  """Generic error class for keycode file processing."""


def add_to_ring_buffer(event_device_path: Text, key_down_time: int,
                       keystroke: Text, device: usb.core.Device):
  """Add time in milliseconds to global ringbuffer.

  Locates the event device (/dev/input/*) in the dict of ringbuffers and adds
  the KEY_DOWN time in milliseconds to it. Then calls the check_for_attack
  function on the event device and the usb core device.

  Args:
    event_device_path: The path to the event device (/dev/input/*).
    key_down_time: The KEY_DOWN time in milliseconds.
    keystroke: The actual key typed.
    device: A USB device (usb.core.Device).
  """
  with _event_devices_lock:
    if event_device_path not in _event_devices_timings:
      _event_devices_timings[event_device_path] = collections.deque(
          maxlen=KEYSTROKE_WINDOW)
      _event_devices_keystrokes[event_device_path] = collections.deque(
          maxlen=KEYSTROKE_WINDOW)

    _event_devices_timings[event_device_path].append(key_down_time)
    _event_devices_keystrokes[event_device_path].append(keystroke)

  check_for_attack(event_device_path, device)


def check_local_allowlist(product_id: Text,
                          vendor_id: Text) -> AllowlistConfigReturn:
  """Check local (user-based) allowlist for specifically allowed devices.

  UKIP users are able to specify USB devices they want to allow in a local
  file. This allowlist is checked, when a device is found attacking (timing
  threshold is exceeded) and whether that device is listed in here. If so, only
  the characters listed in the corresponding allowlist are allowed, the others
  are denied (in case of 'any' and 'none' all or no characters are allowed
  respectively). If the device is not listed in the allowlist, it is denied per
  default.

  Args:
    product_id: The required product ID to look up in the local allowlist.
    vendor_id: The required vendor ID to look up in the local allowlist.

  Raises:
    AllowlistFileError: When there were errors with the allowlist config file.

  Returns:
    A AllowlistConfigReturn object, with the following variations:
    1) allowlist is a list with characters, device_present is true: the returned
    characters are not blocked by UKIP for the given device.
    2) allowlist is an empty list, device_present is true: for the given device
    any character is allowed by UKIP.
    3) allowlist is an empty list, device_present is false: for the given device
    no character is allowed by UKIP (either the device is not in the config
    file, or a user specifically marked that device with 'none' for the allowed
    characters).
  """
  device = '%s:%s' % (product_id, vendor_id)

  try:
    with open('/etc/ukip/allowlist', 'r') as f:
      for line in f:
        # Comments start with '#'.
        if line[0] == '#':
          continue
        # Ignore empty lines.
        if not line.strip():
          continue

        try:
          (key, val) = line.split()
          int(key.split(':')[0], 16)
          int(key.split(':')[1], 16)

          allowlist = val.split(',')

          if key != device:
            continue
          if allowlist[0] == 'any':
            return AllowlistConfigReturn(allowlist=[], device_present=True)
          if allowlist[0] == 'none':
            return AllowlistConfigReturn(allowlist=[], device_present=False)

          # If all of the checks succeed, return the allowlist (but only if it
          # is an allowlist, and not a word).
          if len(allowlist[0]) == 1:
            return AllowlistConfigReturn(
                allowlist=val.split(','), device_present=True)
        except (ValueError, IndexError) as vi:
          raise AllowlistFileError(
              'The format of the config file /etc/ukip/allowlist seems to be'
              ' incorrect: %s' % vi)

      # If the device wasn't found in the file, return False.
      return AllowlistConfigReturn(allowlist=[], device_present=False)
  except FileNotFoundError as fnfe:
    raise AllowlistFileError(
        'The config file /etc/ukip/allowlist could not be found: %s' % fnfe)


def check_for_attack(event_device_path: Text, device: usb.core.Device) -> bool:
  """Check a ringbuffer of KEY_DOWN timings for attacks.

  Locates the event device (/dev/input/*) in the dict of ringbuffers and checks
  the correct ringbuffer for attacks (keystroke injection attack). In case of
  an attack, two actions can be taken, depending on the mode UKIP is running in.
  Those modes are specified in the UKIP_AVAILABLE_MODES enum.

  Args:
    event_device_path: The path to the event device (/dev/input/*).
    device: A USB device (usb.core.Device).

  Returns:
    False: If the check failed (not enough times, mode not set). None otherwise.
  """
  with _event_devices_lock:
    if len(_event_devices_timings[event_device_path]) < KEYSTROKE_WINDOW:
      return False

    attack_counter = 0

    # Count the number of adjacent keystrokes below (or equal) the
    # ABNORMAL_TYPING.
    reversed_buffer = reversed(_event_devices_timings[event_device_path])
    for value in reversed_buffer:
      for prev in reversed_buffer:
        if value - prev <= ABNORMAL_TYPING:
          attack_counter += 1
        value = prev
      break  # Exit after the first backward iteratation.

  # If all the timings in the ringbuffer are within the ABNORMAL_TYPING timing.
  if attack_counter == KEYSTROKE_WINDOW - 1:
    if _UKIP_RUN_MODE == UKIP_AVAILABLE_MODES.MONITOR:
      enforce_monitor_mode(device, event_device_path)
    elif _UKIP_RUN_MODE == UKIP_AVAILABLE_MODES.HARDENING:
      enforce_hardening_mode(device, event_device_path)
    else:
      log.error('No run mode was specified for UKIP. Exiting...')
      return False


def enforce_monitor_mode(device: usb.core.Device, event_device_path: Text):
  """Enforce the MONITOR mode on a given device.

  Information about devices, that would have been blocked in HARDENING mode
  is logged to /dev/log.

  Args:
    device: A USB device (usb.core.Device).
    event_device_path: The path to the event device (/dev/input/*).
  """
  log.warning(
      '[UKIP] The device %s with the vendor id %s and the product id'
      ' %s would have been blocked. The causing timings are: %s.',
      device.product if device.product else 'UNKNOWN', hex(device.idVendor),
      hex(device.idProduct), _event_devices_timings[event_device_path])


def enforce_hardening_mode(device: usb.core.Device, event_device_path: Text):
  """Enforce the HARDENING mode on a given device.

  When enforcing the HARDENING mode, a device gets removed from the operating
  system when the keystrokes exceed the typing speed threshold
  (ABNORMAL_TYPING). This is done by unbinding the drivers from every device
  interface. Before the device is removed, the allowlist is checked. If the
  product and vendor ids are in there, the function will return and the device
  will continue working (possibly with a reduced allowed character set, as
  described in the function check_local_allowlist).

  Args:
    device: A USB device (usb.core.Device).
    event_device_path: The path to the event device (/dev/input/*).
  """

  product_id = hex(device.idProduct)
  vendor_id = hex(device.idVendor)

  local_allowlist = check_local_allowlist(
      hex(device.idProduct), hex(device.idVendor))

  # Device is present in the allowlist and all characters are allowed.
  if local_allowlist.device_present and not local_allowlist.allowlist:
    return
  # Device is present and an allowlist is specified.
  elif local_allowlist.device_present and local_allowlist.allowlist:
    allowlist = local_allowlist.allowlist
  # Device is not in the allowlist or keyword is 'none'.
  # i.e.: not local_allowlist.device_present and not local_allowlist.allowlist
  else:
    allowlist = []

  # If all typed characters are in the allowlist, return. Otherwise run through
  # the rest of the function.
  if not set(_event_devices_keystrokes[event_device_path]).difference(
      set(allowlist)):
    return

  pid_and_vid = '%s:%s' % (product_id, vendor_id)

  for config in device:
    for interface in range(config.bNumInterfaces):
      if device.is_kernel_driver_active(interface):
        try:
          device.detach_kernel_driver(interface)

          if device.product:
            log.warning(
                '[UKIP] The device %s with the vendor id %s and the '
                'product id %s was blocked. The causing timings were: '
                '%s.', device.product, vendor_id, product_id,
                _event_devices_timings[event_device_path])
          else:
            log.warning(
                '[UKIP] The device with the vendor id %s and the '
                'product id %s was blocked. The causing timings were: '
                '%s.', vendor_id, product_id,
                _event_devices_timings[event_device_path])

        except (IOError, OSError, ValueError, usb.core.USBError) as e:
          log.warning(
              'There was an error in unbinding the interface for the USB device'
              ' %s: %s', pid_and_vid, e)
          # In case of an error we still need to continue to the next interface.
          continue

  # The device was removed, so clear the dicts. Most importantly, clear the
  # keystroke dict.
  del _event_devices_timings[event_device_path]
  del _event_devices_keystrokes[event_device_path]
  gc.collect()


def load_keycodes_from_file() -> KeycodesReturn:
  """Helper function to load the keycodes file into memory.

  Returns:
    The lowcodes and capscodes as dicts in a KeycodesReturn attribute.
  Raises:
    KeycodesFileError: If there is a problem with the keycodes file.
  """
  lowcodes = {}
  capscodes = {}

  try:
    with open('/etc/ukip/keycodes', 'r') as keycode_file:
      try:
        keycodes = json.load(keycode_file)
      except (OverflowError, ValueError, TypeError) as je:
        raise KeycodesFileError('The keycodes file could not be read: %s' % je)
  except FileNotFoundError as fnfe:
    raise KeycodesFileError(
        'The keycode file /etc/ukip/keycodes could not be found: %s' % fnfe)

  if not keycodes.get('lowcodes') or not keycodes.get('capscodes'):
    log.error(
        'The keycodes file is missing either the lowcodes or capscodes keyword.'
    )
    return KeycodesReturn(lower_codes=lowcodes, capped_codes=capscodes)

  for keycode in keycodes['lowcodes']:
    for scancode, lowcode in keycode.items():
      lowcodes[int(scancode)] = lowcode

  for keycode in keycodes['capscodes']:
    for scancode, capcode in keycode.items():
      capscodes[int(scancode)] = capcode

  return KeycodesReturn(lower_codes=lowcodes, capped_codes=capscodes)


def monitor_device_thread(device: pyudev.Device, vendor_id: int,
                          product_id: int) -> None:
  """Monitor a given USB device for occurring KEY_DOWN events.

  Creates a passive reading loop over a given event device and waits for
  KEY_DOWN events to occour. Then extracts the time in milliseconds of the event
  and adds it to the ringbuffer.

  Args:
    device: The event device in (/dev/input/*).
    vendor_id: The vendor ID of the device.
    product_id: The product ID of the device.

  Raises:
    OSError: If the given USB device cannot be found or if the OS receives
             keyboard events, after the device was unbound. Both originate from
             the evdev lib.
    StopIteration: If the iteration of the usb device tree breaks.
  """
  keycodes = load_keycodes_from_file()
  lowcodes = keycodes.lower_codes
  capscodes = keycodes.capped_codes

  try:
    try:
      inputdevice = evdev.InputDevice(device.device_node)
      dev = usb.core.find(idVendor=vendor_id, idProduct=product_id)
    except (OSError, StopIteration) as mex:
      log.warning(
          'There was an error while starting the thread for device monitoring:'
          ' %s', mex)

      # Bail the function and with that, end the thread.
      return

    log.info(
        f'Start monitoring {device.device_node} with the VID {hex(vendor_id)} and the PID {hex(product_id)}'
    )

    try:
      # The default behaviour of evdev.InputDevice is a non-exclusive access,
      # so each reader gets a copy of each event.
      for event in inputdevice.read_loop():
        caps = False

        for led in inputdevice.leds(verbose=True):
          # Check if CapsLock is turned on.
          if 'LED_CAPSL' in led:
            caps = True

        # LShift or RShift is either pressed or held.
        if LSHIFT in inputdevice.active_keys(
        ) or RSHIFT in inputdevice.active_keys():
          caps = True

        if event.value == KEY_DOWN and event.type == evdev.ecodes.EV_KEY:
          keystroke_in_ms = (event.sec * 1000000) + event.usec

          if caps:
            keystroke = capscodes.get(evdev.categorize(event).scancode)
          else:
            keystroke = lowcodes.get(evdev.categorize(event).scancode)

          add_to_ring_buffer(device.device_node, keystroke_in_ms, keystroke,
                             dev)

    except OSError as ose:
      log.warning('Events found for unbound device: %s', ose)
  except:
    log.exception('Error monitoring device.')


def init_device_list() -> int:
  """Adds all current event devices to the global dict of event devices.

  Returns:
    The number of event devices connected, at the time UKIP was started.
  Raises:
    TypeError: If there is an error in converting the PID/VID of a USB device.
    ValueError: If there is an error in converting the PID/VID of a USB device.
    RuntimeError: If there is an error in launching the thread.
    DeviceError: If there is an error in creating the device list.
  """

  device_count = 0

  try:
    local_device_context = pyudev.Context()
    local_device_monitor = pyudev.Monitor.from_netlink(local_device_context)
    local_device_monitor.filter_by(subsystem='input')
  except (ValueError, EnvironmentError, DeviceError) as mex:
    log.warning(
        'There was an error creating the initial list of USB devices: %s', mex)
    raise DeviceError('The device context and monitor could not be created.')

  for device in local_device_context.list_devices():
    if device.device_node and device.device_node.startswith(
        '/dev/input/event') and (device.get('ID_VENDOR_ID') and
                                 device.get('ID_MODEL_ID')):

      try:
        vendor_id = int(device.get('ID_VENDOR_ID'), 16)
        product_id = int(device.get('ID_MODEL_ID'), 16)
      except (TypeError, ValueError) as mex:
        log.error(
            'There was an error in converting the PID and VID of a USB device: '
            '%s', mex)
        continue

      try:
        threading.Thread(
            target=monitor_device_thread,
            args=(device, vendor_id, product_id)).start()
        device_count += 1
      except RuntimeError as e:
        log.error(
            'There was an runtime error in starting the monitoring thread %s',
            e)

  return device_count


def main(argv):
  if len(argv) > 1:
    sys.exit('Too many command-line arguments.')

  device_count = init_device_list()

  if not device_count:
    log.warning('No HID devices connected to this machine yet')

  #####################
  # Hotplug detection #
  #####################
  context = pyudev.Context()
  monitor = pyudev.Monitor.from_netlink(context)
  monitor.filter_by(subsystem='input')

  for device in iter(monitor.poll, None):
    try:
      if device.action == 'add':
        if device.device_node and '/dev/input/event' in device.device_node and (
            device.get('ID_VENDOR_ID') and device.get('ID_MODEL_ID')):

          try:
            vendor_id = int(device.get('ID_VENDOR_ID'), 16)
            product_id = int(device.get('ID_MODEL_ID'), 16)
          except (TypeError, ValueError) as mex:
            log.error(
                'There was an error in converting the PID and VID of a USB'
                ' device: %s', mex)
            continue

          threading.Thread(
              target=monitor_device_thread,
              args=(device, vendor_id, product_id)).start()
    except:
      log.exception('Error adding new device to monitoring.')


if __name__ == '__main__':
  sys.exit(main(sys.argv))


================================================
FILE: src/ukip_test.py
================================================
#!/usr/bin/env python3
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import builtins
import collections
import gc
import json
import unittest.mock as mock
import sys
import threading
import unittest
import ukip
import evdev
import pyudev
import usb


sys.modules['evdev'] = mock.MagicMock()
sys.modules['pyudev'] = mock.MagicMock()
sys.modules['usb'] = mock.MagicMock()


# This is needed, because the whole library is (Magic)mocked.
# Therefore, without this an error is thrown, that usb.core.USBError is not
# inheriting from BaseException.
class USBError(IOError):
  pass


class UkipTest(unittest.TestCase):

  def setUp(self):
    super(UkipTest, self).setUp()

    usb.core.USBError = USBError

    ukip._event_devices_timings = {}
    ukip._event_devices_keystrokes = {}

    class FakePyudevDevice(object):
      product = None
      device_node = None
      action = None
      ID_VENDOR_ID = None
      ID_MODEL_ID = None

      def get(self, attribute):
        return getattr(self, attribute)

    class FakeEvent(object):
      value = None
      type = None
      sec = None
      usec = None
      scancode = None

    self.pyudev_device = FakePyudevDevice()
    self.pyudev_device.product = 'FakeProduct'
    self.pyudev_device.device_node = '/dev/input/event1337'
    self.pyudev_device.action = 'add'
    # Pyudev devices emit the PID and VID as strings (hex values, but str).
    # Also, the PID (product ID) is called model ID (ID_MODEL_ID).
    self.pyudev_device.ID_VENDOR_ID = '123'
    self.pyudev_device.ID_MODEL_ID = '456'

    self.fake_event = FakeEvent()
    self.fake_event.value = evdev.KeyEvent.key_down
    self.fake_event.type = evdev.ecodes.EV_KEY
    self.fake_event.sec = 13
    self.fake_event.usec = 477827
    self.fake_event.scancode = 45

    self.mock_inputdevice = mock.create_autospec(evdev.InputDevice)

    self.mock_pyusb_device = mock.MagicMock()
    self.mock_pyusb_device.product = 'SomeVendor Keyboard'
    # PyUSB devices emit the PID and VID as integers.
    self.mock_pyusb_device.idVendor = 123
    self.mock_pyusb_device.idProduct = 456
    self.mock_pyusb_device.is_kernel_driver_active.return_value = True

    self.mock_usb_config = mock.create_autospec(usb.core.Configuration)
    self.mock_usb_config.bNumInterfaces = 1

    self.event_device_path = '/dev/input/event1337'

    evdev.InputDevice.side_effect = None

  @mock.patch.object(ukip, 'enforce_monitor_mode', autospec=True)
  def test_check_for_attack_trigger_monitor(self, monitor_mode_mock):
    """Tests if the monitor mode is triggered for attacking device times."""

    ukip._UKIP_RUN_MODE = ukip.UKIP_AVAILABLE_MODES.MONITOR

    # Need to access the global variable.
    ukip._event_devices_timings[self.event_device_path] = collections.deque(
        maxlen=ukip.KEYSTROKE_WINDOW)
    ukip._event_devices_keystrokes[self.event_device_path] = collections.deque(
        maxlen=ukip.KEYSTROKE_WINDOW)

    # Push amount of KEYSTROKE_WINDOW times into the ringbuffer, that trigger
    # the monitor mode.
    ukip._event_devices_timings[self.event_device_path].append(1555146977759524)
    ukip._event_devices_timings[self.event_device_path].append(1555146977759525)
    ukip._event_devices_timings[self.event_device_path].append(1555146977759526)
    ukip._event_devices_timings[self.event_device_path].append(1555146977759527)
    ukip._event_devices_timings[self.event_device_path].append(1555146977759528)

    ukip.check_for_attack(self.event_device_path, self.mock_pyusb_device)

    # The timings trigger, so call the monitor mode.
    monitor_mode_mock.assert_called_once_with(self.mock_pyusb_device,
                                              self.event_device_path)

  @mock.patch.object(ukip, 'enforce_monitor_mode', autospec=True)
  def test_check_for_attack_not_trigger_monitor(self, monitor_mode_mock):
    """Tests if the monitor mode is NOT triggered for benign device times."""

    ukip._UKIP_RUN_MODE = ukip.UKIP_AVAILABLE_MODES.MONITOR

    # Need to access the global variable.
    ukip._event_devices_timings[self.event_device_path] = collections.deque(
        maxlen=ukip.KEYSTROKE_WINDOW)

    # Normal typing, that doesn't trigger the monitor mode.
    ukip._event_devices_timings[self.event_device_path].append(1555146977759524)
    ukip._event_devices_timings[self.event_device_path].append(1555146980127487)
    ukip._event_devices_timings[self.event_device_path].append(1555146982271470)
    ukip._event_devices_timings[self.event_device_path].append(1555146984415453)
    ukip._event_devices_timings[self.event_device_path].append(1555146986559436)

    ukip.check_for_attack(self.event_device_path, self.mock_pyusb_device)

    # Since normal typing, the monitor mode was not called.
    self.assertFalse(monitor_mode_mock.called)

  @mock.patch.object(ukip, 'enforce_monitor_mode', autospec=True)
  def test_check_for_attack_no_times(self, monitor_mode_mock):
    """Checks if function returns early, if no times are provided."""

    ukip._UKIP_RUN_MODE = ukip.UKIP_AVAILABLE_MODES.MONITOR

    ukip._event_devices_timings[self.event_device_path] = collections.deque(
        maxlen=ukip.KEYSTROKE_WINDOW)
    not_enough_timings = ukip.check_for_attack(self.event_device_path,
                                               self.mock_pyusb_device)

    # Not enough times, so bail out of the function call early (return False).
    self.assertIs(not_enough_timings, False)

    # When not enough times, return value is None and monitor mode is not
    # called.
    self.assertFalse(monitor_mode_mock.called)

  @mock.patch.object(ukip, 'enforce_hardening_mode', autospec=True)
  @mock.patch.object(ukip, 'enforce_monitor_mode', autospec=True)
  def test_check_for_attack_proper_run_mode(self, monitor_mode_mock,
                                            hardening_mode_mock):
    """Tests if the proper mode is executed based on global selection."""

    # Need to access the global variable.
    ukip._event_devices_timings[self.event_device_path] = collections.deque(
        maxlen=ukip.KEYSTROKE_WINDOW)

    # Push amount of KEYSTROKE_WINDOW times into the ringbuffer, that triggers
    # the chosen mode.
    ukip._event_devices_timings[self.event_device_path].append(1555146977759524)
    ukip._event_devices_timings[self.event_device_path].append(1555146977759525)
    ukip._event_devices_timings[self.event_device_path].append(1555146977759526)
    ukip._event_devices_timings[self.event_device_path].append(1555146977759527)
    ukip._event_devices_timings[self.event_device_path].append(1555146977759528)

    # First test with the MONITOR mode.
    ukip._UKIP_RUN_MODE = ukip.UKIP_AVAILABLE_MODES.MONITOR
    ukip.check_for_attack(self.event_device_path, self.mock_pyusb_device)
    monitor_mode_mock.assert_called_once_with(self.mock_pyusb_device,
                                              self.event_device_path)

    # Finally, test with the HARDENING mode.
    ukip._UKIP_RUN_MODE = ukip.UKIP_AVAILABLE_MODES.HARDENING
    ukip.check_for_attack(self.event_device_path, self.mock_pyusb_device)
    hardening_mode_mock.assert_called_once_with(self.mock_pyusb_device,
                                                self.event_device_path)

  @mock.patch.object(ukip, 'log', autospec=True)
  @mock.patch.object(ukip, 'enforce_hardening_mode', autospec=True)
  @mock.patch.object(ukip, 'enforce_monitor_mode', autospec=True)
  def test_check_for_attack_no_run_mode(self, monitor_mode_mock,
                                        hardening_mode_mock, logging_mock):
    """Tests when no run mode is set."""

    # Need to access the global variable.
    ukip._event_devices_timings[self.event_device_path] = collections.deque(
        maxlen=ukip.KEYSTROKE_WINDOW)

    # Push amount of KEYSTROKE_WINDOW times into the ringbuffer, that would
    # trigger a chosen mode.
    ukip._event_devices_timings[self.event_device_path].append(1555146977759524)
    ukip._event_devices_timings[self.event_device_path].append(1555146977759525)
    ukip._event_devices_timings[self.event_device_path].append(1555146977759526)
    ukip._event_devices_timings[self.event_device_path].append(1555146977759527)
    ukip._event_devices_timings[self.event_device_path].append(1555146977759528)

    # Set the run mode to None.
    ukip._UKIP_RUN_MODE = None
    ukip.check_for_attack(self.event_device_path, self.mock_pyusb_device)

    # No mode should trigger.
    self.assertFalse(monitor_mode_mock.called)
    self.assertFalse(hardening_mode_mock.called)

    # But the error should be logged.
    logging_mock.error.assert_called_once()

  @mock.patch.object(ukip, 'check_for_attack', autospec=True)
  def test_add_to_ring_buffer_create_key_time(self, check_for_attack_mock):
    """Tests the ringbuffer key creation on adding a time for the first time."""

    # At the beginning the global dict is empty.
    self.assertFalse(ukip._event_devices_timings)

    # The event_device_path wasn't present, but should be created now.
    ukip.add_to_ring_buffer(self.event_device_path, 1555146977759524, 'x',
                            self.mock_pyusb_device)

    # Check if the key was successfully created.
    self.assertTrue(ukip._event_devices_timings.get(self.event_device_path))

    # Check if the check_for_attack function was called on the created key.
    check_for_attack_mock.assert_called_once_with(self.event_device_path,
                                                  self.mock_pyusb_device)

  @mock.patch.object(ukip, 'check_for_attack', autospec=True)
  def test_add_to_ring_buffer_create_key_keystroke(self, check_for_attack_mock):
    """Tests the ringbuffer key creation on adding an initial keystroke."""

    # At the beginning the global dict is empty.
    self.assertFalse(ukip._event_devices_keystrokes)

    # The event_device_path wasn't present, but should be created now.
    ukip.add_to_ring_buffer(self.event_device_path, 1555146977759524, 'x',
                            self.mock_pyusb_device)

    # Check if the key was successfully created.
    self.assertTrue(ukip._event_devices_keystrokes.get(self.event_device_path))

    # Check if the check_for_attack function was called on the created key.
    check_for_attack_mock.assert_called_once_with(self.event_device_path,
                                                  self.mock_pyusb_device)

  @mock.patch.object(ukip, 'check_for_attack', autospec=True)
  def test_add_to_ring_buffer_multiple_values(self, check_for_attack_mock):
    """Tests if the ringbuffer is working correctly with the set window."""

    ukip.add_to_ring_buffer(self.event_device_path, 1555146977759524, 'a',
                            self.mock_pyusb_device)

    self.assertEqual(
        len(ukip._event_devices_timings.get(self.event_device_path)), 1)

    ukip.add_to_ring_buffer(self.event_device_path, 1555146980127487, 'b',
                            self.mock_pyusb_device)

    self.assertEqual(
        len(ukip._event_devices_timings.get(self.event_device_path)), 2)

    ukip.add_to_ring_buffer(self.event_device_path, 1555146980303490, 'c',
                            self.mock_pyusb_device)

    self.assertEqual(
        len(ukip._event_devices_timings.get(self.event_device_path)), 3)

    ukip.add_to_ring_buffer(self.event_device_path, 1555146982271470, 'd',
                            self.mock_pyusb_device)

    self.assertEqual(
        len(ukip._event_devices_timings.get(self.event_device_path)), 4)

    ukip.add_to_ring_buffer(self.event_device_path, 1555146984271470, 'e',
                            self.mock_pyusb_device)

    self.assertEqual(
        len(ukip._event_devices_timings.get(self.event_device_path)), 5)

    ukip.add_to_ring_buffer(self.event_device_path, 1555147982271470, 'f',
                            self.mock_pyusb_device)

    # Since it's a ringbuffer, the length for both dicts is still
    # KEYSTROKE_WINDOW.
    self.assertEqual(
        len(ukip._event_devices_timings.get(self.event_device_path)),
        ukip.KEYSTROKE_WINDOW)
    self.assertEqual(
        len(ukip._event_devices_timings.get(self.event_device_path)),
        ukip.KEYSTROKE_WINDOW)

    # The check_for_attack function was called KEYSTROKE_WINDOW + 1 times.
    self.assertEqual(check_for_attack_mock.call_count,
                     ukip.KEYSTROKE_WINDOW + 1)

  @mock.patch.object(ukip, 'log', autospec=True)
  def test_enforce_monitor_mode_with_product(self, logging_mock):
    """Tests which logging message is emitted when device has a product set."""

    self.fill_test_ringbuffer_with_data()

    ukip.enforce_monitor_mode(self.mock_pyusb_device, self.event_device_path)

    logging_mock.warning.assert_called_with(
        '[UKIP] The device %s with the vendor id %s and the product'
        ' id %s would have been blocked. The causing timings are: %s.',
        self.mock_pyusb_device.product, hex(self.mock_pyusb_device.idVendor),
        hex(self.mock_pyusb_device.idProduct),
        ukip._event_devices_timings[self.event_device_path])

  @mock.patch.object(ukip, 'log', autospec=True)
  def test_enforce_monitor_mode_no_product(self, logging_mock):
    """Tests which logging message is emitted when device has NO product set."""

    self.fill_test_ringbuffer_with_data()
    self.mock_pyusb_device.product = None

    ukip.enforce_monitor_mode(self.mock_pyusb_device, self.event_device_path)

    logging_mock.warning.assert_called_with(
        '[UKIP] The device %s with the vendor id %s and the product'
        ' id %s would have been blocked. The causing timings are: %s.',
        'UNKNOWN', hex(self.mock_pyusb_device.idVendor),
        hex(self.mock_pyusb_device.idProduct),
        ukip._event_devices_timings[self.event_device_path])

  @mock.patch.object(ukip, 'load_keycodes_from_file', autospec=True)
  @mock.patch.object(evdev, 'InputDevice', autospec=True)
  @mock.patch.object(usb.core, 'find', autospec=True)
  def test_monitor_device_thread_library_calls(self, usb_core_find_mock,
                                               input_device_mock,
                                               load_keycodes_from_file_mock):
    """Tests if all the calls to the libraries are made."""

    vendor_id = int(self.pyudev_device.ID_VENDOR_ID, 16)
    product_id = int(self.pyudev_device.ID_MODEL_ID, 16)

    ukip.monitor_device_thread(self.pyudev_device, vendor_id, product_id)

    load_keycodes_from_file_mock.assert_called()

    input_device_mock.assert_called_once_with(self.pyudev_device.device_node)
    usb_core_find_mock.assert_called_once_with(
        idVendor=vendor_id, idProduct=product_id)

  def test_monitor_device_thread_logging(self):
    """Tests the initial logging of the thread starting function."""
    # TODO Implement this test.

  @mock.patch.object(ukip, 'load_keycodes_from_file', autospec=True)
  @mock.patch.object(ukip, 'log', autospec=True)
  def test_monitor_device_thread_exception_inputdevice(
      self, logging_mock, load_keycodes_from_file_mock):
    """Tests exception and log message for the InputDevice creation."""
    log_message = ('There was an error while starting the thread for device '
                   'monitoring: %s')
    exception_message = '[Errno 19] No such device'
    exception_object = OSError(exception_message)

    evdev.InputDevice.side_effect = exception_object

    vendor_id = int(self.pyudev_device.ID_VENDOR_ID, 16)
    product_id = int(self.pyudev_device.ID_MODEL_ID, 16)

    ukip.monitor_device_thread(self.pyudev_device, vendor_id, product_id)

    load_keycodes_from_file_mock.assert_called()

    logging_mock.warning.assert_called()

  @mock.patch.object(ukip, 'load_keycodes_from_file', autospec=True)
  @mock.patch.object(ukip, 'log', autospec=True)
  def test_monitor_device_thread_exception_read_loop(
      self, logging_mock, load_keycodes_from_file_mock):
    """Tests exception and log message in read_loop."""
    log_message = 'Events found for unbound device: %s'
    exception_message = '[Errno 19] No such device'
    exception_object = OSError(exception_message)

    local_mock_inputdevice = mock.MagicMock()
    evdev.InputDevice.return_value = local_mock_inputdevice

    local_mock_inputdevice.read_loop.side_effect = exception_object

    vendor_id = int(self.pyudev_device.ID_VENDOR_ID, 16)
    product_id = int(self.pyudev_device.ID_MODEL_ID, 16)

    ukip.monitor_device_thread(self.pyudev_device, vendor_id, product_id)

    load_keycodes_from_file_mock.assert_called()

    logging_mock.warning.assert_called()

  def test_monitor_device_thread_keystroke_in_ms(self):
    """Tests if add_to_ringbuffer was called with the keystroke time in ms."""
    # TODO Implement this test.

  def test_monitor_device_thread_keystroke_shift(self):
    """Tests if add_to_ringbuffer was called with the upper case keystroke."""
    # TODO Implement this test.

  def test_monitor_device_thread_keystroke_capslock(self):
    """Tests if add_to_ringbuffer was called with the upper case keystroke."""
    # TODO Implement this test.

  @mock.patch.object(pyudev, 'Context', autospec=True)
  @mock.patch.object(pyudev.Monitor, 'from_netlink', autospec=True)
  def test_init_device_list_library_calls(self, netlink_mock, context_mock):
    """Tests if the initial library calls are made."""

    ukip.init_device_list()

    self.assertEqual(context_mock.call_count, 1)
    self.assertEqual(netlink_mock.call_count, 1)

  def test_init_device_list_exceptions(self):
    """Tests if exceptions were raised (ValueError and DeviceError)."""
    # TODO Implement this test.

  def test_init_device_list_device_count(self):
    """Tests if the number of devices is increased when iterating."""
    # TODO Implement this test.

  def test_init_device_list_invalid_pid_vid(self):
    """Tests if a ValueError is raised, when the VID/PID cannot be converted."""
    # TODO Implement this test.

  def test_init_device_list_runtimeerror(self):
    """Tests if the RuntimeError is thrown, when the thread failed to start."""
    # TODO Implement this test.

  def test_main_threading(self):
    """Tests if the thread was started."""
    # TODO Implement this test.

  def test_main_too_many_arguments(self):
    """Tests if no arguments were provided to main."""
    # TODO Implement this test.

  @mock.patch.object(pyudev.Monitor, 'from_netlink', autospec=True)
  def test_main_filter_by(self, netlink_mock):
    """Tests if the monitor filter_by was actually called."""

    monitor_mock = mock.MagicMock()
    pyudev.Monitor.from_netlink.return_value = monitor_mock
    monitor_mock.poll.side_effect = [self.pyudev_device, None]
    netlink_mock.return_value = monitor_mock

    ukip.main(['ukip.py'])

    calls = [mock.call(subsystem='input'), mock.call(subsystem='input')]
    monitor_mock.filter_by.assert_has_calls(calls)

  @mock.patch.object(builtins, 'open', autospec=True)
  def test_check_local_allowlist(self, open_mock):
    """Tests if the local allowlist check returns the allowlist on success."""

    open_mock.return_value.__enter__ = open_mock

    # Prepare a fake file, that looks similar to the actual file.
    open_mock.return_value.__iter__.return_value = iter([
        '# This is the config file\n', '# for UKIP.\n',
        '0x3784:0x3472 a,b,c\n'
    ])

    # Call with a PID and VID that will be found.
    allowlist = ukip.check_local_allowlist('0x3784', '0x3472')

    # If the PID and VID are found, the function returns the allowlist.
    self.assertEqual(
        allowlist,
        ukip.AllowlistConfigReturn(
            allowlist=['a', 'b', 'c'], device_present=True))

  @mock.patch.object(builtins, 'open', autospec=True)
  def test_check_local_allowlist_two_devices(self, open_mock):
    """Tests if the local allowlist with two devices, where one matches."""

    open_mock.return_value.__enter__ = open_mock

    # Prepare a fake file, that looks similar to the actual file.
    open_mock.return_value.__iter__.return_value = iter([
        '# This is the config file\n', '# for UKIP.\n',
        '0x1337:0x1234 x,y,z\n', '0x3784:0x3472 a,b,c\n'
    ])

    # Call with a PID and VID that will be found.
    allowlist = ukip.check_local_allowlist('0x3784', '0x3472')

    # If the PID and VID are found, the function returns the allowlist.
    self.assertEqual(
        allowlist,
        ukip.AllowlistConfigReturn(
            allowlist=['a', 'b', 'c'], device_present=True))

  @mock.patch.object(builtins, 'open', autospec=True)
  def test_check_local_allowlist_only_comments(self, open_mock):
    """Tests if the local allowlist check returns False when only comments."""

    open_mock.return_value.__enter__ = open_mock

    # Prepare a fake file, with only comments.
    open_mock.return_value.__iter__.return_value = iter([
        '# This is the config file\n', '# for UKIP.\n',
        '# One more comment line.\n'
    ])

    # Lookup for a PID and VID.
    allowlist = ukip.check_local_allowlist('0x3784', '0x3472')

    # If there are only comment in the config file, return False.
    self.assertEqual(
        allowlist,
        ukip.AllowlistConfigReturn(allowlist=[], device_present=False))

  @mock.patch.object(builtins, 'open', autospec=True)
  def test_check_local_allowlist_no_device(self, open_mock):
    """Tests if the allowlist check returns False when device not in file."""

    open_mock.return_value.__enter__ = open_mock

    open_mock.return_value.__iter__.return_value = iter([
        '# This is the config file\n', '# for UKIP.\n',
        '0x3784:0x3472 a,b,c\n'
    ])

    # Lookup for a PID and VID which are not in the config file.
    allowlist = ukip.check_local_allowlist('0x1234', '0x3472')

    # If the device cannot be found in the config file, return False.
    self.assertEqual(
        allowlist,
        ukip.AllowlistConfigReturn(allowlist=[], device_present=False))

  @mock.patch.object(builtins, 'open', autospec=True)
  def test_check_local_allowlist_key_val_parsing(self, open_mock):
    """Tests if the config file could be parsed into keys and values."""

    open_mock.return_value.__enter__ = open_mock
    open_mock.return_value.__iter__.return_value = iter([
        '# This is the config file\n', '# for UKIP.\n',
        'cannotparse\n'
    ])

    # Check if the exception was raised.
    self.assertRaises(ukip.AllowlistFileError, ukip.check_local_allowlist,
                      '0x1234', '0x3472')

  @mock.patch.object(builtins, 'open', autospec=True)
  def test_check_local_allowlist_device_parsing(self, open_mock):
    """Tests if the device in the config file can be parsed."""

    open_mock.return_value.__enter__ = open_mock
    open_mock.return_value.__iter__.return_value = iter([
        '# This is the config file\n', '# for UKIP.\n',
        '37843472 a,b,c\n'
    ])

    self.assertRaises(ukip.AllowlistFileError, ukip.check_local_allowlist,
                      '0x3784', '0x3472')

  @mock.patch.object(builtins, 'open', autospec=True)
  def test_check_local_allowlist_parsing(self, open_mock):
    """Tests if allowlist could be parsed from the config file."""

    open_mock.return_value.__enter__ = open_mock
    open_mock.return_value.__iter__.return_value = iter([
        '# This is the config file\n', '# for UKIP.\n',
        '0x3784:0x3472 cannotparse\n'
    ])

    # The device will be found, but the allowlist cannot be parsed.
    allowlist = ukip.check_local_allowlist('0x3784', '0x3472')

    # If the allowlist is a word, that is not 'any' or 'none', return False.
    self.assertEqual(
        allowlist,
        ukip.AllowlistConfigReturn(allowlist=[], device_present=False))

  @mock.patch.object(builtins, 'open', autospec=True)
  def test_check_local_allowlist_file_not_found(self, open_mock):
    """Tests if the config file could be found."""

    open_mock.side_effect = ukip.AllowlistFileError(
        'The config file /etc/ukip/allowlist could not be found: %s')

    self.assertRaises(ukip.AllowlistFileError, ukip.check_local_allowlist,
                      '0x3784', '0x3472')

  @mock.patch.object(builtins, 'open', autospec=True)
  def test_check_local_allowlist_empty_lines(self, open_mock):
    """Tests if the allowlist check returns False when only empty lines."""

    open_mock.return_value.__enter__ = open_mock

    # Prepare a fake file, with only empty lines.
    open_mock.return_value.__iter__.return_value = iter(
        ['\n', '    \n', '        \n'])

    # Lookup for a PID and VID.
    allowlist = ukip.check_local_allowlist('0x3784', '0x3472')

    # If there are only empty lines in the config file, return False.
    self.assertEqual(
        allowlist,
        ukip.AllowlistConfigReturn(allowlist=[], device_present=False))

  @mock.patch.object(builtins, 'open', autospec=True)
  def test_check_local_allowlist_allow_all(self, open_mock):
    """Tests if the allowlist check returns True for "allow all characters"."""

    open_mock.return_value.__enter__ = open_mock

    # Prepare a fake file, with only empty lines.
    open_mock.return_value.__iter__.return_value = iter([
        '0x1234:0x1337 any\n',
    ])

    # Lookup for a PID and VID.
    allowlist = ukip.check_local_allowlist('0x1234', '0x1337')

    # If all possible characters are allowed for a device, return an empty list
    # and True.
    self.assertEqual(
        allowlist,
        ukip.AllowlistConfigReturn(allowlist=[], device_present=True))

  @mock.patch.object(builtins, 'open', autospec=True)
  def test_check_local_allowlist_deny_all(self, open_mock):
    """Tests if the allowlist is an empty list when denying all characters."""

    open_mock.return_value.__enter__ = open_mock

    # Prepare a fake file, with only empty lines.
    open_mock.return_value.__iter__.return_value = iter([
        '0x1234:0x1337 none\n',
    ])

    # Lookup for a PID and VID.
    allowlist = ukip.check_local_allowlist('0x1234', '0x1337')

    # If no characters are allowed for the given device, return an empty list.
    self.assertEqual(
        allowlist,
        ukip.AllowlistConfigReturn(allowlist=[], device_present=False))

  def fill_test_ringbuffer_with_data(self):
    """A helper function to add times and trigger the hardening mode."""
    ukip.add_to_ring_buffer(self.event_device_path, 1555146977759524, 'a',
                            self.mock_pyusb_device)
    ukip.add_to_ring_buffer(self.event_device_path, 1555146977859525, 'b',
                            self.mock_pyusb_device)
    ukip.add_to_ring_buffer(self.event_device_path, 1555146977959526, 'c',
                            self.mock_pyusb_device)
    ukip.add_to_ring_buffer(self.event_device_path, 1555146977959527, 'd',
                            self.mock_pyusb_device)
    ukip.add_to_ring_buffer(self.event_device_path, 1555146977959528, 'e',
                            self.mock_pyusb_device)

  @mock.patch.object(gc, 'collect', wraps=gc.collect)
  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)
  @mock.patch.object(ukip, 'log', autospec=True)
  def test_enforce_hardening_mode_with_product(self, logging_mock,
                                               check_allowlist_mock, gc_mock):
    """Tests which logging message is emitted when device has a product set."""

    self.fill_test_ringbuffer_with_data()

    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])

    # Need a link, because after the function is run, the dicts are deleted.
    timings = ukip._event_devices_timings[self.event_device_path]

    # Return the allowlist from /etc/ukip/allowlist.
    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(
        allowlist=['a', 'b', 'c'], device_present=True)

    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)

    check_allowlist_mock.assert_called_once_with(
        hex(self.mock_pyusb_device.idProduct),
        hex(self.mock_pyusb_device.idVendor))

    # Only 1 interface, so the range is 0.
    self.mock_pyusb_device.detach_kernel_driver.assert_called_once_with(0)

    logging_mock.warning.assert_called_with(
        '[UKIP] The device %s with the vendor id %s and the product id %s '
        'was blocked. The causing timings were: %s.',
        self.mock_pyusb_device.product, hex(self.mock_pyusb_device.idVendor),
        hex(self.mock_pyusb_device.idProduct), timings)

    # The error was not logged.
    self.assertFalse(logging_mock.error.called)

    # The dicts are deleted now.
    self.assertFalse(ukip._event_devices_timings)
    self.assertFalse(ukip._event_devices_keystrokes)

    # And the garbage collector ran.
    gc_mock.assert_called_once()

  @mock.patch.object(gc, 'collect', wraps=gc.collect)
  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)
  @mock.patch.object(ukip, 'log', autospec=True)
  def test_enforce_hardening_mode_no_product(self, logging_mock,
                                             check_allowlist_mock, gc_mock):
    """Tests which logging message is emitted when device has no product set."""

    self.fill_test_ringbuffer_with_data()

    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])
    self.mock_pyusb_device.product = None

    # Need a link, because after the function is run, the dicts are deleted.
    timings = ukip._event_devices_timings[self.event_device_path]

    # Return the allowlist from /etc/ukip/allowlist.
    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(
        allowlist=['a', 'b', 'c'], device_present=True)

    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)

    check_allowlist_mock.assert_called_once_with(
        hex(self.mock_pyusb_device.idProduct),
        hex(self.mock_pyusb_device.idVendor))

    # Only 1 interface, so the range is 0.
    self.mock_pyusb_device.detach_kernel_driver.assert_called_once_with(0)

    logging_mock.warning.assert_called_with(
        '[UKIP] The device with the vendor id %s and the product id %s was '
        'blocked. The causing timings were: %s.',
        hex(self.mock_pyusb_device.idVendor),
        hex(self.mock_pyusb_device.idProduct), timings)

    self.assertFalse(logging_mock.error.called)

    # The dicts are deleted now.
    self.assertFalse(ukip._event_devices_timings)
    self.assertFalse(ukip._event_devices_keystrokes)

    # And the garbage collector ran.
    gc_mock.assert_called_once()

  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)
  @mock.patch.object(ukip, 'log', autospec=True)
  def test_enforce_hardening_mode_no_active_driver(self, logging_mock,
                                                   check_allowlist_mock):
    """Tests flow through function when no interface has an active driver."""

    self.fill_test_ringbuffer_with_data()

    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])
    self.mock_pyusb_device.is_kernel_driver_active.return_value = False

    # Return the allowlist from /etc/ukip/allowlist.
    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(
        allowlist=['a', 'b', 'c'], device_present=True)

    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)

    check_allowlist_mock.assert_called_once_with(
        hex(self.mock_pyusb_device.idProduct),
        hex(self.mock_pyusb_device.idVendor))

    self.assertFalse(self.mock_pyusb_device.detach_kernel_driver.called)
    self.assertFalse(logging_mock.warning.called)
    self.assertFalse(logging_mock.error.called)

  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)
  @mock.patch.object(ukip, 'log', autospec=True)
  def test_enforce_hardening_mode_ioerror(self, logging_mock,
                                          check_allowlist_mock):
    """Tests IOError/log message for unbinding a driver from an interface."""

    self.fill_test_ringbuffer_with_data()

    log_message = ('There was an error in unbinding the interface for the USB '
                   'device %s: %s')
    exception_message = '[Errno 16] Device or resource busy'
    exception_object = IOError(exception_message)

    product_id = hex(self.mock_pyusb_device.idProduct)
    vendor_id = hex(self.mock_pyusb_device.idVendor)
    pid_and_vid = '%s:%s' % (product_id, vendor_id)

    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])
    self.mock_pyusb_device.detach_kernel_driver.side_effect = exception_object

    # Return the allowlist from /etc/ukip/allowlist.
    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(
        allowlist=['a', 'b', 'c'], device_present=True)

    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)

    check_allowlist_mock.assert_called_once_with(
        hex(self.mock_pyusb_device.idProduct),
        hex(self.mock_pyusb_device.idVendor))

    logging_mock.warning.assert_called()

  @mock.patch.object(gc, 'collect', wraps=gc.collect)
  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)
  @mock.patch.object(ukip, 'log', autospec=True)
  def test_enforce_hardening_mode_multiple_interfaces_error(
      self, logging_mock, check_allowlist_mock, gc_mock):
    """Tests multiple interfaces, with one failing with an IOError."""

    self.fill_test_ringbuffer_with_data()

    log_message = ('There was an error in unbinding the interface for the USB '
                   'device %s: %s')
    exception_message = '[Errno 16] Device or resource busy'
    exception_object = IOError(exception_message)

    product_id = hex(self.mock_pyusb_device.idProduct)
    vendor_id = hex(self.mock_pyusb_device.idVendor)
    pid_and_vid = '%s:%s' % (product_id, vendor_id)

    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])
    self.mock_usb_config.bNumInterfaces = 2

    self.mock_pyusb_device.detach_kernel_driver.side_effect = [
        exception_object, mock.DEFAULT
    ]

    # Need a link, because after the function is run, the dicts are deleted.
    timings = ukip._event_devices_timings[self.event_device_path]

    # Return the allowlist from /etc/ukip/allowlist.
    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(
        allowlist=['a', 'b', 'c'], device_present=True)

    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)

    check_allowlist_mock.assert_called_once_with(
        hex(self.mock_pyusb_device.idProduct),
        hex(self.mock_pyusb_device.idVendor))

    call = [
        mock.call(
            '[UKIP] The device %s with the vendor id %s and the product id '
            '%s was blocked. The causing timings were: %s.',
            self.mock_pyusb_device.product,
            hex(self.mock_pyusb_device.idVendor),
            hex(self.mock_pyusb_device.idProduct), timings)
    ]
    logging_mock.warning.assert_has_calls(call)

    # The dicts are deleted now.
    self.assertFalse(ukip._event_devices_timings)
    self.assertFalse(ukip._event_devices_keystrokes)

    # And the garbage collector ran.
    gc_mock.assert_called_once()

  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)
  @mock.patch.object(ukip, 'log', autospec=True)
  def test_enforce_hardening_mode_oserror(self, logging_mock,
                                          check_allowlist_mock):
    """Tests OSError/log message for unbinding a driver from an interface."""

    self.fill_test_ringbuffer_with_data()

    log_message = ('There was an error in unbinding the interface for the USB '
                   'device %s: %s')
    exception_message = 'access violation'
    exception_object = OSError(exception_message)

    product_id = hex(self.mock_pyusb_device.idProduct)
    vendor_id = hex(self.mock_pyusb_device.idVendor)
    pid_and_vid = '%s:%s' % (product_id, vendor_id)

    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])
    self.mock_pyusb_device.detach_kernel_driver.side_effect = exception_object

    # Return the allowlist from /etc/ukip/allowlist.
    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(
        allowlist=['a', 'b', 'c'], device_present=True)

    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)

    check_allowlist_mock.assert_called_once_with(
        hex(self.mock_pyusb_device.idProduct),
        hex(self.mock_pyusb_device.idVendor))

    logging_mock.warning.assert_called()

  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)
  @mock.patch.object(ukip, 'log', autospec=True)
  def test_enforce_hardening_mode_valueerror(self, logging_mock,
                                             check_allowlist_mock):
    """Tests ValueError/log message for unbinding a driver from an interface."""

    self.fill_test_ringbuffer_with_data()

    log_message = ('There was an error in unbinding the interface for the USB '
                   'device %s: %s')
    exception_message = 'Invalid configuration'
    exception_object = ValueError(exception_message)

    product_id = hex(self.mock_pyusb_device.idProduct)
    vendor_id = hex(self.mock_pyusb_device.idVendor)
    pid_and_vid = '%s:%s' % (product_id, vendor_id)

    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])
    self.mock_pyusb_device.detach_kernel_driver.side_effect = exception_object

    # Return the allowlist from /etc/ukip/allowlist.
    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(
        allowlist=['a', 'b', 'c'], device_present=True)

    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)

    check_allowlist_mock.assert_called_once_with(
        hex(self.mock_pyusb_device.idProduct),
        hex(self.mock_pyusb_device.idVendor))

    logging_mock.warning.assert_called()

  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)
  @mock.patch.object(ukip, 'log', autospec=True)
  def test_enforce_hardening_mode_usberror(self, logging_mock,
                                           check_allowlist_mock):
    """Tests USBError/log message for unbinding a driver from an interface."""

    self.fill_test_ringbuffer_with_data()

    log_message = ('There was an error in unbinding the interface for the USB '
                   'device %s: %s')
    exception_message = 'USBError Accessing Configurations'
    exception_object = usb.core.USBError(exception_message)

    product_id = hex(self.mock_pyusb_device.idProduct)
    vendor_id = hex(self.mock_pyusb_device.idVendor)
    pid_and_vid = '%s:%s' % (product_id, vendor_id)

    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])
    self.mock_pyusb_device.detach_kernel_driver.side_effect = exception_object

    # Return the allowlist from /etc/ukip/allowlist.
    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(
        allowlist=['a', 'b', 'c'], device_present=True)

    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)

    check_allowlist_mock.assert_called_once_with(
        hex(self.mock_pyusb_device.idProduct),
        hex(self.mock_pyusb_device.idVendor))

    logging_mock.warning.assert_called()

  @mock.patch.object(gc, 'collect', wraps=gc.collect)
  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)
  @mock.patch.object(ukip, 'log', autospec=True)
  def test_enforce_hardening_mode_any_keyword(self, logging_mock,
                                              check_allowlist_mock, gc_mock):
    """Tests an early return if the any keyword is set in the allowlist."""

    self.fill_test_ringbuffer_with_data()

    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])

    # Device present and empty allowlist -> any keyword was set.
    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(
        allowlist=[], device_present=True)

    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)

    check_allowlist_mock.assert_called_once_with(
        hex(self.mock_pyusb_device.idProduct),
        hex(self.mock_pyusb_device.idVendor))

    # Due to the early return, none of the followup functions are called.
    self.assertFalse(self.mock_pyusb_device.detach_kernel_driver.called)
    self.assertFalse(logging_mock.called)
    self.assertFalse(gc_mock.called)

  @mock.patch.object(gc, 'collect', wraps=gc.collect)
  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)
  @mock.patch.object(ukip, 'log', autospec=True)
  def test_enforce_hardening_mode_keystrokes_allowed(self, logging_mock,
                                                     check_allowlist_mock,
                                                     gc_mock):
    """Tests an early return if the typed keys are allowed in the allowlist."""

    # This sets the typed keys to [a,b,c,d,e]
    self.fill_test_ringbuffer_with_data()

    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])

    # Device present and allowlist set to typed characters.
    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(
        allowlist=['a', 'b', 'c', 'd', 'e'], device_present=True)

    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)

    check_allowlist_mock.assert_called_once_with(
        hex(self.mock_pyusb_device.idProduct),
        hex(self.mock_pyusb_device.idVendor))

    # Due to the early return, none of the followup functions are called.
    self.assertFalse(self.mock_pyusb_device.detach_kernel_driver.called)
    self.assertFalse(logging_mock.called)
    self.assertFalse(gc_mock.called)

  @mock.patch.object(gc, 'collect', wraps=gc.collect)
  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)
  @mock.patch.object(ukip, 'log', autospec=True)
  def test_enforce_hardening_mode_keystrokes_allowed_subset(
      self, logging_mock, check_allowlist_mock, gc_mock):
    """Tests an early return with a subset of allowed keys."""

    ukip.add_to_ring_buffer(self.event_device_path, 1555146977759524, 'a',
                            self.mock_pyusb_device)
    ukip.add_to_ring_buffer(self.event_device_path, 1555146977859525, 'b',
                            self.mock_pyusb_device)
    ukip.add_to_ring_buffer(self.event_device_path, 1555146977959526, 'c',
                            self.mock_pyusb_device)

    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])

    # Device present and allowlist set to typed characters.
    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(
        allowlist=['a', 'b', 'c', 'd', 'e', 'f'], device_present=True)

    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)

    check_allowlist_mock.assert_called_once_with(
        hex(self.mock_pyusb_device.idProduct),
        hex(self.mock_pyusb_device.idVendor))

    # Due to the early return, none of the followup functions are called.
    self.assertFalse(self.mock_pyusb_device.detach_kernel_driver.called)
    self.assertFalse(logging_mock.called)
    self.assertFalse(gc_mock.called)

  @mock.patch.object(gc, 'collect', wraps=gc.collect)
  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)
  @mock.patch.object(ukip, 'log', autospec=True)
  def test_enforce_hardening_mode_device_not_present(self, logging_mock,
                                                     check_allowlist_mock,
                                                     gc_mock):
    """Tests function flow when the device is not present in the allowlist."""

    self.fill_test_ringbuffer_with_data()

    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])

    # Need a link, because after the function is run, the dicts are deleted.
    timings = ukip._event_devices_timings[self.event_device_path]

    # Return the allowlist from /etc/ukip/allowlist.
    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(
        allowlist=[], device_present=False)

    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)

    check_allowlist_mock.assert_called_once_with(
        hex(self.mock_pyusb_device.idProduct),
        hex(self.mock_pyusb_device.idVendor))

    # Only 1 interface, so the range is 0.
    self.mock_pyusb_device.detach_kernel_driver.assert_called_once_with(0)

    logging_mock.warning.assert_called_with(
        '[UKIP] The device %s with the vendor id %s and the product id %s '
        'was blocked. The causing timings were: %s.',
        self.mock_pyusb_device.product, hex(self.mock_pyusb_device.idVendor),
        hex(self.mock_pyusb_device.idProduct), timings)

    # The error was not logged.
    self.assertFalse(logging_mock.error.called)

    # The dicts are deleted now.
    self.assertFalse(ukip._event_devices_timings)
    self.assertFalse(ukip._event_devices_keystrokes)

    # And the garbage collector ran.
    gc_mock.assert_called_once()

  @mock.patch.object(gc, 'collect', wraps=gc.collect)
  @mock.patch.object(ukip, 'check_local_allowlist', autospec=True)
  @mock.patch.object(ukip, 'log', autospec=True)
  def test_enforce_hardening_mode_one_key_off(self, logging_mock,
                                              check_allowlist_mock, gc_mock):
    """Tests the hardening mode when one typed key is not allowed."""

    # This sets the typed keys to [a,b,c,d,e]
    self.fill_test_ringbuffer_with_data()

    self.mock_pyusb_device.__iter__.return_value = iter([self.mock_usb_config])

    # Need a link, because after the function is run, the dicts are deleted.
    timings = ukip._event_devices_timings[self.event_device_path]

    # Return the allowlist from /etc/ukip/allowlist. The 'e' from the typed
    # keys is not allowed.
    check_allowlist_mock.return_value = ukip.AllowlistConfigReturn(
        allowlist=['a', 'b', 'c', 'd', 'f'], device_present=False)

    ukip.enforce_hardening_mode(self.mock_pyusb_device, self.event_device_path)

    check_allowlist_mock.assert_called_once_with(
        hex(self.mock_pyusb_device.idProduct),
        hex(self.mock_pyusb_device.idVendor))

    # Only 1 interface, so the range is 0.
    self.mock_pyusb_device.detach_kernel_driver.assert_called_once_with(0)

    logging_mock.warning.assert_called_with(
        '[UKIP] The device %s with the vendor id %s and the product id %s '
        'was blocked. The causing timings were: %s.',
        self.mock_pyusb_device.product, hex(self.mock_pyusb_device.idVendor),
        hex(self.mock_pyusb_device.idProduct), timings)

    # The error was not logged.
    self.assertFalse(logging_mock.error.called)

    # The dicts are deleted now.
    self.assertFalse(ukip._event_devices_timings)
    self.assertFalse(ukip._event_devices_keystrokes)

    # And the garbage collector ran.
    gc_mock.assert_called_once()

  @mock.patch.object(ukip, 'log', autospec=True)
  @mock.patch.object(builtins, 'open')
  def test_load_keycodes_from_file(self, open_mock, logging_mock):
    """Tests if the keycode file returns the KeycodesReturn class."""

    handle = open_mock().__enter__.return_value

    keycode_file_content = [{
        'lowcodes': [{
            '1': 'ESC',
            '2': '1'
        }],
        'capscodes': [{
            '1': 'ESC',
            '2': '!'
        }]
    }]

    file_mock = mock.MagicMock(side_effect=keycode_file_content)
    json_mock = mock.patch('json.load', file_mock)

    with open_mock:
      with json_mock as json_load_mock:
        keycodes = ukip.load_keycodes_from_file()
        json_load_mock.assert_called_with(handle)

    self.assertEqual(keycodes.lower_codes, {1: 'ESC', 2: '1'})
    self.assertEqual(keycodes.capped_codes, {1: 'ESC', 2: '!'})
    logging_mock.assert_not_called()

  @mock.patch.object(ukip, 'log', autospec=True)
  @mock.patch.object(builtins, 'open')
  def test_load_keycodes_from_file_missing_keyword(self, open_mock,
                                                   logging_mock):
    """Tests the keycode file returns when a keyword is missing."""

    handle = open_mock().__enter__.return_value

    keycode_file_content = [{
        'not_low_codes': [{
            '1': 'ESC',
            '2': '1'
        }],
        'capscodes': [{
            '1': 'ESC',
            '2': '!'
        }]
    }]

    file_mock = mock.MagicMock(side_effect=keycode_file_content)
    json_mock = mock.patch('json.load', file_mock)

    with open_mock:
      with json_mock as json_load_mock:
        keycodes = ukip.load_keycodes_from_file()
        json_load_mock.assert_called_with(handle)

    # The lowcodes keyword is missing in the keycodes file.
    self.assertEqual(keycodes.lower_codes, {})
    self.assertEqual(keycodes.capped_codes, {})
    logging_mock.error.assert_called()

  @mock.patch.object(ukip, 'log', autospec=True)
  @mock.patch.object(json, 'load', autospec=True)
  @mock.patch.object(builtins, 'open', autospec=True)
  def test_load_keycodes_from_file_overflowerror(self, open_mock, json_mock,
                                                 logging_mock):
    """Tests if KeycodesFileError is raised on an OverflowError."""

    json_mock.side_effect = OverflowError
    self.assertRaises(ukip.KeycodesFileError, ukip.load_keycodes_from_file)
    open_mock.assert_called()
    json_mock.assert_called()
    logging_mock.assert_not_called()

  @mock.patch.object(ukip, 'log', autospec=True)
  @mock.patch.object(json, 'load', autospec=True)
  @mock.patch.object(builtins, 'open', autospec=True)
  def test_load_keycodes_from_file_valueerror(self, open_mock, json_mock,
                                              logging_mock):
    """Tests if KeycodesFileError is raised on a ValueError."""

    json_mock.side_effect = ValueError
    self.assertRaises(ukip.KeycodesFileError, ukip.load_keycodes_from_file)
    open_mock.assert_called()
    json_mock.assert_called()
    logging_mock.assert_not_called()

  @mock.patch.object(ukip, 'log', autospec=True)
  @mock.patch.object(json, 'load', autospec=True)
  @mock.patch.object(builtins, 'open', autospec=True)
  def test_load_keycodes_from_file_typeerror(self, open_mock, json_mock,
                                             logging_mock):
    """Tests if KeycodesFileError is raised on a TypeError."""

    json_mock.side_effect = TypeError
    self.assertRaises(ukip.KeycodesFileError, ukip.load_keycodes_from_file)
    open_mock.assert_called()
    json_mock.assert_called()
    logging_mock.assert_not_called()

  @mock.patch.object(ukip, 'log', autospec=True)
  @mock.patch.object(json, 'load', autospec=True)
  @mock.patch.object(builtins, 'open', autospec=True)
  def test_load_keycodes_from_file_not_found(self, open_mock, json_mock,
                                             logging_mock):
    """Tests if KeycodesFileError is raised on a FileNotFoundError."""

    json_mock.side_effect = FileNotFoundError
    self.assertRaises(ukip.KeycodesFileError, ukip.load_keycodes_from_file)
    open_mock.assert_called()
    json_mock.assert_called()
    logging_mock.assert_not_called()


if __name__ == '__main__':
  unittest.main()
Download .txt
gitextract_f7kggwew/

├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── data/
│   ├── allowlist
│   ├── keycodes
│   └── ukip.service
├── requirements.txt
├── setup.sh
└── src/
    ├── __init__.py
    ├── ukip.py
    └── ukip_test.py
Download .txt
SYMBOL INDEX (74 symbols across 2 files)

FILE: src/ukip.py
  class UKIP_AVAILABLE_MODES (line 40) | class UKIP_AVAILABLE_MODES(enum.Enum):
  class AllowlistConfigReturn (line 85) | class AllowlistConfigReturn(object):
  class KeycodesReturn (line 107) | class KeycodesReturn(object):
  class DeviceError (line 118) | class DeviceError(Exception):
  class AllowlistFileError (line 122) | class AllowlistFileError(Exception):
  class KeycodesFileError (line 126) | class KeycodesFileError(Exception):
  function add_to_ring_buffer (line 130) | def add_to_ring_buffer(event_device_path: Text, key_down_time: int,
  function check_local_allowlist (line 157) | def check_local_allowlist(product_id: Text,
  function check_for_attack (line 230) | def check_for_attack(event_device_path: Text, device: usb.core.Device) -...
  function enforce_monitor_mode (line 272) | def enforce_monitor_mode(device: usb.core.Device, event_device_path: Text):
  function enforce_hardening_mode (line 289) | def enforce_hardening_mode(device: usb.core.Device, event_device_path: T...
  function load_keycodes_from_file (line 363) | def load_keycodes_from_file() -> KeycodesReturn:
  function monitor_device_thread (line 401) | def monitor_device_thread(device: pyudev.Device, vendor_id: int,
  function init_device_list (line 473) | def init_device_list() -> int:
  function main (line 523) | def main(argv):

FILE: src/ukip_test.py
  class USBError (line 42) | class USBError(IOError):
  class UkipTest (line 46) | class UkipTest(unittest.TestCase):
    method setUp (line 48) | def setUp(self):
    method test_check_for_attack_trigger_monitor (line 106) | def test_check_for_attack_trigger_monitor(self, monitor_mode_mock):
    method test_check_for_attack_not_trigger_monitor (line 132) | def test_check_for_attack_not_trigger_monitor(self, monitor_mode_mock):
    method test_check_for_attack_no_times (line 154) | def test_check_for_attack_no_times(self, monitor_mode_mock):
    method test_check_for_attack_proper_run_mode (line 173) | def test_check_for_attack_proper_run_mode(self, monitor_mode_mock,
    method test_check_for_attack_no_run_mode (line 204) | def test_check_for_attack_no_run_mode(self, monitor_mode_mock,
    method test_add_to_ring_buffer_create_key_time (line 232) | def test_add_to_ring_buffer_create_key_time(self, check_for_attack_mock):
    method test_add_to_ring_buffer_create_key_keystroke (line 250) | def test_add_to_ring_buffer_create_key_keystroke(self, check_for_attac...
    method test_add_to_ring_buffer_multiple_values (line 268) | def test_add_to_ring_buffer_multiple_values(self, check_for_attack_mock):
    method test_enforce_monitor_mode_with_product (line 318) | def test_enforce_monitor_mode_with_product(self, logging_mock):
    method test_enforce_monitor_mode_no_product (line 333) | def test_enforce_monitor_mode_no_product(self, logging_mock):
    method test_monitor_device_thread_library_calls (line 351) | def test_monitor_device_thread_library_calls(self, usb_core_find_mock,
    method test_monitor_device_thread_logging (line 367) | def test_monitor_device_thread_logging(self):
    method test_monitor_device_thread_exception_inputdevice (line 373) | def test_monitor_device_thread_exception_inputdevice(
    method test_monitor_device_thread_exception_read_loop (line 394) | def test_monitor_device_thread_exception_read_loop(
    method test_monitor_device_thread_keystroke_in_ms (line 415) | def test_monitor_device_thread_keystroke_in_ms(self):
    method test_monitor_device_thread_keystroke_shift (line 419) | def test_monitor_device_thread_keystroke_shift(self):
    method test_monitor_device_thread_keystroke_capslock (line 423) | def test_monitor_device_thread_keystroke_capslock(self):
    method test_init_device_list_library_calls (line 429) | def test_init_device_list_library_calls(self, netlink_mock, context_mo...
    method test_init_device_list_exceptions (line 437) | def test_init_device_list_exceptions(self):
    method test_init_device_list_device_count (line 441) | def test_init_device_list_device_count(self):
    method test_init_device_list_invalid_pid_vid (line 445) | def test_init_device_list_invalid_pid_vid(self):
    method test_init_device_list_runtimeerror (line 449) | def test_init_device_list_runtimeerror(self):
    method test_main_threading (line 453) | def test_main_threading(self):
    method test_main_too_many_arguments (line 457) | def test_main_too_many_arguments(self):
    method test_main_filter_by (line 462) | def test_main_filter_by(self, netlink_mock):
    method test_check_local_allowlist (line 476) | def test_check_local_allowlist(self, open_mock):
    method test_check_local_allowlist_two_devices (line 497) | def test_check_local_allowlist_two_devices(self, open_mock):
    method test_check_local_allowlist_only_comments (line 518) | def test_check_local_allowlist_only_comments(self, open_mock):
    method test_check_local_allowlist_no_device (line 538) | def test_check_local_allowlist_no_device(self, open_mock):
    method test_check_local_allowlist_key_val_parsing (line 557) | def test_check_local_allowlist_key_val_parsing(self, open_mock):
    method test_check_local_allowlist_device_parsing (line 571) | def test_check_local_allowlist_device_parsing(self, open_mock):
    method test_check_local_allowlist_parsing (line 584) | def test_check_local_allowlist_parsing(self, open_mock):
    method test_check_local_allowlist_file_not_found (line 602) | def test_check_local_allowlist_file_not_found(self, open_mock):
    method test_check_local_allowlist_empty_lines (line 612) | def test_check_local_allowlist_empty_lines(self, open_mock):
    method test_check_local_allowlist_allow_all (line 630) | def test_check_local_allowlist_allow_all(self, open_mock):
    method test_check_local_allowlist_deny_all (line 650) | def test_check_local_allowlist_deny_all(self, open_mock):
    method fill_test_ringbuffer_with_data (line 668) | def fill_test_ringbuffer_with_data(self):
    method test_enforce_hardening_mode_with_product (line 684) | def test_enforce_hardening_mode_with_product(self, logging_mock,
    method test_enforce_hardening_mode_no_product (line 727) | def test_enforce_hardening_mode_no_product(self, logging_mock,
    method test_enforce_hardening_mode_no_active_driver (line 769) | def test_enforce_hardening_mode_no_active_driver(self, logging_mock,
    method test_enforce_hardening_mode_ioerror (line 794) | def test_enforce_hardening_mode_ioerror(self, logging_mock,
    method test_enforce_hardening_mode_multiple_interfaces_error (line 827) | def test_enforce_hardening_mode_multiple_interfaces_error(
    method test_enforce_hardening_mode_oserror (line 881) | def test_enforce_hardening_mode_oserror(self, logging_mock,
    method test_enforce_hardening_mode_valueerror (line 913) | def test_enforce_hardening_mode_valueerror(self, logging_mock,
    method test_enforce_hardening_mode_usberror (line 945) | def test_enforce_hardening_mode_usberror(self, logging_mock,
    method test_enforce_hardening_mode_any_keyword (line 978) | def test_enforce_hardening_mode_any_keyword(self, logging_mock,
    method test_enforce_hardening_mode_keystrokes_allowed (line 1004) | def test_enforce_hardening_mode_keystrokes_allowed(self, logging_mock,
    method test_enforce_hardening_mode_keystrokes_allowed_subset (line 1032) | def test_enforce_hardening_mode_keystrokes_allowed_subset(
    method test_enforce_hardening_mode_device_not_present (line 1063) | def test_enforce_hardening_mode_device_not_present(self, logging_mock,
    method test_enforce_hardening_mode_one_key_off (line 1107) | def test_enforce_hardening_mode_one_key_off(self, logging_mock,
    method test_load_keycodes_from_file (line 1151) | def test_load_keycodes_from_file(self, open_mock, logging_mock):
    method test_load_keycodes_from_file_missing_keyword (line 1181) | def test_load_keycodes_from_file_missing_keyword(self, open_mock,
    method test_load_keycodes_from_file_overflowerror (line 1214) | def test_load_keycodes_from_file_overflowerror(self, open_mock, json_m...
    method test_load_keycodes_from_file_valueerror (line 1227) | def test_load_keycodes_from_file_valueerror(self, open_mock, json_mock,
    method test_load_keycodes_from_file_typeerror (line 1240) | def test_load_keycodes_from_file_typeerror(self, open_mock, json_mock,
    method test_load_keycodes_from_file_not_found (line 1253) | def test_load_keycodes_from_file_not_found(self, open_mock, json_mock,
Condensed preview — 11 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (100K chars).
[
  {
    "path": "CONTRIBUTING.md",
    "chars": 1097,
    "preview": "# How to Contribute\n\nWe'd love to accept your patches and contributions to this project. There are\njust a few small guid"
  },
  {
    "path": "LICENSE",
    "chars": 11358,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "README.md",
    "chars": 5646,
    "preview": "# USB Keystroke Injection Protection\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://op"
  },
  {
    "path": "data/allowlist",
    "chars": 799,
    "preview": "# This is the allowlist for UKIP: USB Keystroke Injection Protection.\n#\n# Devices are added manually by a user in the fo"
  },
  {
    "path": "data/keycodes",
    "chars": 1805,
    "preview": "{\n  \"lowcodes\":\n  [{\n    \"1\": \"ESC\",\n    \"2\": \"1\",\n    \"3\": \"2\",\n    \"4\": \"3\",\n    \"5\": \"4\",\n    \"6\": \"5\",\n    \"7\": \"6\","
  },
  {
    "path": "data/ukip.service",
    "chars": 157,
    "preview": "[Unit]\nDescription=UKIP\nRequires=systemd-udevd.service\nAfter=systemd-udevd.service\n\n[Service]\nExecStart=/usr/sbin/ukip\n\n"
  },
  {
    "path": "requirements.txt",
    "chars": 67,
    "preview": "attrs==18.1.0\npyudev==0.21.0\nattr==0.3.1\nevdev==1.3.0\npyusb==1.0.2\n"
  },
  {
    "path": "setup.sh",
    "chars": 3706,
    "preview": "#!/bin/bash\n# Copyright 2019 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may no"
  },
  {
    "path": "src/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/ukip.py",
    "chars": 20297,
    "preview": "#!/usr/bin/env python3\n# Copyright 2020 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n#"
  },
  {
    "path": "src/ukip_test.py",
    "chars": 51591,
    "preview": "#!/usr/bin/env python3\n# Copyright 2020 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n#"
  }
]

About this extraction

This page contains the full source code of the google/ukip GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 11 files (94.3 KB), approximately 23.2k tokens, and a symbol index with 74 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!