python-gvm 24.3.0__py3-none-any.whl → 24.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gvm/__version__.py +1 -1
- gvm/_enum.py +11 -2
- gvm/connections/__init__.py +32 -0
- gvm/connections/_connection.py +124 -0
- gvm/connections/_debug.py +74 -0
- gvm/connections/_ssh.py +352 -0
- gvm/connections/_tls.py +104 -0
- gvm/connections/_unix.py +54 -0
- gvm/errors.py +22 -8
- gvm/protocols/__init__.py +2 -7
- gvm/protocols/_protocol.py +137 -0
- gvm/protocols/core/__init__.py +15 -0
- gvm/protocols/core/_connection.py +201 -0
- gvm/protocols/core/_request.py +16 -0
- gvm/protocols/core/_response.py +114 -0
- gvm/protocols/gmp/__init__.py +30 -0
- gvm/protocols/gmp/_gmp.py +118 -0
- gvm/protocols/gmp/_gmp224.py +4300 -0
- gvm/protocols/gmp/_gmp225.py +95 -0
- gvm/protocols/gmp/requests/__init__.py +11 -0
- gvm/protocols/gmp/requests/_entity_id.py +8 -0
- gvm/protocols/gmp/requests/_version.py +15 -0
- gvm/protocols/gmp/requests/v224/__init__.py +124 -0
- gvm/protocols/{gmpv208/system/aggregates.py → gmp/requests/v224/_aggregates.py} +55 -53
- gvm/protocols/{gmpv208/entities/alerts.py → gmp/requests/v224/_alerts.py} +172 -213
- gvm/protocols/{gmpv208/entities/audits.py → gmp/requests/v224/_audits.py} +173 -244
- gvm/protocols/gmp/requests/v224/_auth.py +77 -0
- gvm/protocols/gmp/requests/v224/_cert_bund_advisories.py +67 -0
- gvm/protocols/gmp/requests/v224/_cpes.py +68 -0
- gvm/protocols/{gmpv208/entities/credentials.py → gmp/requests/v224/_credentials.py} +125 -121
- gvm/protocols/gmp/requests/v224/_cves.py +68 -0
- gvm/protocols/gmp/requests/v224/_dfn_cert_advisories.py +68 -0
- gvm/protocols/{gmpv208/entities/entities.py → gmp/requests/v224/_entity_type.py} +1 -2
- gvm/protocols/gmp/requests/v224/_feed.py +46 -0
- gvm/protocols/{gmpv208/entities/filter.py → gmp/requests/v224/_filters.py} +54 -69
- gvm/protocols/{gmpv208/entities/groups.py → gmp/requests/v224/_groups.py} +51 -58
- gvm/protocols/{gmpv208/system/help.py → gmp/requests/v224/_help.py} +9 -17
- gvm/protocols/{gmpv208/entities/hosts.py → gmp/requests/v224/_hosts.py} +41 -42
- gvm/protocols/gmp/requests/v224/_notes.py +230 -0
- gvm/protocols/gmp/requests/v224/_nvts.py +253 -0
- gvm/protocols/{gmpv208/entities/operating_systems.py → gmp/requests/v224/_operating_systems.py} +39 -43
- gvm/protocols/{gmpv208/entities/overrides.py → gmp/requests/v224/_overrides.py} +115 -174
- gvm/protocols/{gmpv208/entities/permissions.py → gmp/requests/v224/_permissions.py} +87 -102
- gvm/protocols/{gmpv208/entities/policies.py → gmp/requests/v224/_policies.py} +124 -130
- gvm/protocols/{gmpv208/entities/port_lists.py → gmp/requests/v224/_port_lists.py} +68 -80
- gvm/protocols/{gmpv208/entities/report_formats.py → gmp/requests/v224/_report_formats.py} +65 -88
- gvm/protocols/{gmpv208/entities/reports.py → gmp/requests/v224/_reports.py} +45 -55
- gvm/protocols/{gmpv208/entities/results.py → gmp/requests/v224/_results.py} +20 -21
- gvm/protocols/{gmpv208/entities/roles.py → gmp/requests/v224/_roles.py} +48 -57
- gvm/protocols/{gmpv224/entities/scan_configs.py → gmp/requests/v224/_scan_configs.py} +148 -220
- gvm/protocols/{gmpv208/entities/scanners.py → gmp/requests/v224/_scanners.py} +143 -162
- gvm/protocols/{gmpv208/entities/schedules.py → gmp/requests/v224/_schedules.py} +56 -60
- gvm/protocols/gmp/requests/v224/_secinfo.py +100 -0
- gvm/protocols/{gmpv208/system/system_reports.py → gmp/requests/v224/_system_reports.py} +15 -13
- gvm/protocols/{gmpv208/entities/tags.py → gmp/requests/v224/_tags.py} +75 -92
- gvm/protocols/{gmpv208/entities/targets.py → gmp/requests/v224/_targets.py} +178 -177
- gvm/protocols/{gmpv208/entities/tasks.py → gmp/requests/v224/_tasks.py} +129 -193
- gvm/protocols/{gmpv208/entities/tickets.py → gmp/requests/v224/_tickets.py} +65 -76
- gvm/protocols/{gmpv208/entities/tls_certificates.py → gmp/requests/v224/_tls_certificates.py} +50 -60
- gvm/protocols/gmp/requests/v224/_trashcan.py +39 -0
- gvm/protocols/{gmpv208/system/user_settings.py → gmp/requests/v224/_user_settings.py} +25 -27
- gvm/protocols/gmp/requests/v224/_users.py +235 -0
- gvm/protocols/gmp/requests/v224/_vulnerabilities.py +46 -0
- gvm/protocols/gmp/requests/v225/__init__.py +144 -0
- gvm/protocols/{gmpv225/entities/resourcenames.py → gmp/requests/v225/_resource_names.py} +31 -43
- gvm/protocols/latest.py +3 -51
- gvm/protocols/next.py +3 -51
- gvm/protocols/ospv1.py +83 -91
- gvm/transforms.py +10 -8
- gvm/utils.py +105 -24
- gvm/xml.py +109 -47
- {python_gvm-24.3.0.dist-info → python_gvm-24.6.0.dist-info}/METADATA +10 -4
- python_gvm-24.6.0.dist-info/RECORD +78 -0
- gvm/connections.py +0 -636
- gvm/protocols/base.py +0 -129
- gvm/protocols/gmp.py +0 -119
- gvm/protocols/gmpv208/__init__.py +0 -183
- gvm/protocols/gmpv208/entities/__init__.py +0 -4
- gvm/protocols/gmpv208/entities/notes.py +0 -264
- gvm/protocols/gmpv208/entities/scan_configs.py +0 -626
- gvm/protocols/gmpv208/entities/secinfo.py +0 -620
- gvm/protocols/gmpv208/entities/users.py +0 -278
- gvm/protocols/gmpv208/entities/vulnerabilities.py +0 -51
- gvm/protocols/gmpv208/system/__init__.py +0 -4
- gvm/protocols/gmpv208/system/authentication.py +0 -101
- gvm/protocols/gmpv208/system/feed.py +0 -55
- gvm/protocols/gmpv208/system/trashcan.py +0 -42
- gvm/protocols/gmpv208/system/version.py +0 -31
- gvm/protocols/gmpv214/__init__.py +0 -187
- gvm/protocols/gmpv214/entities/__init__.py +0 -4
- gvm/protocols/gmpv214/entities/notes.py +0 -167
- gvm/protocols/gmpv214/entities/overrides.py +0 -196
- gvm/protocols/gmpv214/entities/scanners.py +0 -204
- gvm/protocols/gmpv214/entities/targets.py +0 -245
- gvm/protocols/gmpv214/entities/users.py +0 -110
- gvm/protocols/gmpv214/system/__init__.py +0 -4
- gvm/protocols/gmpv214/system/version.py +0 -23
- gvm/protocols/gmpv224/__init__.py +0 -189
- gvm/protocols/gmpv224/entities/__init__.py +0 -4
- gvm/protocols/gmpv224/entities/scanners.py +0 -201
- gvm/protocols/gmpv224/entities/users.py +0 -176
- gvm/protocols/gmpv224/system/__init__.py +0 -4
- gvm/protocols/gmpv224/system/version.py +0 -23
- gvm/protocols/gmpv225/__init__.py +0 -197
- gvm/protocols/gmpv225/entities/__init__.py +0 -4
- gvm/protocols/gmpv225/system/__init__.py +0 -4
- gvm/protocols/gmpv225/system/version.py +0 -22
- python_gvm-24.3.0.dist-info/RECORD +0 -80
- /gvm/protocols/{gmpv208/entities/severity.py → gmp/requests/v224/_severity.py} +0 -0
- {python_gvm-24.3.0.dist-info → python_gvm-24.6.0.dist-info}/LICENSE +0 -0
- {python_gvm-24.3.0.dist-info → python_gvm-24.6.0.dist-info}/WHEEL +0 -0
gvm/__version__.py
CHANGED
gvm/_enum.py
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
#
|
|
5
5
|
|
|
6
6
|
from enum import Enum as PythonEnum
|
|
7
|
-
from typing import Optional
|
|
7
|
+
from typing import Any, Optional
|
|
8
8
|
|
|
9
9
|
from typing_extensions import Self
|
|
10
10
|
|
|
@@ -16,6 +16,12 @@ class Enum(PythonEnum):
|
|
|
16
16
|
Base class for Enums in python-gvm
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
+
@classmethod
|
|
20
|
+
def _missing_(cls, value: Any) -> Optional[Self]:
|
|
21
|
+
if isinstance(value, PythonEnum):
|
|
22
|
+
return cls.from_string(value.name)
|
|
23
|
+
return cls.from_string(str(value) if value else None)
|
|
24
|
+
|
|
19
25
|
@classmethod
|
|
20
26
|
def from_string(
|
|
21
27
|
cls,
|
|
@@ -33,6 +39,9 @@ class Enum(PythonEnum):
|
|
|
33
39
|
return cls[value.replace(" ", "_").upper()]
|
|
34
40
|
except KeyError:
|
|
35
41
|
raise InvalidArgument(
|
|
36
|
-
f"Invalid argument {value}
|
|
42
|
+
f"Invalid argument {value}. "
|
|
37
43
|
f"Allowed values are {','.join(e.name for e in cls)}."
|
|
38
44
|
) from None
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
return self.value
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Greenbone AG
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from ._connection import DEFAULT_TIMEOUT, GvmConnection
|
|
6
|
+
from ._debug import DebugConnection
|
|
7
|
+
from ._ssh import (
|
|
8
|
+
DEFAULT_HOSTNAME,
|
|
9
|
+
DEFAULT_KNOWN_HOSTS_FILE,
|
|
10
|
+
DEFAULT_SSH_PASSWORD,
|
|
11
|
+
DEFAULT_SSH_PORT,
|
|
12
|
+
DEFAULT_SSH_USERNAME,
|
|
13
|
+
SSHConnection,
|
|
14
|
+
)
|
|
15
|
+
from ._tls import DEFAULT_GVM_PORT, TLSConnection
|
|
16
|
+
from ._unix import DEFAULT_UNIX_SOCKET_PATH, UnixSocketConnection
|
|
17
|
+
|
|
18
|
+
__all__ = (
|
|
19
|
+
"DEFAULT_TIMEOUT",
|
|
20
|
+
"DEFAULT_UNIX_SOCKET_PATH",
|
|
21
|
+
"DEFAULT_GVM_PORT",
|
|
22
|
+
"DEFAULT_HOSTNAME",
|
|
23
|
+
"DEFAULT_KNOWN_HOSTS_FILE",
|
|
24
|
+
"DEFAULT_SSH_PASSWORD",
|
|
25
|
+
"DEFAULT_SSH_USERNAME",
|
|
26
|
+
"DEFAULT_SSH_PORT",
|
|
27
|
+
"DebugConnection",
|
|
28
|
+
"GvmConnection",
|
|
29
|
+
"SSHConnection",
|
|
30
|
+
"TLSConnection",
|
|
31
|
+
"UnixSocketConnection",
|
|
32
|
+
)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Greenbone AG
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import socket as socketlib
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from time import time
|
|
9
|
+
from typing import Optional, Protocol, Union, runtime_checkable
|
|
10
|
+
|
|
11
|
+
from gvm.errors import GvmError
|
|
12
|
+
|
|
13
|
+
BUF_SIZE = 16 * 1024
|
|
14
|
+
|
|
15
|
+
DEFAULT_TIMEOUT = 60 # in seconds
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@runtime_checkable
|
|
21
|
+
class GvmConnection(Protocol):
|
|
22
|
+
"""
|
|
23
|
+
Python `protocol <https://docs.python.org/3/library/typing.html#typing.Protocol>`_
|
|
24
|
+
for GvmConnection classes.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def connect(self) -> None:
|
|
28
|
+
"""Establish a connection to a remote server"""
|
|
29
|
+
|
|
30
|
+
def disconnect(self) -> None:
|
|
31
|
+
"""Send data to the connected remote server
|
|
32
|
+
|
|
33
|
+
Arguments:
|
|
34
|
+
data: Data to be send to the server. Either utf-8 encoded string or
|
|
35
|
+
bytes.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def send(self, data: bytes) -> None:
|
|
39
|
+
"""Send data to the connected remote server
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
data: Data to be send to the server as bytes.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def read(self) -> bytes:
|
|
46
|
+
"""Read data from the remote server
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
data as bytes
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def finish_send(self):
|
|
53
|
+
"""Indicate to the remote server you are done with sending data"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AbstractGvmConnection(ABC):
|
|
57
|
+
"""
|
|
58
|
+
Base class for establishing a connection to a remote server daemon.
|
|
59
|
+
|
|
60
|
+
Arguments:
|
|
61
|
+
timeout: Timeout in seconds for the connection. None to
|
|
62
|
+
wait indefinitely
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, timeout: Optional[Union[int, float]] = DEFAULT_TIMEOUT):
|
|
66
|
+
self._socket: Optional[socketlib.SocketType] = None
|
|
67
|
+
self._timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
|
|
68
|
+
|
|
69
|
+
def _read(self) -> bytes:
|
|
70
|
+
if self._socket is None:
|
|
71
|
+
raise GvmError("Socket is not connected")
|
|
72
|
+
|
|
73
|
+
return self._socket.recv(BUF_SIZE)
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def connect(self) -> None:
|
|
77
|
+
"""Establish a connection to a remote server"""
|
|
78
|
+
raise NotImplementedError
|
|
79
|
+
|
|
80
|
+
def send(self, data: bytes) -> None:
|
|
81
|
+
"""Send data to the connected remote server
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
data: Data to be send to the server as bytes.
|
|
85
|
+
"""
|
|
86
|
+
if self._socket is None:
|
|
87
|
+
raise GvmError("Socket is not connected")
|
|
88
|
+
|
|
89
|
+
self._socket.sendall(data)
|
|
90
|
+
|
|
91
|
+
def read(self) -> bytes:
|
|
92
|
+
"""Read data from the remote server
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
data as bytes
|
|
96
|
+
"""
|
|
97
|
+
break_timeout = (
|
|
98
|
+
time() + self._timeout if self._timeout is not None else None
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
data = self._read()
|
|
102
|
+
|
|
103
|
+
if not data:
|
|
104
|
+
# Connection was closed by server
|
|
105
|
+
raise GvmError("Remote closed the connection")
|
|
106
|
+
|
|
107
|
+
if break_timeout and time() > break_timeout:
|
|
108
|
+
raise GvmError("Timeout while reading the response")
|
|
109
|
+
|
|
110
|
+
return data
|
|
111
|
+
|
|
112
|
+
def disconnect(self) -> None:
|
|
113
|
+
"""Disconnect and close the connection to the remote server"""
|
|
114
|
+
try:
|
|
115
|
+
if self._socket is not None:
|
|
116
|
+
self._socket.close()
|
|
117
|
+
except OSError as e:
|
|
118
|
+
logger.debug("Connection closing error: %s", e)
|
|
119
|
+
|
|
120
|
+
def finish_send(self):
|
|
121
|
+
"""Indicate to the remote server you are done with sending data"""
|
|
122
|
+
if self._socket is not None:
|
|
123
|
+
# shutdown socket for sending. only allow reading data afterwards
|
|
124
|
+
self._socket.shutdown(socketlib.SHUT_WR)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Greenbone AG
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from ._connection import GvmConnection
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("gvm.connections.debug")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DebugConnection:
|
|
13
|
+
"""Wrapper around a connection for debugging purposes
|
|
14
|
+
|
|
15
|
+
Allows to debug the connection flow including send and read data. Internally
|
|
16
|
+
it uses the python `logging`_ framework to create debug messages. Please
|
|
17
|
+
take a look at `the logging tutorial
|
|
18
|
+
<https://docs.python.org/3/howto/logging.html#logging-basic-tutorial>`_
|
|
19
|
+
for further details.
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
|
|
23
|
+
.. code-block:: python
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
|
|
27
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
28
|
+
|
|
29
|
+
socket_connection = UnixSocketConnection(path='/var/run/gvm.sock')
|
|
30
|
+
connection = DebugConnection(socket_connection)
|
|
31
|
+
gmp = Gmp(connection=connection)
|
|
32
|
+
|
|
33
|
+
.. _logging:
|
|
34
|
+
https://docs.python.org/3/library/logging.html
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, connection: GvmConnection):
|
|
38
|
+
"""
|
|
39
|
+
Create a new DebugConnection instance.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
connection: GvmConnection to observe
|
|
43
|
+
"""
|
|
44
|
+
self._connection = connection
|
|
45
|
+
|
|
46
|
+
def read(self) -> bytes:
|
|
47
|
+
data = self._connection.read()
|
|
48
|
+
|
|
49
|
+
logger.debug("Read %s characters. Data %r", len(data), data)
|
|
50
|
+
|
|
51
|
+
self.last_read_data = data
|
|
52
|
+
return data
|
|
53
|
+
|
|
54
|
+
def send(self, data: bytes) -> None:
|
|
55
|
+
self.last_send_data = data
|
|
56
|
+
|
|
57
|
+
logger.debug("Sending %s characters. Data %r", len(data), data)
|
|
58
|
+
|
|
59
|
+
return self._connection.send(data)
|
|
60
|
+
|
|
61
|
+
def connect(self) -> None:
|
|
62
|
+
logger.debug("Connecting")
|
|
63
|
+
|
|
64
|
+
return self._connection.connect()
|
|
65
|
+
|
|
66
|
+
def disconnect(self) -> None:
|
|
67
|
+
logger.debug("Disconnecting")
|
|
68
|
+
|
|
69
|
+
return self._connection.disconnect()
|
|
70
|
+
|
|
71
|
+
def finish_send(self) -> None:
|
|
72
|
+
logger.debug("Finish send")
|
|
73
|
+
|
|
74
|
+
self._connection.finish_send()
|
gvm/connections/_ssh.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Greenbone AG
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import errno
|
|
7
|
+
import hashlib
|
|
8
|
+
import logging
|
|
9
|
+
import socket as socketlib
|
|
10
|
+
import sys
|
|
11
|
+
from os import PathLike
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from time import time
|
|
14
|
+
from typing import Any, Callable, Optional, TextIO, Union
|
|
15
|
+
|
|
16
|
+
import paramiko
|
|
17
|
+
import paramiko.ssh_exception
|
|
18
|
+
import paramiko.transport
|
|
19
|
+
|
|
20
|
+
from gvm.errors import GvmError
|
|
21
|
+
|
|
22
|
+
from ._connection import BUF_SIZE, DEFAULT_TIMEOUT
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("gvm.connections.ssh")
|
|
25
|
+
|
|
26
|
+
DEFAULT_SSH_PORT = 22
|
|
27
|
+
DEFAULT_SSH_USERNAME = "gmp"
|
|
28
|
+
DEFAULT_SSH_PASSWORD = ""
|
|
29
|
+
DEFAULT_HOSTNAME = "127.0.0.1"
|
|
30
|
+
DEFAULT_KNOWN_HOSTS_FILE = ".ssh/known_hosts"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SSHConnection:
|
|
34
|
+
"""
|
|
35
|
+
SSH Class to connect, read and write from GVM via SSH
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
timeout: Optional[Union[int, float]] = DEFAULT_TIMEOUT,
|
|
43
|
+
hostname: Optional[str] = DEFAULT_HOSTNAME,
|
|
44
|
+
port: Optional[int] = DEFAULT_SSH_PORT,
|
|
45
|
+
username: Optional[str] = DEFAULT_SSH_USERNAME,
|
|
46
|
+
password: Optional[str] = DEFAULT_SSH_PASSWORD,
|
|
47
|
+
known_hosts_file: Optional[Union[str, PathLike]] = None,
|
|
48
|
+
auto_accept_host: Optional[bool] = None,
|
|
49
|
+
file: TextIO = sys.stdout,
|
|
50
|
+
input: Callable[[], str] = input,
|
|
51
|
+
exit: Callable[[str], Any] = sys.exit,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Create a new SSH connection instance.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
timeout: Timeout in seconds for the connection.
|
|
58
|
+
hostname: DNS name or IP address of the remote server. Default is
|
|
59
|
+
127.0.0.1.
|
|
60
|
+
port: Port of the remote SSH server. Default is port 22.
|
|
61
|
+
username: Username to use for SSH login. Default is "gmp".
|
|
62
|
+
password: Password to use for SSH login. Default is "".
|
|
63
|
+
"""
|
|
64
|
+
self._client: Optional[paramiko.SSHClient] = None
|
|
65
|
+
self.hostname = hostname if hostname is not None else DEFAULT_HOSTNAME
|
|
66
|
+
self.port = int(port) if port is not None else DEFAULT_SSH_PORT
|
|
67
|
+
self.username = (
|
|
68
|
+
username if username is not None else DEFAULT_SSH_USERNAME
|
|
69
|
+
)
|
|
70
|
+
self.password = (
|
|
71
|
+
password if password is not None else DEFAULT_SSH_PASSWORD
|
|
72
|
+
)
|
|
73
|
+
self.known_hosts_file = (
|
|
74
|
+
Path(known_hosts_file)
|
|
75
|
+
if known_hosts_file is not None
|
|
76
|
+
else Path.home() / DEFAULT_KNOWN_HOSTS_FILE
|
|
77
|
+
)
|
|
78
|
+
self.auto_accept_host = auto_accept_host
|
|
79
|
+
self._timeout = timeout
|
|
80
|
+
self._file = file
|
|
81
|
+
self._input = input
|
|
82
|
+
self._exit = exit
|
|
83
|
+
|
|
84
|
+
def _send_all(self, data: bytes) -> int:
|
|
85
|
+
"""Returns the sum of sent bytes if success"""
|
|
86
|
+
sent_sum = 0
|
|
87
|
+
while data:
|
|
88
|
+
sent = self._stdin.channel.send(data)
|
|
89
|
+
|
|
90
|
+
if not sent:
|
|
91
|
+
# Connection was closed by server
|
|
92
|
+
raise GvmError("Remote closed the connection")
|
|
93
|
+
|
|
94
|
+
sent_sum += sent
|
|
95
|
+
|
|
96
|
+
data = data[sent:]
|
|
97
|
+
return sent_sum
|
|
98
|
+
|
|
99
|
+
def _auto_accept_host(
|
|
100
|
+
self, hostkeys: paramiko.HostKeys, key: paramiko.PKey
|
|
101
|
+
) -> None:
|
|
102
|
+
if self.port == DEFAULT_SSH_PORT:
|
|
103
|
+
hostkeys.add(self.hostname, key.get_name(), key)
|
|
104
|
+
elif self.port != DEFAULT_SSH_PORT:
|
|
105
|
+
hostkeys.add(
|
|
106
|
+
"[" + self.hostname + "]:" + str(self.port),
|
|
107
|
+
key.get_name(),
|
|
108
|
+
key,
|
|
109
|
+
)
|
|
110
|
+
try:
|
|
111
|
+
hostkeys.save(filename=str(self.known_hosts_file))
|
|
112
|
+
except OSError as e:
|
|
113
|
+
raise GvmError(
|
|
114
|
+
"Something went wrong with writing "
|
|
115
|
+
f"the known_hosts file {self.known_hosts_file.absolute()}: {e}"
|
|
116
|
+
) from None
|
|
117
|
+
|
|
118
|
+
key_type = key.get_name().replace("ssh-", "").upper()
|
|
119
|
+
|
|
120
|
+
logger.info(
|
|
121
|
+
"Warning: Permanently added '%s' (%s) to "
|
|
122
|
+
"the list of known hosts.",
|
|
123
|
+
self.hostname,
|
|
124
|
+
key_type,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def _ssh_authentication_input_loop(
|
|
128
|
+
self, hostkeys: paramiko.HostKeys, key: paramiko.PKey
|
|
129
|
+
) -> None:
|
|
130
|
+
# Ask user for permission to continue
|
|
131
|
+
# let it look like openssh
|
|
132
|
+
sha64_fingerprint = base64.b64encode(
|
|
133
|
+
hashlib.sha256(base64.b64decode(key.get_base64())).digest()
|
|
134
|
+
).decode("utf-8")[:-1]
|
|
135
|
+
key_type = key.get_name().replace("ssh-", "").upper()
|
|
136
|
+
|
|
137
|
+
print(
|
|
138
|
+
f"The authenticity of host '{self.hostname}' can't "
|
|
139
|
+
"be established.",
|
|
140
|
+
file=self._file,
|
|
141
|
+
)
|
|
142
|
+
print(
|
|
143
|
+
f"{key_type} key fingerprint is {sha64_fingerprint}.",
|
|
144
|
+
file=self._file,
|
|
145
|
+
)
|
|
146
|
+
print(
|
|
147
|
+
"Are you sure you want to continue connecting (yes/no)? ",
|
|
148
|
+
end="",
|
|
149
|
+
file=self._file,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
add = self._input()
|
|
153
|
+
while True:
|
|
154
|
+
if add == "yes":
|
|
155
|
+
if self.port == DEFAULT_SSH_PORT:
|
|
156
|
+
hostkeys.add(self.hostname, key.get_name(), key)
|
|
157
|
+
elif self.port != DEFAULT_SSH_PORT:
|
|
158
|
+
hostkeys.add(
|
|
159
|
+
"[" + self.hostname + "]:" + str(self.port),
|
|
160
|
+
key.get_name(),
|
|
161
|
+
key,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# ask user if the key should be added permanently
|
|
165
|
+
print(
|
|
166
|
+
f"Do you want to add {self.hostname} "
|
|
167
|
+
"to known_hosts (yes/no)? ",
|
|
168
|
+
end="",
|
|
169
|
+
file=self._file,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
save = self._input()
|
|
173
|
+
while True:
|
|
174
|
+
if save == "yes":
|
|
175
|
+
try:
|
|
176
|
+
hostkeys.save(filename=str(self.known_hosts_file))
|
|
177
|
+
except OSError as e:
|
|
178
|
+
raise GvmError(
|
|
179
|
+
"Something went wrong with writing "
|
|
180
|
+
f"the known_hosts file: {e}"
|
|
181
|
+
) from None
|
|
182
|
+
|
|
183
|
+
logger.info(
|
|
184
|
+
"Warning: Permanently added '%s' (%s) to "
|
|
185
|
+
"the list of known hosts.",
|
|
186
|
+
self.hostname,
|
|
187
|
+
key_type,
|
|
188
|
+
)
|
|
189
|
+
break
|
|
190
|
+
elif save == "no":
|
|
191
|
+
logger.info(
|
|
192
|
+
"Warning: Host '%s' (%s) not added to "
|
|
193
|
+
"the list of known hosts.",
|
|
194
|
+
self.hostname,
|
|
195
|
+
key_type,
|
|
196
|
+
)
|
|
197
|
+
break
|
|
198
|
+
else:
|
|
199
|
+
print(
|
|
200
|
+
"Please type 'yes' or 'no': ",
|
|
201
|
+
end="",
|
|
202
|
+
file=self._file,
|
|
203
|
+
)
|
|
204
|
+
save = self._input()
|
|
205
|
+
break
|
|
206
|
+
elif add == "no":
|
|
207
|
+
self._exit("User denied key. Host key verification failed.")
|
|
208
|
+
else:
|
|
209
|
+
print("Please type 'yes' or 'no': ", end="", file=self._file)
|
|
210
|
+
add = self._input()
|
|
211
|
+
|
|
212
|
+
def _get_remote_host_key(self) -> paramiko.PKey:
|
|
213
|
+
"""Get the remote host key for ssh connection"""
|
|
214
|
+
try:
|
|
215
|
+
tmp_socket = socketlib.socket()
|
|
216
|
+
tmp_socket.settimeout(self._timeout)
|
|
217
|
+
tmp_socket.connect((self.hostname, self.port))
|
|
218
|
+
except OSError as e:
|
|
219
|
+
tmp_socket.close()
|
|
220
|
+
raise GvmError(
|
|
221
|
+
"Couldn't establish a connection to fetch the"
|
|
222
|
+
f" remote server key: {e}"
|
|
223
|
+
) from None
|
|
224
|
+
|
|
225
|
+
trans = paramiko.transport.Transport(tmp_socket)
|
|
226
|
+
try:
|
|
227
|
+
trans.start_client()
|
|
228
|
+
except paramiko.SSHException as e:
|
|
229
|
+
tmp_socket.close()
|
|
230
|
+
raise GvmError(
|
|
231
|
+
f"Couldn't fetch the remote server key: {e}"
|
|
232
|
+
) from None
|
|
233
|
+
|
|
234
|
+
key = trans.get_remote_server_key()
|
|
235
|
+
try:
|
|
236
|
+
trans.close()
|
|
237
|
+
except paramiko.SSHException as e:
|
|
238
|
+
raise GvmError(
|
|
239
|
+
f"Couldn't close the connection to the remote server key: {e}"
|
|
240
|
+
) from None
|
|
241
|
+
finally:
|
|
242
|
+
tmp_socket.close()
|
|
243
|
+
|
|
244
|
+
return key
|
|
245
|
+
|
|
246
|
+
def _ssh_authentication(self) -> None:
|
|
247
|
+
"""Search/add/save the servers key for the SSH authentication process"""
|
|
248
|
+
|
|
249
|
+
if not self._client:
|
|
250
|
+
raise GvmError("SSH Client not connected.")
|
|
251
|
+
|
|
252
|
+
# set to reject policy (avoid MITM attacks)
|
|
253
|
+
self._client.set_missing_host_key_policy(paramiko.RejectPolicy())
|
|
254
|
+
|
|
255
|
+
# openssh is posix, so this might only a posix approach
|
|
256
|
+
# https://stackoverflow.com/q/32945533
|
|
257
|
+
try:
|
|
258
|
+
# load the keys into paramiko and check if remote is in the list
|
|
259
|
+
self._client.load_host_keys(filename=str(self.known_hosts_file))
|
|
260
|
+
except OSError as e:
|
|
261
|
+
if e.errno != errno.ENOENT:
|
|
262
|
+
raise GvmError(
|
|
263
|
+
"Something went wrong with reading "
|
|
264
|
+
f"the known_hosts file: {e}"
|
|
265
|
+
) from None
|
|
266
|
+
|
|
267
|
+
hostkeys = self._client.get_host_keys()
|
|
268
|
+
|
|
269
|
+
# Switch based on SSH Port
|
|
270
|
+
if self.port == DEFAULT_SSH_PORT:
|
|
271
|
+
hostname = self.hostname
|
|
272
|
+
else:
|
|
273
|
+
hostname = f"[{self.hostname}]:{self.port}"
|
|
274
|
+
|
|
275
|
+
if not hostkeys.lookup(hostname):
|
|
276
|
+
# Key not found, so connect to remote and fetch the key
|
|
277
|
+
# with the paramiko Transport protocol
|
|
278
|
+
key = self._get_remote_host_key()
|
|
279
|
+
if self.auto_accept_host:
|
|
280
|
+
self._auto_accept_host(hostkeys=hostkeys, key=key)
|
|
281
|
+
else:
|
|
282
|
+
self._ssh_authentication_input_loop(hostkeys=hostkeys, key=key)
|
|
283
|
+
|
|
284
|
+
def _read(self) -> bytes:
|
|
285
|
+
return self._stdout.channel.recv(BUF_SIZE)
|
|
286
|
+
|
|
287
|
+
def send(self, data: bytes) -> None:
|
|
288
|
+
self._send_all(data)
|
|
289
|
+
|
|
290
|
+
def read(self) -> bytes:
|
|
291
|
+
break_timeout = (
|
|
292
|
+
time() + self._timeout if self._timeout is not None else None
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
data = self._read()
|
|
296
|
+
|
|
297
|
+
if not data:
|
|
298
|
+
# Connection was closed by server
|
|
299
|
+
raise GvmError("Remote closed the connection")
|
|
300
|
+
|
|
301
|
+
if break_timeout and time() > break_timeout:
|
|
302
|
+
raise GvmError("Timeout while reading the response")
|
|
303
|
+
|
|
304
|
+
return data
|
|
305
|
+
|
|
306
|
+
def connect(self) -> None:
|
|
307
|
+
"""
|
|
308
|
+
Connect to the SSH server and authenticate to it
|
|
309
|
+
"""
|
|
310
|
+
self._client = paramiko.SSHClient()
|
|
311
|
+
self._ssh_authentication()
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
self._client.connect(
|
|
315
|
+
hostname=self.hostname,
|
|
316
|
+
username=self.username,
|
|
317
|
+
password=self.password,
|
|
318
|
+
timeout=self._timeout,
|
|
319
|
+
port=int(self.port),
|
|
320
|
+
allow_agent=False,
|
|
321
|
+
look_for_keys=False,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
except (
|
|
325
|
+
paramiko.BadHostKeyException,
|
|
326
|
+
paramiko.AuthenticationException,
|
|
327
|
+
paramiko.SSHException,
|
|
328
|
+
paramiko.ssh_exception.NoValidConnectionsError,
|
|
329
|
+
ConnectionError,
|
|
330
|
+
) as e:
|
|
331
|
+
raise GvmError(f"SSH Connection failed: {e}") from None
|
|
332
|
+
|
|
333
|
+
self._stdin, self._stdout, self._stderr = self._client.exec_command(
|
|
334
|
+
"", get_pty=False
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def disconnect(self) -> None:
|
|
338
|
+
"""Disconnect and close the connection to the remote server"""
|
|
339
|
+
try:
|
|
340
|
+
if self._client is not None:
|
|
341
|
+
self._client.close()
|
|
342
|
+
except OSError as e:
|
|
343
|
+
logger.debug("Connection closing error: %s", e)
|
|
344
|
+
raise e
|
|
345
|
+
|
|
346
|
+
if self._client is not None:
|
|
347
|
+
self._client = None
|
|
348
|
+
del self._stdin, self._stdout, self._stderr
|
|
349
|
+
|
|
350
|
+
def finish_send(self) -> None:
|
|
351
|
+
# shutdown socket for sending. only allow reading data afterwards
|
|
352
|
+
self._stdout.channel.shutdown(socketlib.SHUT_WR)
|