- 
          
- 
                Notifications
    You must be signed in to change notification settings 
- Fork 172
New Monitor: remote queries via ssh #1398
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 12 commits
ca45f85
              cb41f6c
              4cb591c
              fa72565
              475bbee
              7ee06a5
              a4cae21
              246011f
              fd8abdd
              bae0764
              954e8c8
              4ccfe69
              074399d
              7e409c2
              1e9b02f
              f5bb0ef
              1f88704
              c3a0d2b
              c2bddf8
              651745f
              9f7c760
              10c2e14
              3e44514
              f665a31
              63a4187
              931e705
              8362dba
              9959c7e
              b581907
              b8d73ee
              b75689c
              97566cc
              2e087ed
              62cf843
              2239702
              c931224
              8cb51f8
              d2c93d0
              0b219cf
              06f9dd4
              093302b
              275cd0a
              9b6dd68
              5c798ad
              0c0348c
              7ecff22
              b5d2f56
              14c993f
              f08db80
              f5ab22b
              c406477
              c0b5d2d
              597d247
              e6408bb
              7bb1d29
              b0c0b5d
              7969cfc
              fa690f2
              09f0979
              aa483be
              ed7f97e
              a28f638
              bca5bfe
              fe4481d
              55dbd73
              8a44e58
              0cceeed
              ada3bfb
              6b947b4
              17b8603
              aec1d41
              486b2b6
              2f06602
              dcb2c12
              fea7d5a
              d7daf07
              4480790
              b06d65b
              71f5c54
              696863c
              df1442e
              5dd25f6
              cbebdb1
              b29847a
              0ec151b
              eb92c0f
              ad94d9d
              72307f6
              00e99e4
              64d31b0
              ffbe7b2
              248d8c2
              e79aae1
              f7dc196
              9b5711e
              214e4f0
              7267de1
              68f3358
              2b3284e
              0e18428
              11e7a26
              e800221
              40760a2
              eaa32df
              eee69ba
              c879c58
              00dd778
              c5fe7a0
              aa6cd5d
              7ccca0c
              3e116d5
              4453a2f
              f41778e
              d4cc356
              c9fdf7d
              b6e762a
              8041217
              6d9153f
              378ca2b
              b1a41ce
              114db3f
              de4b6b1
              110493e
              f3b5976
              583bae8
              705b918
              dd5a971
              c671614
              2c976bc
              934446d
              3b36d85
              9fbb1e4
              91a7f98
              4ac6aa7
              24e804a
              0730f1d
              26ae960
              ef4e516
              f1bca57
              c1e0f25
              6b55a0a
              2e89de2
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -45,3 +45,4 @@ docs/_build | |
| .tox | ||
| pyrightconfig.json | ||
| simplemonitor/html/node_modules | ||
| .venv | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| remote_ssh - Monitor Remote Entities With SSH | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
|  | ||
| ``remote_ssh`` is a generic Monitor intended to be used for remote machines that do not have Simplemonitor installed. | ||
| It connects with ``ssh`` to run a remote command, then parses the reply and monitors the result. | ||
|  | ||
| .. warning:: | ||
| This Monitor uses bare `ssh` commands, in the context of the user they are run againt. This means that you can break everything and a little bit more if you are not careful. | ||
|  | ||
| You should also consider the security risk of having an SSH private key on the monotoring machine. And while we are at the topic of cybersecurity, you should ensure that the SSH command is not injected (this is not liklely if you do not dynamically generate `monitors.ini`) | ||
|  | ||
| The sequence of this Monitor is to send to ``ssh_username@target_host:target_port`` the ``command`` via SSH, retrieve the output and parse it with ``regex`` to extract a value. | ||
|  | ||
| This value is then compared with ``target_value`` with ``operator``. A failed comparison raises an alert. | ||
|  | ||
| .. info:: | ||
| This Monitor is limited in the edge cases it can manage. If you use a command with a predictible output and a proper regex you are good. If you start to tinker or have a regex that is not solid you may crash your Monitor (which just means you have to correct something) | ||
|  | ||
|  | ||
| .. confval:: description | ||
|  | ||
| :type: string | ||
| :required: false | ||
|  | ||
| The description of the Monitor which is sent back upon configuring an instance of this Monitor. Fallsback to a generic description. | ||
|  | ||
| .. confval:: target_hostname | ||
|  | ||
| :type: string | ||
| :required: true | ||
|  | ||
| The remote host to run the command on. | ||
|  | ||
| .. confval:: target_port | ||
|  | ||
| :type: int | ||
| :required: true | ||
|  | ||
| The port SSH runs on (on the remote server). Defaults to 22. | ||
|  | ||
| .. confval:: ssh_username | ||
|  | ||
| :type: string | ||
| :required: true | ||
|  | ||
| Login username. | ||
|  | ||
| .. confval:: ssh_private_key_path | ||
|  | ||
| :type: string | ||
| :required: true | ||
|  | ||
| The absolute path to the OpenSSH *private* key to login on the remote server. The remote server must have a corresponding entry in ``authorized_keys`` for the user that connects. | ||
|  | ||
| .. confval:: command | ||
|  | ||
| :type: string | ||
| :required: true | ||
|  | ||
| The command to run. It will use the context of the logged-in user and it is recommended to use absolute pathnames for commands. It is best to test the command by logging in as ``ssh_username`` and trying the command at the prompt. | ||
|  | ||
| .. confval:: regex | ||
|  | ||
| :type: string | ||
| :required: true | ||
|  | ||
| The regular expression the output of the command above will be matched to. | ||
|  | ||
| * Make sure to have one matching group - this is the value that will be checked | ||
| * Do not escape the sequences (i.e. use ``\s`` in the configuration when you mean "whitespace") | ||
| * A fantastic site to check your regex is https://regex101.com (do not block their ads!) | ||
|  | ||
| .. confval:: result_type | ||
|  | ||
| :type: string | ||
| :required: true | ||
|  | ||
| The type of the extracted value. Can be ``str`` (a string) or ``int`` (a number) | ||
|  | ||
| .. confval:: target_value | ||
|  | ||
| :type: string | ||
| :required: true | ||
|  | ||
| The value to compare extracted results with. Must be of the same type as the extracted value. | ||
|  | ||
| .. confval:: operator | ||
|  | ||
| :type: string | ||
| :required: true | ||
|  | ||
| The operator that compares the extracted value with ``target_value``. The possible operators are: | ||
|  | ||
| * ``equals`` - works with numbers and strings | ||
| * ``not_equals`` - works with number and strings | ||
| * ``greater_than`` - works with numbers | ||
| * ``less_than`` - works with numbers | ||
|  | ||
| .. confval:: success_message | ||
|  | ||
| :type: string | ||
| :required: false | ||
|  | ||
| A templated message for monitoring success. It must be a string `compatible with ``.format()`` https://docs.python.org/3/tutorial/inputoutput.html#the-string-format-method`_. You can use one bracket (``{}``) which will be replaced with the extracted value. | ||
|  | ||
| An example of a full configuration that checks if the ``/dev/sda`` disk on machine ``srv.example.com`:2255`` has more that 10% of free space available: | ||
|  | ||
| .. code-block:: | ||
|  | ||
| [srv] | ||
| type = remote_ssh | ||
| description=check disk space on srv | ||
| command = df -k | grep /dev/sda | ||
| ssh_private_key_path = C:\Users\mark\.ssh\srv.private.openssh | ||
| ssh_username = root | ||
| target_hostname = srv.example.com | ||
| target_port = 2255 | ||
| regex = .*\s(\d+)% | ||
| operator = greater_than | ||
| target_value = 10 | ||
| result_type = int | ||
| success_message=free disk {}% | 
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| """ | ||
| Remote command via ssh to check for stuff without simplemonitor on the remote target | ||
| Input: | ||
| - command to execute | ||
| - regex to extract the monitored value | ||
| - expected value | ||
| - logic to apply | ||
| """ | ||
|  | ||
| from venv import logger | ||
| from .monitor import Monitor, register | ||
| from enum import Enum | ||
| import paramiko | ||
| import re | ||
| from typing import Tuple, cast | ||
|  | ||
|  | ||
| class Operator(Enum): | ||
| EQUALS = "equals" | ||
| NOT_EQUALS = "not_equals" | ||
| GREATER_THAN = "greater_than" | ||
| LESS_THAN = "less_than" | ||
|  | ||
|  | ||
| class OperatorType(Enum): | ||
| STRING = "str" | ||
| INTEGER = "int" | ||
|  | ||
|  | ||
| @register | ||
| class MonitorRemoteSSH(Monitor): | ||
|  | ||
| monitor_type = "remote_ssh" | ||
|  | ||
| def __init__(self, name: str, config_options: dict) -> None: | ||
| super().__init__(name, config_options) | ||
| # description | ||
| self.description = cast(str, self.get_config_option("description", required=False)) # maybe define default here instead of a try: ? | ||
| self.success_message = cast(str, self.get_config_option("success_message", required=False, default="it worked")) | ||
| # ssh configuration | ||
| self.command = cast(str, self.get_config_option("command", required=True)) | ||
| self.ssh_private_key_path = cast(str, self.get_config_option("ssh_private_key_path", required=True)) | ||
| self.ssh_username = cast(str, self.get_config_option("ssh_username", required=True)) | ||
| self.target_hostname = cast(str, self.get_config_option("target_hostname", required=True)) | ||
| self.target_port = cast(int, self.get_config_option("target_port", required=False, default="22")) | ||
| # operator logic | ||
| self.operator = cast( | ||
| str, | ||
| self.get_config_option( | ||
| "operator", | ||
| required=True, | ||
| allowed_values=[Operator.EQUALS.value, Operator.NOT_EQUALS.value, Operator.LESS_THAN.value, Operator.GREATER_THAN.value], | ||
| ), | ||
| ) | ||
| self.regex = re.compile(cast(str, self.get_config_option("regex", required=True))) | ||
| # values to compare, cast to the expected type | ||
| self.result_type = cast( | ||
| str, | ||
| self.get_config_option("result_type", required=True, allowed_values=[OperatorType.INTEGER.value, OperatorType.STRING.value]), | ||
| ) | ||
| match self.result_type: | ||
| case OperatorType.INTEGER.value: | ||
| self.target_value = cast(int, self.get_config_option("target_value", required=True)) | ||
| case OperatorType.STRING.value: | ||
| self.target_value = cast(str, self.get_config_option("target_value", required=True)) | ||
|  | ||
| def run_test(self) -> bool: | ||
| # run remote command | ||
| client = paramiko.SSHClient() | ||
| client.set_missing_host_key_policy(paramiko.AutoAddPolicy) | ||
|         
                  github-advanced-security[bot] marked this conversation as resolved.
              Fixed
          
            Show fixed
            Hide fixed | ||
| try: | ||
| client.connect( | ||
| self.target_hostname, | ||
| username=self.ssh_username, | ||
| key_filename=self.ssh_private_key_path, | ||
| port=self.target_port, | ||
| ) | ||
| except TimeoutError: | ||
| return self.record_fail(f"connection to {self.target_hostname} timed out") | ||
| except ConnectionRefusedError: | ||
| return self.record_fail(f"connection to {self.target_hostname} actively refused") | ||
| except Exception as e: | ||
|         
                  wsw70 marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| return self.record_fail(f"connection to {self.target_hostname} failed: {e}") | ||
| else: | ||
| _, stdout, _ = client.exec_command(self.command) | ||
|  | ||
| # extract and cast the actual value | ||
| command_result = stdout.read().decode("utf-8") # let's hope for the best | ||
|         
                  wsw70 marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| client.close() # it's only now that we are done with the client | ||
| actual_value = re.match(self.regex, command_result).groups()[0] | ||
|          | ||
| match self.result_type: | ||
| case OperatorType.INTEGER.value: | ||
| actual_value = cast(int, actual_value) | ||
| case OperatorType.STRING.value: | ||
| actual_value = cast(str, actual_value) | ||
|  | ||
| # assess the comparison logic. str and int can be checked for equality, only int can be checked for greater/less than | ||
| test_succeeded = False # better be pessimistic | ||
| match self.operator: | ||
| case Operator.EQUALS.value: | ||
| test_succeeded = actual_value == self.target_value | ||
| case Operator.NOT_EQUALS.value: | ||
| test_succeeded = actual_value != self.target_value | ||
| case Operator.GREATER_THAN.value: | ||
| test_succeeded = actual_value > self.target_value | ||
| case Operator.LESS_THAN.value: | ||
| test_succeeded = actual_value < self.target_value | ||
| if self.result_type == OperatorType.STRING.value and (self.operator in [Operator.GREATER_THAN.value, Operator.LESS_THAN.value]): | ||
| logger.warning(f"strings compared with '{self.operator}'") | ||
| if test_succeeded: | ||
| return self.record_success(self.success_message.format(actual_value)) | ||
| else: | ||
| return self.record_fail(f"actual value: {actual_value} | operator: {self.operator} | target value: {self.target_value}") | ||
|          | ||
|  | ||
| def get_params(self) -> Tuple: | ||
| return ( | ||
| self.command, | ||
| self.description, | ||
| self.success_message, | ||
| self.regex, | ||
| self.target_value, | ||
| self.operator, | ||
| self.result_type, | ||
| self.ssh_private_key_path, | ||
| self.ssh_username, | ||
| self.target_hostname, | ||
| self.target_port, | ||
| ) | ||
|  | ||
| def describe(self) -> str: | ||
| try: | ||
| return self.description | ||
| except AttributeError: | ||
| return "run a remote command, extract its output and apply logic" | ||
Uh oh!
There was an error while loading. Please reload this page.