diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 17:07:52 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 17:07:52 +0000 |
commit | f0f453c916e279980df981c1e1dee0d167dc124e (patch) | |
tree | d09973c9f173820ade2dc814467d3e57df8a042d /src/ssh_audit/software.py | |
parent | Initial commit. (diff) | |
download | ssh-audit-f0f453c916e279980df981c1e1dee0d167dc124e.tar.xz ssh-audit-f0f453c916e279980df981c1e1dee0d167dc124e.zip |
Adding upstream version 3.1.0.upstream/3.1.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/ssh_audit/software.py')
-rw-r--r-- | src/ssh_audit/software.py | 227 |
1 files changed, 227 insertions, 0 deletions
diff --git a/src/ssh_audit/software.py b/src/ssh_audit/software.py new file mode 100644 index 0000000..f13ebeb --- /dev/null +++ b/src/ssh_audit/software.py @@ -0,0 +1,227 @@ +""" + The MIT License (MIT) + + Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +""" +import re + +# pylint: disable=unused-import +from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 +from typing import Callable, Optional, Union, Any # noqa: F401 + +from ssh_audit.banner import Banner +from ssh_audit.product import Product + + +class Software: + def __init__(self, vendor: Optional[str], product: str, version: str, patch: Optional[str], os_version: Optional[str]) -> None: + self.__vendor = vendor + self.__product = product + self.__version = version + self.__patch = patch + self.__os = os_version + + @property + def vendor(self) -> Optional[str]: + return self.__vendor + + @property + def product(self) -> str: + return self.__product + + @property + def version(self) -> str: + return self.__version + + @property + def patch(self) -> Optional[str]: + return self.__patch + + @property + def os(self) -> Optional[str]: + return self.__os + + def compare_version(self, other: Union[None, 'Software', str]) -> int: + # pylint: disable=too-many-branches,too-many-return-statements + if other is None: + return 1 + if isinstance(other, Software): + other = '{}{}'.format(other.version, other.patch or '') + else: + other = str(other) + mx = re.match(r'^([\d\.]+\d+)(.*)$', other) + if mx is not None: + oversion, opatch = mx.group(1), mx.group(2).strip() + else: + oversion, opatch = other, '' + if self.version < oversion: + return -1 + elif self.version > oversion: + return 1 + spatch = self.patch or '' + if self.product == Product.DropbearSSH: + if not re.match(r'^test\d.*$', opatch): + opatch = 'z{}'.format(opatch) + if not re.match(r'^test\d.*$', spatch): + spatch = 'z{}'.format(spatch) + elif self.product == Product.OpenSSH: + mx1 = re.match(r'^p(\d).*', opatch) + mx2 = re.match(r'^p(\d).*', spatch) + if not (bool(mx1) and bool(mx2)): + if mx1 is not None: + opatch = mx1.group(1) + if mx2 is not None: + spatch = mx2.group(1) + # OpenBSD version and p1 versions are considered the same. + if ((spatch == '') and (opatch == '1')) or ((spatch == '1') and (opatch == '')): + return 0 + if spatch < opatch: + return -1 + elif spatch > opatch: + return 1 + return 0 + + def between_versions(self, vfrom: str, vtill: str) -> bool: + if bool(vfrom) and self.compare_version(vfrom) < 0: + return False + if bool(vtill) and self.compare_version(vtill) > 0: + return False + return True + + def display(self, full: bool = True) -> str: + r = '{} '.format(self.vendor) if bool(self.vendor) else '' + r += self.product + if bool(self.version): + r += ' {}'.format(self.version) + if full: + patch = self.patch or '' + if self.product == Product.OpenSSH: + mx = re.match(r'^(p\d)(.*)$', patch) + if mx is not None: + r += mx.group(1) + patch = mx.group(2).strip() + if bool(patch): + r += ' ({})'.format(patch) + if bool(self.os): + r += ' running on {}'.format(self.os) + return r + + def __str__(self) -> str: + return self.display() + + def __repr__(self) -> str: + r = 'vendor={}, '.format(self.vendor) if bool(self.vendor) else '' + r += 'product={}'.format(self.product) + if bool(self.version): + r += ', version={}'.format(self.version) + if bool(self.patch): + r += ', patch={}'.format(self.patch) + if bool(self.os): + r += ', os={}'.format(self.os) + return '<{}({})>'.format(self.__class__.__name__, r) + + @staticmethod + def _fix_patch(patch: str) -> Optional[str]: + return re.sub(r'^[-_\.]+', '', patch) or None + + @staticmethod + def _fix_date(d: Optional[str]) -> Optional[str]: + if d is not None and len(d) == 8: + return '{}-{}-{}'.format(d[:4], d[4:6], d[6:8]) + else: + return None + + @classmethod + def _extract_os_version(cls, c: Optional[str]) -> Optional[str]: + if c is None: + return None + mx = re.match(r'^NetBSD(?:_Secure_Shell)?(?:[\s-]+(\d{8})(.*))?$', c) + if mx is not None: + d = cls._fix_date(mx.group(1)) + return 'NetBSD' if d is None else 'NetBSD ({})'.format(d) + mx = re.match(r'^FreeBSD(?:\slocalisations)?[\s-]+(\d{8})(.*)$', c) + if not bool(mx): + mx = re.match(r'^[^@]+@FreeBSD\.org[\s-]+(\d{8})(.*)$', c) + if mx is not None: + d = cls._fix_date(mx.group(1)) + return 'FreeBSD' if d is None else 'FreeBSD ({})'.format(d) + w = ['RemotelyAnywhere', 'DesktopAuthority', 'RemoteSupportManager'] + for win_soft in w: + mx = re.match(r'^in ' + win_soft + r' ([\d\.]+\d)$', c) + if mx is not None: + ver = mx.group(1) + return 'Microsoft Windows ({} {})'.format(win_soft, ver) + generic = ['NetBSD', 'FreeBSD'] + for g in generic: + if c.startswith(g) or c.endswith(g): + return g + return None + + @classmethod + def parse(cls, banner: 'Banner') -> Optional['Software']: + # pylint: disable=too-many-return-statements + software = str(banner.software) + mx = re.match(r'^dropbear_([\d\.]+\d+)(.*)', software) + v: Optional[str] = None + if mx is not None: + patch = cls._fix_patch(mx.group(2)) + v, p = 'Matt Johnston', Product.DropbearSSH + v = None + return cls(v, p, mx.group(1), patch, None) + mx = re.match(r'^OpenSSH[_\.-]+([\d\.]+\d+)(.*)', software) + if mx is not None: + patch = cls._fix_patch(mx.group(2)) + v, p = 'OpenBSD', Product.OpenSSH + v = None + os_version = cls._extract_os_version(banner.comments) + return cls(v, p, mx.group(1), patch, os_version) + mx = re.match(r'^libssh-([\d\.]+\d+)(.*)', software) + if mx is not None: + patch = cls._fix_patch(mx.group(2)) + v, p = None, Product.LibSSH + os_version = cls._extract_os_version(banner.comments) + return cls(v, p, mx.group(1), patch, os_version) + mx = re.match(r'^libssh_([\d\.]+\d+)(.*)', software) + if mx is not None: + patch = cls._fix_patch(mx.group(2)) + v, p = None, Product.LibSSH + os_version = cls._extract_os_version(banner.comments) + return cls(v, p, mx.group(1), patch, os_version) + mx = re.match(r'^RomSShell_([\d\.]+\d+)(.*)', software) + if mx is not None: + patch = cls._fix_patch(mx.group(2)) + v, p = 'Allegro Software', 'RomSShell' + return cls(v, p, mx.group(1), patch, None) + mx = re.match(r'^mpSSH_([\d\.]+\d+)', software) + if mx is not None: + v, p = 'HP', 'iLO (Integrated Lights-Out) sshd' + return cls(v, p, mx.group(1), None, None) + mx = re.match(r'^Cisco-([\d\.]+\d+)', software) + if mx is not None: + v, p = 'Cisco', 'IOS/PIX sshd' + return cls(v, p, mx.group(1), None, None) + mx = re.match(r'^tinyssh_(.*)', software) + if mx is not None: + return cls(None, Product.TinySSH, mx.group(1), None, None) + mx = re.match(r'^PuTTY_Release_(.*)', software) + if mx: + return cls(None, Product.PuTTY, mx.group(1), None, None) + return None |