From 0bbc53a4794e52dd7a3dde882af9f0cceec3bbaf Mon Sep 17 00:00:00 2001 From: Juanmi Taboada Date: Sun, 26 Feb 2023 13:30:14 +0100 Subject: [PATCH 01/11] Added a security plugin to get listening ports and sende to landscape-server --- landscape/client/monitor/config.py | 41 +- landscape/client/monitor/listeningports.py | 37 + .../monitor/tests/test_listeningports.py | 86 ++ landscape/lib/security.py | 80 ++ landscape/lib/tests/test_security.py | 300 +++++ landscape/message_schemas/server_bound.py | 1015 +++++++++++------ 6 files changed, 1175 insertions(+), 384 deletions(-) create mode 100644 landscape/client/monitor/listeningports.py create mode 100644 landscape/client/monitor/tests/test_listeningports.py create mode 100644 landscape/lib/security.py create mode 100644 landscape/lib/tests/test_security.py diff --git a/landscape/client/monitor/config.py b/landscape/client/monitor/config.py index 19f6aa4af..f1e3f0e56 100644 --- a/landscape/client/monitor/config.py +++ b/landscape/client/monitor/config.py @@ -1,12 +1,28 @@ from landscape.client.deployment import Configuration -ALL_PLUGINS = ["ActiveProcessInfo", "ComputerInfo", - "LoadAverage", "MemoryInfo", "MountInfo", "ProcessorInfo", - "Temperature", "PackageMonitor", "UserMonitor", - "RebootRequired", "AptPreferences", "NetworkActivity", - "NetworkDevice", "UpdateManager", "CPUUsage", "SwiftUsage", - "CephUsage", "ComputerTags", "UbuntuProInfo"] +ALL_PLUGINS = [ + "ActiveProcessInfo", + "ComputerInfo", + "LoadAverage", + "MemoryInfo", + "MountInfo", + "ProcessorInfo", + "Temperature", + "PackageMonitor", + "UserMonitor", + "RebootRequired", + "AptPreferences", + "NetworkActivity", + "NetworkDevice", + "UpdateManager", + "CPUUsage", + "SwiftUsage", + "CephUsage", + "ComputerTags", + "UbuntuProInfo", + "ListeningPorts", +] class MonitorConfiguration(Configuration): @@ -17,12 +33,15 @@ def make_parser(self): Specialize L{Configuration.make_parser}, adding many monitor-specific options. """ - parser = super(MonitorConfiguration, self).make_parser() + parser = super().make_parser() - parser.add_option("--monitor-plugins", metavar="PLUGIN_LIST", - help="Comma-delimited list of monitor plugins to " - "use. ALL means use all plugins.", - default="ALL") + parser.add_option( + "--monitor-plugins", + metavar="PLUGIN_LIST", + help="Comma-delimited list of monitor plugins to " + "use. ALL means use all plugins.", + default="ALL", + ) return parser @property diff --git a/landscape/client/monitor/listeningports.py b/landscape/client/monitor/listeningports.py new file mode 100644 index 000000000..eb44aa471 --- /dev/null +++ b/landscape/client/monitor/listeningports.py @@ -0,0 +1,37 @@ +import logging + +from landscape.client.monitor.plugin import MonitorPlugin +from landscape.lib.security import get_listeningports + + +class ListeningPorts(MonitorPlugin): + """Plugin captures information about listening ports.""" + + persist_name = "listening-ports" + scope = "security" + run_interval = 60 # 1 minute + run_immediately = True + + def send_message(self, urgent=False): + ports = get_listeningports() + if ports == self._persist.get("ports"): + return + self._persist.set("ports", ports) + + message = { + "type": "listening-ports-info", + "ports": [port.canonical for port in ports], + } + logging.info( + "Queueing message with updated " "listening-ports status.", + ) + return self.registry.broker.send_message(message, self._session_id) + + def run(self, urgent=False): + """ + Send the listening-ports-info messages, if the server accepted them. + """ + return self.registry.broker.call_if_accepted( + "listening-ports-info", + self.send_message, + ) diff --git a/landscape/client/monitor/tests/test_listeningports.py b/landscape/client/monitor/tests/test_listeningports.py new file mode 100644 index 000000000..cdcfa90ba --- /dev/null +++ b/landscape/client/monitor/tests/test_listeningports.py @@ -0,0 +1,86 @@ +from unittest import mock + +from landscape.client.monitor.listeningports import ListeningPorts +from landscape.client.tests.helpers import LandscapeTest +from landscape.client.tests.helpers import MonitorHelper +from landscape.lib.testing import LogKeeperHelper +from landscape.lib.tests.test_security import sample_listening_ports_canonical +from landscape.lib.tests.test_security import sample_subprocess_run + + +class ListeningPortsTest(LandscapeTest): + """ + Tests relating to the L{ListeningPortsTes} monitoring plug-in, which should + notice changes to listening ports and report these back to + landscape server. + """ + + helpers = [MonitorHelper, LogKeeperHelper] + + def setUp(self): + super().setUp() + self.plugin = ListeningPorts() + self.monitor.add(self.plugin) + self.mstore.set_accepted_types(["listening-ports-info"]) + + @mock.patch("landscape.lib.security.subprocess.run", sample_subprocess_run) + def test_resynchronize(self): + """ + The "resynchronize" reactor message cause the plugin to send fresh + data. + """ + self.plugin.run() + self.reactor.fire("resynchronize", scopes=["security"]) + self.plugin.run() + messages = self.mstore.get_pending_messages() + self.assertEqual(len(messages), 2) + + def test_run_interval(self): + """ + The L{UpdateManager} plugin will be scheduled to run every hour. + """ + self.assertEqual(60, self.plugin.run_interval) + + def test_run_immediately(self): + """ + The L{UpdateManager} plugin will be run immediately at startup. + """ + self.assertTrue(True, self.plugin.run_immediately) + + @mock.patch("landscape.lib.security.subprocess.run", sample_subprocess_run) + def test_run(self): + """ + If the server can accept them, the plugin should send + C{listening-ports} messages. + """ + with mock.patch.object(self.remote, "send_message"): + self.plugin.run() + self.remote.send_message.assert_called_once_with( + mock.ANY, + mock.ANY, + ) + self.mstore.set_accepted_types([]) + self.plugin.run() + + @mock.patch("landscape.lib.security.subprocess.run", sample_subprocess_run) + def test_send_message(self): + """ + A new C{"listening-ports-info"} message should be enqueued if and only + if the listening-ports status of the system has changed. + """ + self.plugin.send_message() + self.assertIn( + "Queueing message with updated listening-ports status.", + self.logfile.getvalue(), + ) + canonical_sample = sample_listening_ports_canonical() + self.assertMessages( + self.mstore.get_pending_messages(), + [{"type": "listening-ports-info", "ports": canonical_sample}], + ) + self.mstore.delete_all_messages() + self.plugin.send_message() + self.assertMessages( + self.mstore.get_pending_messages(), + canonical_sample, + ) diff --git a/landscape/lib/security.py b/landscape/lib/security.py new file mode 100644 index 000000000..030717e8d --- /dev/null +++ b/landscape/lib/security.py @@ -0,0 +1,80 @@ +import subprocess + +__all__ = ["get_listeningports"] + + +class ListeningPort: + """ + Details about a listeining port in the system + """ + + lsof_cmd = "/usr/bin/lsof" + awk_cmd = "/usr/bin/awk" + + def __init__(self, cmd, pid, user, kind, mode, port, *args): + self.cmd = cmd + self.pid = pid + self.user = user + self.kind = kind + self.mode = mode + self.port = port + + @property + def canonical(self): + return { + "cmd": self.cmd, + "pid": self.pid, + "user": self.user, + "kind": self.kind, + "mode": self.mode, + "port": self.port, + } + + def __eq__(self, other): + return ( + self.cmd == other.cmd + and self.pid == other.pid + and self.user == other.user + and self.kind == other.kind + and self.mode == other.mode + and self.port == other.port + ) + + def __repr__(self): + return ( + f"{self.cmd} {self.pid} {self.user} " + f"{self.kind} {self.mode} {self.port}" + ) + + +def get_listeningports(): + + # Launch lsof to find all ports being used + ps = subprocess.run( + [ListeningPort.lsof_cmd, "-i", "-P", "-n"], + check=True, + capture_output=True, + ) + + # Filter result with AWK for port listening and + # select columns: COMMAND, PID, USER, TYPE, MODE, NAME + ps2 = subprocess.run( + [ + ListeningPort.awk_cmd, + '$10 ~ "LISTEN" {n=split($9, a, ":"); ' + 'print $1" "$2" "$3" "$5" "$8" "a[n]}', + ], + input=ps.stdout, + capture_output=True, + ) + + # Get output + output = ps2.stdout.decode("utf-8").strip() + + # Build ports information + ports = [] + for line in output.splitlines(): + elements = line.split(" ") + ports.append(ListeningPort(*elements)) + + return ports diff --git a/landscape/lib/tests/test_security.py b/landscape/lib/tests/test_security.py new file mode 100644 index 000000000..37ce7c467 --- /dev/null +++ b/landscape/lib/tests/test_security.py @@ -0,0 +1,300 @@ +import os +from subprocess import run as run_orig +from unittest import TestCase +from unittest.mock import patch + +from landscape.lib import testing +from landscape.lib.security import get_listeningports +from landscape.lib.security import ListeningPort + + +SAMPLE_LSOF_OUTPUT = """COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME +systemd 1 root 47u IPv6 800489 0t0 TCP *:4369 (LISTEN) +systemd-n 189 systemd-network 18u IPv4 809990 0t0 UDP 10.154.207.42:68 +systemd-r 191 systemd-resolve 13u IPv4 806291 0t0 UDP 127.0.0.53:53 +systemd-r 191 systemd-resolve 14u IPv4 806292 0t0 TCP 127.0.0.53:53 (LISTEN) +beam.smp 250 rabbitmq 18u IPv4 808522 0t0 TCP *:25672 (LISTEN) +beam.smp 250 rabbitmq 19u IPv4 808524 0t0 TCP 127.0.0.1:52100->127.0.0.1:4369 (ESTABLISHED) +beam.smp 250 rabbitmq 36u IPv6 808693 0t0 TCP *:5672 (LISTEN) +beam.smp 250 rabbitmq 37u IPv6 808734 0t0 TCP 127.0.0.1:5672->127.0.0.1:41554 (ESTABLISHED) +beam.smp 250 rabbitmq 38u IPv6 808737 0t0 TCP 127.0.0.1:5672->127.0.0.1:41570 (ESTABLISHED) +beam.smp 250 rabbitmq 39u IPv6 808764 0t0 TCP 127.0.0.1:5672->127.0.0.1:41582 (ESTABLISHED) +beam.smp 250 rabbitmq 40u IPv6 808786 0t0 TCP 127.0.0.1:5672->127.0.0.1:41598 (ESTABLISHED) +beam.smp 250 rabbitmq 41u IPv6 808803 0t0 TCP 127.0.0.1:5672->127.0.0.1:41612 (ESTABLISHED) +beam.smp 250 rabbitmq 42u IPv6 808821 0t0 TCP 127.0.0.1:5672->127.0.0.1:41616 (ESTABLISHED) +ntpd 274 ntp 16u IPv6 810072 0t0 UDP *:123 +ntpd 274 ntp 17u IPv4 810075 0t0 UDP *:123 +ntpd 274 ntp 18u IPv4 810079 0t0 UDP 127.0.0.1:123 +ntpd 274 ntp 19u IPv4 810081 0t0 UDP 10.154.207.42:123 +ntpd 274 ntp 20u IPv6 810083 0t0 UDP [::1]:123 +ntpd 274 ntp 21u IPv6 810085 0t0 UDP [fe80::216:3eff:fe68:1d2c]:123 +apache2 279 root 4u IPv6 799739 0t0 TCP *:80 (LISTEN) +apache2 279 root 6u IPv6 799743 0t0 TCP *:443 (LISTEN) +sshd 287 root 3u IPv4 805664 0t0 TCP *:22 (LISTEN) +sshd 287 root 4u IPv6 805666 0t0 TCP *:22 (LISTEN) +apache2 292 www-data 4u IPv6 799739 0t0 TCP *:80 (LISTEN) +apache2 292 www-data 6u IPv6 799743 0t0 TCP *:443 (LISTEN) +apache2 293 www-data 4u IPv6 799739 0t0 TCP *:80 (LISTEN) +apache2 293 www-data 6u IPv6 799743 0t0 TCP *:443 (LISTEN) +postgres 412 postgres 5u IPv4 810253 0t0 TCP 127.0.0.1:5432 (LISTEN) +postgres 412 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 444 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 445 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 446 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 447 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 448 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 449 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +epmd 528 epmd 3u IPv6 800489 0t0 TCP *:4369 (LISTEN) +epmd 528 epmd 4u IPv6 816421 0t0 TCP 127.0.0.1:4369->127.0.0.1:52100 (ESTABLISHED) +packagese 570 landscape 5u IPv4 811354 0t0 TCP 127.0.0.1:42456->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 6u IPv4 811357 0t0 TCP 127.0.0.1:42468->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 7u IPv4 811361 0t0 TCP 127.0.0.1:42476->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 8u IPv4 811363 0t0 TCP 127.0.0.1:42488->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 9u IPv4 811365 0t0 TCP 127.0.0.1:42490->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 10u IPv4 811367 0t0 TCP 127.0.0.1:42504->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 11u IPv4 811368 0t0 TCP 127.0.0.1:42514->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 12u IPv4 811372 0t0 TCP 127.0.0.1:42530->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 13u IPv4 811375 0t0 TCP 127.0.0.1:42532->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 14u IPv4 811379 0t0 TCP 127.0.0.1:42548->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 15u IPv4 811380 0t0 TCP 127.0.0.1:42552->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 16u IPv4 811381 0t0 TCP 127.0.0.1:42556->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 17u IPv4 811382 0t0 TCP 127.0.0.1:42564->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 18u IPv4 814415 0t0 TCP 127.0.0.1:42568->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 19u IPv4 814419 0t0 TCP 127.0.0.1:42580->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 20u IPv4 811386 0t0 TCP 127.0.0.1:42590->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 21u IPv4 814423 0t0 TCP 127.0.0.1:42600->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 22u IPv4 811390 0t0 TCP 127.0.0.1:42608->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 23u IPv4 811391 0t0 TCP 127.0.0.1:42616->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 24u IPv4 814428 0t0 TCP 127.0.0.1:42620->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 25u IPv4 811395 0t0 TCP 127.0.0.1:42636->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 26u IPv4 814432 0t0 TCP 127.0.0.1:42650->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 27u IPv4 811399 0t0 TCP 127.0.0.1:42658->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 28u IPv4 814436 0t0 TCP 127.0.0.1:42666->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 29u IPv4 811403 0t0 TCP 127.0.0.1:42672->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 30u IPv4 811405 0t0 TCP 127.0.0.1:42674->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 31u IPv4 808590 0t0 TCP 127.0.0.1:42676->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 32u IPv4 814441 0t0 TCP 127.0.0.1:42684->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 33u IPv4 811412 0t0 TCP 127.0.0.1:42688->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 34u IPv4 811415 0t0 TCP 127.0.0.1:42692->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 35u IPv4 809456 0t0 TCP 127.0.0.1:42704->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 36u IPv4 809457 0t0 TCP 127.0.0.1:42714->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 37u IPv4 815438 0t0 TCP 127.0.0.1:42728->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 38u IPv4 814449 0t0 TCP 127.0.0.1:42742->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 39u IPv4 812230 0t0 TCP 127.0.0.1:42744->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 40u IPv4 811422 0t0 TCP 127.0.0.1:42756->127.0.0.1:5432 (ESTABLISHED) +packagese 570 landscape 41u IPv6 811426 0t0 TCP *:9099 (LISTEN) +postgres 580 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 580 postgres 8u IPv4 809443 0t0 TCP 127.0.0.1:5432->127.0.0.1:42456 (ESTABLISHED) +postgres 581 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 581 postgres 8u IPv4 811358 0t0 TCP 127.0.0.1:5432->127.0.0.1:42468 (ESTABLISHED) +postgres 582 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 582 postgres 8u IPv4 811362 0t0 TCP 127.0.0.1:5432->127.0.0.1:42476 (ESTABLISHED) +postgres 585 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 585 postgres 8u IPv4 811364 0t0 TCP 127.0.0.1:5432->127.0.0.1:42488 (ESTABLISHED) +postgres 586 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 586 postgres 8u IPv4 811366 0t0 TCP 127.0.0.1:5432->127.0.0.1:42490 (ESTABLISHED) +postgres 587 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 587 postgres 8u IPv4 808587 0t0 TCP 127.0.0.1:5432->127.0.0.1:42504 (ESTABLISHED) +postgres 588 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 588 postgres 8u IPv4 811369 0t0 TCP 127.0.0.1:5432->127.0.0.1:42514 (ESTABLISHED) +postgres 589 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 589 postgres 8u IPv4 801645 0t0 TCP 127.0.0.1:5432->127.0.0.1:42530 (ESTABLISHED) +postgres 590 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 590 postgres 8u IPv4 801646 0t0 TCP 127.0.0.1:5432->127.0.0.1:42532 (ESTABLISHED) +postgres 593 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 593 postgres 8u IPv4 807213 0t0 TCP 127.0.0.1:5432->127.0.0.1:42548 (ESTABLISHED) +postgres 595 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 595 postgres 8u IPv4 808588 0t0 TCP 127.0.0.1:5432->127.0.0.1:42552 (ESTABLISHED) +postgres 597 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 597 postgres 8u IPv4 809449 0t0 TCP 127.0.0.1:5432->127.0.0.1:42556 (ESTABLISHED) +postgres 598 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 598 postgres 8u IPv4 814414 0t0 TCP 127.0.0.1:5432->127.0.0.1:42564 (ESTABLISHED) +postgres 599 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 599 postgres 8u IPv4 814416 0t0 TCP 127.0.0.1:5432->127.0.0.1:42568 (ESTABLISHED) +postgres 600 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 600 postgres 8u IPv4 811385 0t0 TCP 127.0.0.1:5432->127.0.0.1:42580 (ESTABLISHED) +postgres 601 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 601 postgres 8u IPv4 814422 0t0 TCP 127.0.0.1:5432->127.0.0.1:42590 (ESTABLISHED) +postgres 602 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 602 postgres 8u IPv4 811389 0t0 TCP 127.0.0.1:5432->127.0.0.1:42600 (ESTABLISHED) +postgres 603 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 603 postgres 8u IPv4 814426 0t0 TCP 127.0.0.1:5432->127.0.0.1:42608 (ESTABLISHED) +postgres 604 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 604 postgres 8u IPv4 814427 0t0 TCP 127.0.0.1:5432->127.0.0.1:42616 (ESTABLISHED) +postgres 605 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 605 postgres 8u IPv4 811394 0t0 TCP 127.0.0.1:5432->127.0.0.1:42620 (ESTABLISHED) +postgres 606 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 606 postgres 8u IPv4 814431 0t0 TCP 127.0.0.1:5432->127.0.0.1:42636 (ESTABLISHED) +postgres 607 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 607 postgres 8u IPv4 811398 0t0 TCP 127.0.0.1:5432->127.0.0.1:42650 (ESTABLISHED) +postgres 609 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 609 postgres 8u IPv4 814435 0t0 TCP 127.0.0.1:5432->127.0.0.1:42658 (ESTABLISHED) +postgres 610 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 610 postgres 8u IPv4 811402 0t0 TCP 127.0.0.1:5432->127.0.0.1:42666 (ESTABLISHED) +postgres 611 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 611 postgres 8u IPv4 811404 0t0 TCP 127.0.0.1:5432->127.0.0.1:42672 (ESTABLISHED) +postgres 612 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 612 postgres 8u IPv4 808589 0t0 TCP 127.0.0.1:5432->127.0.0.1:42674 (ESTABLISHED) +postgres 613 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 613 postgres 8u IPv4 811408 0t0 TCP 127.0.0.1:5432->127.0.0.1:42676 (ESTABLISHED) +postgres 614 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 614 postgres 8u IPv4 811411 0t0 TCP 127.0.0.1:5432->127.0.0.1:42684 (ESTABLISHED) +postgres 615 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 615 postgres 8u IPv4 814442 0t0 TCP 127.0.0.1:5432->127.0.0.1:42688 (ESTABLISHED) +postgres 616 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 616 postgres 8u IPv4 811416 0t0 TCP 127.0.0.1:5432->127.0.0.1:42692 (ESTABLISHED) +postgres 617 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 617 postgres 8u IPv4 814446 0t0 TCP 127.0.0.1:5432->127.0.0.1:42704 (ESTABLISHED) +postgres 618 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 618 postgres 8u IPv4 814447 0t0 TCP 127.0.0.1:5432->127.0.0.1:42714 (ESTABLISHED) +postgres 619 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 619 postgres 8u IPv4 814448 0t0 TCP 127.0.0.1:5432->127.0.0.1:42728 (ESTABLISHED) +postgres 620 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 620 postgres 8u IPv4 812229 0t0 TCP 127.0.0.1:5432->127.0.0.1:42742 (ESTABLISHED) +postgres 621 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 621 postgres 8u IPv4 814450 0t0 TCP 127.0.0.1:5432->127.0.0.1:42744 (ESTABLISHED) +postgres 622 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 622 postgres 8u IPv4 814451 0t0 TCP 127.0.0.1:5432->127.0.0.1:42756 (ESTABLISHED) +python3 722 landscape 10u IPv4 809560 0t0 TCP *:9100 (LISTEN) +python3 722 landscape 12u IPv4 815502 0t0 TCP 127.0.0.1:51604->127.0.0.1:5432 (ESTABLISHED) +postgres 725 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 725 postgres 8u IPv4 806472 0t0 TCP 127.0.0.1:5432->127.0.0.1:51604 (ESTABLISHED) +python3 727 landscape 10u IPv4 805812 0t0 TCP *:8070 (LISTEN) +python3 727 landscape 11u IPv4 811692 0t0 TCP 127.0.0.1:41582->127.0.0.1:5672 (ESTABLISHED) +python3 740 landscape 11u IPv4 814516 0t0 TCP *:8090 (LISTEN) +python3 740 landscape 12u IPv4 817171 0t0 TCP 127.0.0.1:41570->127.0.0.1:5672 (ESTABLISHED) +sshd 748 root 4u IPv4 815669 0t0 TCP 10.154.207.42:22->10.154.207.1:46170 (ESTABLISHED) +sshd 856 ubuntu 4u IPv4 815669 0t0 TCP 10.154.207.42:22->10.154.207.1:46170 (ESTABLISHED) +python3 867 landscape 7u IPv4 814793 0t0 TCP 127.0.0.1:41554->127.0.0.1:5672 (ESTABLISHED) +python3 867 landscape 11u IPv4 816642 0t0 TCP *:8080 (LISTEN) +python3 867 landscape 13u IPv4 813744 0t0 TCP 127.0.0.1:51676->127.0.0.1:5432 (ESTABLISHED) +python3 867 landscape 14u IPv4 814795 0t0 TCP 127.0.0.1:51686->127.0.0.1:5432 (ESTABLISHED) +postgres 932 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 932 postgres 8u IPv4 812343 0t0 TCP 127.0.0.1:5432->127.0.0.1:51676 (ESTABLISHED) +postgres 933 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 933 postgres 8u IPv4 816694 0t0 TCP 127.0.0.1:5432->127.0.0.1:51686 (ESTABLISHED) +python3 970 landscape 10u IPv4 811701 0t0 TCP *:9090 (LISTEN) +python3 970 landscape 11u IPv4 811702 0t0 TCP 127.0.0.1:41598->127.0.0.1:5672 (ESTABLISHED) +postgres 974 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 974 postgres 8u IPv4 813768 0t0 TCP 127.0.0.1:5432->127.0.0.1:51850 (ESTABLISHED) +python3 976 landscape 7u IPv4 806684 0t0 TCP 127.0.0.1:51850->127.0.0.1:5432 (ESTABLISHED) +python3 976 landscape 10u IPv4 815749 0t0 TCP 127.0.0.1:41612->127.0.0.1:5672 (ESTABLISHED) +postgres 980 postgres 7u IPv4 810255 0t0 UDP 127.0.0.1:42868->127.0.0.1:42868 +postgres 980 postgres 8u IPv4 813769 0t0 TCP 127.0.0.1:5432->127.0.0.1:51858 (ESTABLISHED) +python3 982 landscape 7u IPv4 806693 0t0 TCP 127.0.0.1:51858->127.0.0.1:5432 (ESTABLISHED) +python3 982 landscape 11u IPv4 806697 0t0 TCP *:9080 (LISTEN) +python3 982 landscape 12u IPv4 806698 0t0 TCP 127.0.0.1:41616->127.0.0.1:5672 (ESTABLISHED)""" # noqa:E501,W291 + + +EXPECTED_AWK_OUTPUT = """systemd 1 root IPv6 TCP 4369 +systemd-r 191 systemd-resolve IPv4 TCP 53 +beam.smp 250 rabbitmq IPv4 TCP 25672 +beam.smp 250 rabbitmq IPv6 TCP 5672 +apache2 279 root IPv6 TCP 80 +apache2 279 root IPv6 TCP 443 +sshd 287 root IPv4 TCP 22 +sshd 287 root IPv6 TCP 22 +apache2 292 www-data IPv6 TCP 80 +apache2 292 www-data IPv6 TCP 443 +apache2 293 www-data IPv6 TCP 80 +apache2 293 www-data IPv6 TCP 443 +postgres 412 postgres IPv4 TCP 5432 +epmd 528 epmd IPv6 TCP 4369 +packagese 570 landscape IPv6 TCP 9099 +python3 722 landscape IPv4 TCP 9100 +python3 727 landscape IPv4 TCP 8070 +python3 740 landscape IPv4 TCP 8090 +python3 867 landscape IPv4 TCP 8080 +python3 970 landscape IPv4 TCP 9090 +python3 982 landscape IPv4 TCP 9080""" + +echo_cmd = "/usr/bin/echo" + + +def sample_subprocess_run( + *args, + **kwargs, +): + if "lsof" in args[0][0]: + args = ([echo_cmd, "-n", "-e", SAMPLE_LSOF_OUTPUT],) + + return run_orig(*args, **kwargs) + + +def sample_listening_ports(): + listening = [] + for listeningport in EXPECTED_AWK_OUTPUT.splitlines(): + args = listeningport.split(" ") + listening.append(ListeningPort(*args)) + return listening + + +def sample_listening_ports_canonical(): + return [port.canonical for port in sample_listening_ports()] + + +class BaseTestCase( + testing.TwistedTestCase, + testing.FSTestCase, + TestCase, +): + pass + + +class ListeningPortsTest(BaseTestCase): + """Test for parsing /proc/uptime data.""" + + def test_analyze_object_behaviour(self): + listening1 = ListeningPort( + "cmd", + "pid", + "user", + "kind", + "mode", + "port", + ) + listening2 = ListeningPort( + "cmd", + "pid", + "user", + "kind", + "mode", + "port", + ) + self.assertEqual(listening1, listening2) + + listening3 = ListeningPort( + "cmdX", + "pid", + "user", + "kind", + "mode", + "port", + ) + self.assertNotEqual(listening1, listening3) + + can = listening1.canonical + for val in ["cmd", "pid", "user", "kind", "mode", "port"]: + attr = getattr(listening1, val) + self.assertEqual(attr, val) + self.assertEqual(attr, can[val]) + + def test_cmd_exists_and_executable(self): + assert os.access(echo_cmd, os.X_OK) + assert os.access(ListeningPort.lsof_cmd, os.X_OK) + assert os.access(ListeningPort.awk_cmd, os.X_OK) + + @patch("landscape.lib.security.subprocess.run", sample_subprocess_run) + def test_listeningports(self): + + listening_test = sample_listening_ports() + listening = get_listeningports() + self.assertEqual(listening, get_listeningports()) + + listening_test_canonical = [port.canonical for port in listening_test] + listening_canonical = [port.canonical for port in listening] + self.assertEqual(listening_test_canonical, listening_canonical) + self.assertEqual( + sample_listening_ports_canonical(), + listening_canonical, + ) diff --git a/landscape/message_schemas/server_bound.py b/landscape/message_schemas/server_bound.py index 1e8f69893..fc0dd12c0 100644 --- a/landscape/message_schemas/server_bound.py +++ b/landscape/message_schemas/server_bound.py @@ -1,26 +1,64 @@ # Copyright 2017 Canonical Limited. All rights reserved. - -from landscape.lib.schema import ( - KeyDict, Dict, List, Tuple, - Bool, Int, Float, Bytes, Unicode, Constant, Any) from .message import Message +from landscape.lib.schema import Any +from landscape.lib.schema import Bool +from landscape.lib.schema import Bytes +from landscape.lib.schema import Constant +from landscape.lib.schema import Dict +from landscape.lib.schema import Float +from landscape.lib.schema import Int +from landscape.lib.schema import KeyDict +from landscape.lib.schema import List +from landscape.lib.schema import Tuple +from landscape.lib.schema import Unicode __all__ = [ - "ACTIVE_PROCESS_INFO", "COMPUTER_UPTIME", "CLIENT_UPTIME", - "OPERATION_RESULT", "COMPUTER_INFO", "DISTRIBUTION_INFO", - "HARDWARE_INVENTORY", "HARDWARE_INFO", "LOAD_AVERAGE", "MEMORY_INFO", - "RESYNCHRONIZE", "MOUNT_ACTIVITY", "MOUNT_INFO", "FREE_SPACE", - "REGISTER", "REGISTER_3_3", - "TEMPERATURE", "PROCESSOR_INFO", "USERS", "PACKAGES", "PACKAGE_LOCKS", - "CHANGE_PACKAGES_RESULT", "UNKNOWN_PACKAGE_HASHES", - "ADD_PACKAGES", "PACKAGE_REPORTER_RESULT", "TEXT_MESSAGE", "TEST", - "CUSTOM_GRAPH", "REBOOT_REQUIRED", "APT_PREFERENCES", - "NETWORK_DEVICE", "NETWORK_ACTIVITY", - "REBOOT_REQUIRED_INFO", "UPDATE_MANAGER_INFO", "CPU_USAGE", - "CEPH_USAGE", "SWIFT_USAGE", "SWIFT_DEVICE_INFO", "KEYSTONE_TOKEN", - "JUJU_UNITS_INFO", "CLOUD_METADATA", "COMPUTER_TAGS", "UBUNTU_PRO_INFO", - ] + "ACTIVE_PROCESS_INFO", + "COMPUTER_UPTIME", + "CLIENT_UPTIME", + "OPERATION_RESULT", + "COMPUTER_INFO", + "DISTRIBUTION_INFO", + "HARDWARE_INVENTORY", + "HARDWARE_INFO", + "LOAD_AVERAGE", + "MEMORY_INFO", + "RESYNCHRONIZE", + "MOUNT_ACTIVITY", + "MOUNT_INFO", + "FREE_SPACE", + "REGISTER", + "REGISTER_3_3", + "TEMPERATURE", + "PROCESSOR_INFO", + "USERS", + "PACKAGES", + "PACKAGE_LOCKS", + "CHANGE_PACKAGES_RESULT", + "UNKNOWN_PACKAGE_HASHES", + "ADD_PACKAGES", + "PACKAGE_REPORTER_RESULT", + "TEXT_MESSAGE", + "TEST", + "CUSTOM_GRAPH", + "REBOOT_REQUIRED", + "APT_PREFERENCES", + "NETWORK_DEVICE", + "NETWORK_ACTIVITY", + "REBOOT_REQUIRED_INFO", + "UPDATE_MANAGER_INFO", + "CPU_USAGE", + "CEPH_USAGE", + "SWIFT_USAGE", + "SWIFT_DEVICE_INFO", + "KEYSTONE_TOKEN", + "JUJU_UNITS_INFO", + "CLOUD_METADATA", + "COMPUTER_TAGS", + "UBUNTU_PRO_INFO", + "LISTENING_PORTS_INFO", +] # When adding a new schema, which deprecates an older schema, the recommended @@ -31,160 +69,234 @@ # USERS_2_1 -process_info = KeyDict({"pid": Int(), - "name": Unicode(), - "state": Bytes(), - "sleep-average": Int(), - "uid": Int(), - "gid": Int(), - "vm-size": Int(), - "start-time": Int(), - "percent-cpu": Float()}, - # Optional for backwards compatibility - optional=["vm-size", "sleep-average", "percent-cpu"]) +process_info = KeyDict( + { + "pid": Int(), + "name": Unicode(), + "state": Bytes(), + "sleep-average": Int(), + "uid": Int(), + "gid": Int(), + "vm-size": Int(), + "start-time": Int(), + "percent-cpu": Float(), + }, + # Optional for backwards compatibility + optional=["vm-size", "sleep-average", "percent-cpu"], +) ACTIVE_PROCESS_INFO = Message( "active-process-info", - {"kill-processes": List(Int()), - "kill-all-processes": Bool(), - "add-processes": List(process_info), - "update-processes": List(process_info)}, + { + "kill-processes": List(Int()), + "kill-all-processes": Bool(), + "add-processes": List(process_info), + "update-processes": List(process_info), + }, # XXX Really we don't want all three of these keys to be optional: # we always want _something_... - optional=["add-processes", "update-processes", "kill-processes", - "kill-all-processes"]) + optional=[ + "add-processes", + "update-processes", + "kill-processes", + "kill-all-processes", + ], +) COMPUTER_UPTIME = Message( "computer-uptime", - {"startup-times": List(Int()), - "shutdown-times": List(Int())}, + {"startup-times": List(Int()), "shutdown-times": List(Int())}, # XXX Again, one or the other. - optional=["startup-times", "shutdown-times"]) + optional=["startup-times", "shutdown-times"], +) CLIENT_UPTIME = Message( "client-uptime", - {"period": Tuple(Float(), Float()), - "components": List(Int())}, - optional=["components"]) # just for backwards compatibility + {"period": Tuple(Float(), Float()), "components": List(Int())}, + optional=["components"], +) # just for backwards compatibility OPERATION_RESULT = Message( "operation-result", - {"operation-id": Int(), - "status": Int(), - "result-code": Int(), - "result-text": Unicode()}, - optional=["result-code", "result-text"]) + { + "operation-id": Int(), + "status": Int(), + "result-code": Int(), + "result-text": Unicode(), + }, + optional=["result-code", "result-text"], +) COMPUTER_INFO = Message( "computer-info", - {"hostname": Unicode(), - "total-memory": Int(), - "total-swap": Int(), - "annotations": Dict(Unicode(), Unicode())}, + { + "hostname": Unicode(), + "total-memory": Int(), + "total-swap": Int(), + "annotations": Dict(Unicode(), Unicode()), + }, # Not sure why these are all optional, but it's explicitly tested # in the server - optional=["hostname", "total-memory", "total-swap", "annotations"]) + optional=["hostname", "total-memory", "total-swap", "annotations"], +) DISTRIBUTION_INFO = Message( "distribution-info", - {"distributor-id": Unicode(), - "description": Unicode(), - "release": Unicode(), - "code-name": Unicode()}, + { + "distributor-id": Unicode(), + "description": Unicode(), + "release": Unicode(), + "code-name": Unicode(), + }, # all optional because the lsb-release file may not have all data. - optional=["distributor-id", "description", "release", "code-name"]) + optional=["distributor-id", "description", "release", "code-name"], +) CLOUD_METADATA = Message( "cloud-instance-metadata", - {"instance-id": Unicode(), - "ami-id": Unicode(), - "instance-type": Unicode()}) - - -hal_data = Dict(Unicode(), - Any(Unicode(), List(Unicode()), Bool(), Int(), Float())) - -HARDWARE_INVENTORY = Message("hardware-inventory", { - "devices": List(Any(Tuple(Constant("create"), hal_data), - Tuple(Constant("update"), - Unicode(), # udi, - hal_data, # creates, - hal_data, # updates, - hal_data), # deletes - Tuple(Constant("delete"), - Unicode()), - ), - )}) - - -HARDWARE_INFO = Message("hardware-info", { - "data": Unicode()}) - -juju_data = {"environment-uuid": Unicode(), - "api-addresses": List(Unicode()), - "unit-name": Unicode(), - "private-address": Unicode()} + { + "instance-id": Unicode(), + "ami-id": Unicode(), + "instance-type": Unicode(), + }, +) + + +hal_data = Dict( + Unicode(), + Any(Unicode(), List(Unicode()), Bool(), Int(), Float()), +) + +HARDWARE_INVENTORY = Message( + "hardware-inventory", + { + "devices": List( + Any( + Tuple(Constant("create"), hal_data), + Tuple( + Constant("update"), + Unicode(), # udi, + hal_data, # creates, + hal_data, # updates, + hal_data, + ), # deletes + Tuple(Constant("delete"), Unicode()), + ), + ), + }, +) + + +HARDWARE_INFO = Message("hardware-info", {"data": Unicode()}) + +juju_data = { + "environment-uuid": Unicode(), + "api-addresses": List(Unicode()), + "unit-name": Unicode(), + "private-address": Unicode(), +} # The copy of juju_data is needed because Message mutates the dictionary -JUJU_UNITS_INFO = Message("juju-units-info", { - "juju-info-list": List(KeyDict(juju_data.copy(), - optional=["private-address"])) - }) - -LOAD_AVERAGE = Message("load-average", { - "load-averages": List(Tuple(Int(), Float())), - }) - -CPU_USAGE = Message("cpu-usage", { - "cpu-usages": List(Tuple(Int(), Float())), - }) - -CEPH_USAGE = Message("ceph-usage", { - "ring-id": Unicode(), - # Usage data points in the form (timestamp, size, avail, used) - "data-points": List(Tuple(Int(), Int(), Int(), Int())), - # Unused now, for backwards compatibility - "ceph-usages": List(None)}) - -SWIFT_DEVICE_INFO = Message("swift-device-info", { - "swift-device-info": List( - KeyDict({"device": Unicode(), "mounted": Bool()})) - }) - -SWIFT_USAGE = Message("swift-usage", { - # Usage data points in the form (timestamp, device, size, avail, used) - "data-points": List(Tuple(Int(), Unicode(), Int(), Int(), Int()))}) - -KEYSTONE_TOKEN = Message("keystone-token", { - "data": Any(Bytes(), Constant(None)) -}) - -MEMORY_INFO = Message("memory-info", { - "memory-info": List(Tuple(Float(), Int(), Int())), - }) +JUJU_UNITS_INFO = Message( + "juju-units-info", + { + "juju-info-list": List( + KeyDict(juju_data.copy(), optional=["private-address"]), + ), + }, +) + +LOAD_AVERAGE = Message( + "load-average", + { + "load-averages": List(Tuple(Int(), Float())), + }, +) + +CPU_USAGE = Message( + "cpu-usage", + { + "cpu-usages": List(Tuple(Int(), Float())), + }, +) + +CEPH_USAGE = Message( + "ceph-usage", + { + "ring-id": Unicode(), + # Usage data points in the form (timestamp, size, avail, used) + "data-points": List(Tuple(Int(), Int(), Int(), Int())), + # Unused now, for backwards compatibility + "ceph-usages": List(None), + }, +) + +SWIFT_DEVICE_INFO = Message( + "swift-device-info", + { + "swift-device-info": List( + KeyDict({"device": Unicode(), "mounted": Bool()}), + ), + }, +) + +SWIFT_USAGE = Message( + "swift-usage", + { + # Usage data points in the form (timestamp, device, size, avail, used) + "data-points": List(Tuple(Int(), Unicode(), Int(), Int(), Int())), + }, +) + +KEYSTONE_TOKEN = Message( + "keystone-token", + {"data": Any(Bytes(), Constant(None))}, +) + +MEMORY_INFO = Message( + "memory-info", + { + "memory-info": List(Tuple(Float(), Int(), Int())), + }, +) RESYNCHRONIZE = Message( "resynchronize", {"operation-id": Int()}, # operation-id is only there if it's a response to a server-initiated # resynchronize. - optional=["operation-id"]) - -MOUNT_ACTIVITY = Message("mount-activity", { - "activities": List(Tuple(Float(), Unicode(), Bool()))}) - - -MOUNT_INFO = Message("mount-info", { - "mount-info": List(Tuple(Float(), - KeyDict({"mount-point": Unicode(), - "device": Unicode(), - "filesystem": Unicode(), - "total-space": Int()}) - )), - }) - -FREE_SPACE = Message("free-space", { - "free-space": List(Tuple(Float(), Unicode(), Int()))}) + optional=["operation-id"], +) + +MOUNT_ACTIVITY = Message( + "mount-activity", + {"activities": List(Tuple(Float(), Unicode(), Bool()))}, +) + + +MOUNT_INFO = Message( + "mount-info", + { + "mount-info": List( + Tuple( + Float(), + KeyDict( + { + "mount-point": Unicode(), + "device": Unicode(), + "filesystem": Unicode(), + "total-space": Int(), + }, + ), + ), + ), + }, +) + +FREE_SPACE = Message( + "free-space", + {"free-space": List(Tuple(Float(), Unicode(), Int()))}, +) REGISTER = Message( @@ -192,16 +304,25 @@ # The term used in the UI is actually 'registration_key', but we keep # the message schema field as 'registration_password' in case a new # client contacts an older server. - {"registration_password": Any(Unicode(), Constant(None)), - "computer_title": Unicode(), - "hostname": Unicode(), - "account_name": Unicode(), - "tags": Any(Unicode(), Constant(None)), - "vm-info": Bytes(), - "container-info": Unicode(), - "access_group": Unicode()}, - optional=["registration_password", "hostname", "tags", "vm-info", - "container-info", "access_group"]) + { + "registration_password": Any(Unicode(), Constant(None)), + "computer_title": Unicode(), + "hostname": Unicode(), + "account_name": Unicode(), + "tags": Any(Unicode(), Constant(None)), + "vm-info": Bytes(), + "container-info": Unicode(), + "access_group": Unicode(), + }, + optional=[ + "registration_password", + "hostname", + "tags", + "vm-info", + "container-info", + "access_group", + ], +) REGISTER_3_3 = Message( @@ -209,23 +330,38 @@ # The term used in the UI is actually 'registration_key', but we keep # the message schema field as 'registration_password' in case a new # client contacts an older server. - {"registration_password": Any(Unicode(), Constant(None)), - "computer_title": Unicode(), - "hostname": Unicode(), - "account_name": Unicode(), - "tags": Any(Unicode(), Constant(None)), - "vm-info": Bytes(), - "container-info": Unicode(), - "juju-info": KeyDict({"environment-uuid": Unicode(), - "api-addresses": List(Unicode()), - "machine-id": Unicode()}), - "access_group": Unicode(), - "clone_secure_id": Any(Unicode(), Constant(None)), - "ubuntu_pro_info": Unicode()}, + { + "registration_password": Any(Unicode(), Constant(None)), + "computer_title": Unicode(), + "hostname": Unicode(), + "account_name": Unicode(), + "tags": Any(Unicode(), Constant(None)), + "vm-info": Bytes(), + "container-info": Unicode(), + "juju-info": KeyDict( + { + "environment-uuid": Unicode(), + "api-addresses": List(Unicode()), + "machine-id": Unicode(), + }, + ), + "access_group": Unicode(), + "clone_secure_id": Any(Unicode(), Constant(None)), + "ubuntu_pro_info": Unicode(), + }, api=b"3.3", - optional=["registration_password", "hostname", "tags", "vm-info", - "container-info", "access_group", "juju-info", - "clone_secure_id", "ubuntu_pro_info"]) + optional=[ + "registration_password", + "hostname", + "tags", + "vm-info", + "container-info", + "access_group", + "juju-info", + "clone_secure_id", + "ubuntu_pro_info", + ], +) # XXX The register-provisioned-machine message is obsolete, it's kept around @@ -233,7 +369,8 @@ # to have it is 14.07). Eventually it shall be dropped. REGISTER_PROVISIONED_MACHINE = Message( "register-provisioned-machine", - {"otp": Bytes()}) + {"otp": Bytes()}, +) # XXX The register-cloud-vm message is obsolete, it's kept around just to not @@ -241,242 +378,329 @@ # is 14.07). Eventually it shall be dropped. REGISTER_CLOUD_VM = Message( "register-cloud-vm", - {"hostname": Unicode(), - "otp": Any(Bytes(), Constant(None)), - "instance_key": Unicode(), - "account_name": Any(Unicode(), Constant(None)), - "registration_password": Any(Unicode(), Constant(None)), - "reservation_key": Unicode(), - "public_hostname": Unicode(), - "local_hostname": Unicode(), - "kernel_key": Any(Unicode(), Constant(None)), - "ramdisk_key": Any(Unicode(), Constant(None)), - "launch_index": Int(), - "image_key": Unicode(), - "tags": Any(Unicode(), Constant(None)), - "vm-info": Bytes(), - "public_ipv4": Unicode(), - "local_ipv4": Unicode(), - "access_group": Unicode()}, - optional=["tags", "vm-info", "public_ipv4", "local_ipv4", "access_group"]) - - -TEMPERATURE = Message("temperature", { - "thermal-zone": Unicode(), - "temperatures": List(Tuple(Int(), Float())), - }) + { + "hostname": Unicode(), + "otp": Any(Bytes(), Constant(None)), + "instance_key": Unicode(), + "account_name": Any(Unicode(), Constant(None)), + "registration_password": Any(Unicode(), Constant(None)), + "reservation_key": Unicode(), + "public_hostname": Unicode(), + "local_hostname": Unicode(), + "kernel_key": Any(Unicode(), Constant(None)), + "ramdisk_key": Any(Unicode(), Constant(None)), + "launch_index": Int(), + "image_key": Unicode(), + "tags": Any(Unicode(), Constant(None)), + "vm-info": Bytes(), + "public_ipv4": Unicode(), + "local_ipv4": Unicode(), + "access_group": Unicode(), + }, + optional=["tags", "vm-info", "public_ipv4", "local_ipv4", "access_group"], +) + + +TEMPERATURE = Message( + "temperature", + { + "thermal-zone": Unicode(), + "temperatures": List(Tuple(Int(), Float())), + }, +) PROCESSOR_INFO = Message( "processor-info", - {"processors": List(KeyDict({"processor-id": Int(), - "vendor": Unicode(), - "model": Unicode(), - "cache-size": Int(), - }, - optional=["vendor", "cache-size"]))}) - -user_data = KeyDict({ - "uid": Int(), - "username": Unicode(), - "name": Any(Unicode(), Constant(None)), - "enabled": Bool(), - "location": Any(Unicode(), Constant(None)), - "home-phone": Any(Unicode(), Constant(None)), - "work-phone": Any(Unicode(), Constant(None)), - "primary-gid": Any(Int(), Constant(None)), - "primary-groupname": Unicode()}, - optional=["primary-groupname", "primary-gid"]) - -group_data = KeyDict({ - "gid": Int(), - "name": Unicode()}) + { + "processors": List( + KeyDict( + { + "processor-id": Int(), + "vendor": Unicode(), + "model": Unicode(), + "cache-size": Int(), + }, + optional=["vendor", "cache-size"], + ), + ), + }, +) + +user_data = KeyDict( + { + "uid": Int(), + "username": Unicode(), + "name": Any(Unicode(), Constant(None)), + "enabled": Bool(), + "location": Any(Unicode(), Constant(None)), + "home-phone": Any(Unicode(), Constant(None)), + "work-phone": Any(Unicode(), Constant(None)), + "primary-gid": Any(Int(), Constant(None)), + "primary-groupname": Unicode(), + }, + optional=["primary-groupname", "primary-gid"], +) + +group_data = KeyDict({"gid": Int(), "name": Unicode()}) USERS = Message( "users", - {"operation-id": Int(), - "create-users": List(user_data), - "update-users": List(user_data), - "delete-users": List(Unicode()), - "create-groups": List(group_data), - "update-groups": List(group_data), - "delete-groups": List(Unicode()), - "create-group-members": Dict(Unicode(), List(Unicode())), - "delete-group-members": Dict(Unicode(), List(Unicode())), - }, + { + "operation-id": Int(), + "create-users": List(user_data), + "update-users": List(user_data), + "delete-users": List(Unicode()), + "create-groups": List(group_data), + "update-groups": List(group_data), + "delete-groups": List(Unicode()), + "create-group-members": Dict(Unicode(), List(Unicode())), + "delete-group-members": Dict(Unicode(), List(Unicode())), + }, # operation-id is only there for responses, and all other are # optional as long as one of them is there (no way to say that yet) - optional=["operation-id", "create-users", "update-users", "delete-users", - "create-groups", "update-groups", "delete-groups", - "create-group-members", "delete-group-members"]) + optional=[ + "operation-id", + "create-users", + "update-users", + "delete-users", + "create-groups", + "update-groups", + "delete-groups", + "create-group-members", + "delete-group-members", + ], +) USERS_2_1 = Message( "users", - {"operation-id": Int(), - "create-users": List(user_data), - "update-users": List(user_data), - "delete-users": List(Int()), - "create-groups": List(group_data), - "update-groups": List(group_data), - "delete-groups": List(Int()), - "create-group-members": Dict(Int(), List(Int())), - "delete-group-members": Dict(Int(), List(Int())), - }, + { + "operation-id": Int(), + "create-users": List(user_data), + "update-users": List(user_data), + "delete-users": List(Int()), + "create-groups": List(group_data), + "update-groups": List(group_data), + "delete-groups": List(Int()), + "create-group-members": Dict(Int(), List(Int())), + "delete-group-members": Dict(Int(), List(Int())), + }, # operation-id is only there for responses, and all other are # optional as long as one of them is there (no way to say that yet) - optional=["operation-id", "create-users", "update-users", "delete-users", - "create-groups", "update-groups", "delete-groups", - "create-group-members", "delete-group-members"]) + optional=[ + "operation-id", + "create-users", + "update-users", + "delete-users", + "create-groups", + "update-groups", + "delete-groups", + "create-group-members", + "delete-group-members", + ], +) USERS_2_0 = Message( "users", - {"operation-id": Int(), - "create-users": List(user_data), - "update-users": List(user_data), - "delete-users": List(Int()), - "create-groups": List(group_data), - "update-groups": List(group_data), - "delete-groups": List(Int()), - "create-group-members": Dict(Int(), List(Int())), - "delete-group-members": Dict(Int(), List(Int())), - }, + { + "operation-id": Int(), + "create-users": List(user_data), + "update-users": List(user_data), + "delete-users": List(Int()), + "create-groups": List(group_data), + "update-groups": List(group_data), + "delete-groups": List(Int()), + "create-group-members": Dict(Int(), List(Int())), + "delete-group-members": Dict(Int(), List(Int())), + }, # operation-id is only there for responses, and all other are # optional as long as one of them is there (no way to say that yet) - optional=["operation-id", "create-users", "update-users", "delete-users", - "create-groups", "update-groups", "delete-groups", - "create-group-members", "delete-group-members"]) + optional=[ + "operation-id", + "create-users", + "update-users", + "delete-users", + "create-groups", + "update-groups", + "delete-groups", + "create-group-members", + "delete-group-members", + ], +) opt_str = Any(Unicode(), Constant(None)) OLD_USERS = Message( "users", - {"users": List(KeyDict({"username": Unicode(), - "uid": Int(), - "realname": opt_str, - "location": opt_str, - "home-phone": opt_str, - "work-phone": opt_str, - "enabled": Bool()}, - optional=["location", "home-phone", "work-phone"])), - "groups": List(KeyDict({"gid": Int(), - "name": Unicode(), - "members": List(Unicode())}))}, - optional=["groups"]) + { + "users": List( + KeyDict( + { + "username": Unicode(), + "uid": Int(), + "realname": opt_str, + "location": opt_str, + "home-phone": opt_str, + "work-phone": opt_str, + "enabled": Bool(), + }, + optional=["location", "home-phone", "work-phone"], + ), + ), + "groups": List( + KeyDict( + {"gid": Int(), "name": Unicode(), "members": List(Unicode())}, + ), + ), + }, + optional=["groups"], +) package_ids_or_ranges = List(Any(Tuple(Int(), Int()), Int())) PACKAGES = Message( "packages", - {"installed": package_ids_or_ranges, - "available": package_ids_or_ranges, - "available-upgrades": package_ids_or_ranges, - "locked": package_ids_or_ranges, - "autoremovable": package_ids_or_ranges, - "not-autoremovable": package_ids_or_ranges, - "security": package_ids_or_ranges, - "not-installed": package_ids_or_ranges, - "not-available": package_ids_or_ranges, - "not-available-upgrades": package_ids_or_ranges, - "not-locked": package_ids_or_ranges, - "not-security": package_ids_or_ranges}, - optional=["installed", "available", "available-upgrades", "locked", - "not-available", "not-installed", "not-available-upgrades", - "not-locked", "autoremovable", "not-autoremovable", "security", - "not-security"]) + { + "installed": package_ids_or_ranges, + "available": package_ids_or_ranges, + "available-upgrades": package_ids_or_ranges, + "locked": package_ids_or_ranges, + "autoremovable": package_ids_or_ranges, + "not-autoremovable": package_ids_or_ranges, + "security": package_ids_or_ranges, + "not-installed": package_ids_or_ranges, + "not-available": package_ids_or_ranges, + "not-available-upgrades": package_ids_or_ranges, + "not-locked": package_ids_or_ranges, + "not-security": package_ids_or_ranges, + }, + optional=[ + "installed", + "available", + "available-upgrades", + "locked", + "not-available", + "not-installed", + "not-available-upgrades", + "not-locked", + "autoremovable", + "not-autoremovable", + "security", + "not-security", + ], +) package_locks = List(Tuple(Unicode(), Unicode(), Unicode())) PACKAGE_LOCKS = Message( "package-locks", - {"created": package_locks, - "deleted": package_locks}, - optional=["created", "deleted"]) + {"created": package_locks, "deleted": package_locks}, + optional=["created", "deleted"], +) CHANGE_PACKAGE_HOLDS = Message( "change-package-holds", - {"created": List(Unicode()), - "deleted": List(Unicode())}, - optional=["created", "deleted"]) + {"created": List(Unicode()), "deleted": List(Unicode())}, + optional=["created", "deleted"], +) CHANGE_PACKAGES_RESULT = Message( "change-packages-result", - {"operation-id": Int(), - "must-install": List(Any(Int(), Constant(None))), - "must-remove": List(Any(Int(), Constant(None))), - "result-code": Int(), - "result-text": Unicode()}, - optional=["result-text", "must-install", "must-remove"]) - -UNKNOWN_PACKAGE_HASHES = Message("unknown-package-hashes", { - "hashes": List(Bytes()), - "request-id": Int(), - }) + { + "operation-id": Int(), + "must-install": List(Any(Int(), Constant(None))), + "must-remove": List(Any(Int(), Constant(None))), + "result-code": Int(), + "result-text": Unicode(), + }, + optional=["result-text", "must-install", "must-remove"], +) + +UNKNOWN_PACKAGE_HASHES = Message( + "unknown-package-hashes", + { + "hashes": List(Bytes()), + "request-id": Int(), + }, +) PACKAGE_REPORTER_RESULT = Message( - "package-reporter-result", { - "report-timestamp": Float(), - "code": Int(), - "err": Unicode()}, - optional=["report-timestamp"]) - -ADD_PACKAGES = Message("add-packages", { - "packages": List(KeyDict({"name": Unicode(), - "description": Unicode(), - "section": Unicode(), - "relations": List(Tuple(Int(), Unicode())), - "summary": Unicode(), - "installed-size": Any(Int(), Constant(None)), - "size": Any(Int(), Constant(None)), - "version": Unicode(), - "type": Int(), - })), - "request-id": Int(), - }) - -TEXT_MESSAGE = Message("text-message", { - "message": Unicode()}) + "package-reporter-result", + {"report-timestamp": Float(), "code": Int(), "err": Unicode()}, + optional=["report-timestamp"], +) + +ADD_PACKAGES = Message( + "add-packages", + { + "packages": List( + KeyDict( + { + "name": Unicode(), + "description": Unicode(), + "section": Unicode(), + "relations": List(Tuple(Int(), Unicode())), + "summary": Unicode(), + "installed-size": Any(Int(), Constant(None)), + "size": Any(Int(), Constant(None)), + "version": Unicode(), + "type": Int(), + }, + ), + ), + "request-id": Int(), + }, +) + +TEXT_MESSAGE = Message("text-message", {"message": Unicode()}) TEST = Message( "test", - {"greeting": Bytes(), - "consistency-error": Bool(), - "echo": Bytes(), - "sequence": Int()}, - optional=["greeting", "consistency-error", "echo", "sequence"]) + { + "greeting": Bytes(), + "consistency-error": Bool(), + "echo": Bytes(), + "sequence": Int(), + }, + optional=["greeting", "consistency-error", "echo", "sequence"], +) # The tuples are timestamp, value -GRAPH_DATA = KeyDict({"values": List(Tuple(Float(), Float())), - "error": Unicode(), - "script-hash": Bytes()}) +GRAPH_DATA = KeyDict( + { + "values": List(Tuple(Float(), Float())), + "error": Unicode(), + "script-hash": Bytes(), + }, +) -CUSTOM_GRAPH = Message("custom-graph", { - "data": Dict(Int(), GRAPH_DATA)}) +CUSTOM_GRAPH = Message("custom-graph", {"data": Dict(Int(), GRAPH_DATA)}) # XXX This is kept for backward compatibility, it can eventually be removed # when all clients will support REBOOT_REQUIRED_INFO -REBOOT_REQUIRED = Message( - "reboot-required", - {"flag": Bool()}) +REBOOT_REQUIRED = Message("reboot-required", {"flag": Bool()}) REBOOT_REQUIRED_INFO = Message( "reboot-required-info", - {"flag": Bool(), - "packages": List(Unicode())}, - optional=["flag", "packages"]) + {"flag": Bool(), "packages": List(Unicode())}, + optional=["flag", "packages"], +) APT_PREFERENCES = Message( "apt-preferences", - {"data": Any(Dict(Unicode(), Unicode()), Constant(None))}) + {"data": Any(Dict(Unicode(), Unicode()), Constant(None))}, +) EUCALYPTUS_INFO = Message( "eucalyptus-info", - {"basic_info": Dict(Bytes(), Any(Bytes(), Constant(None))), - "walrus_info": Bytes(), - "cluster_controller_info": Bytes(), - "storage_controller_info": Bytes(), - "node_controller_info": Bytes(), - "capacity_info": Bytes()}, - optional=["capacity_info"]) - -EUCALYPTUS_INFO_ERROR = Message( - "eucalyptus-info-error", - {"error": Bytes()}) + { + "basic_info": Dict(Bytes(), Any(Bytes(), Constant(None))), + "walrus_info": Bytes(), + "cluster_controller_info": Bytes(), + "storage_controller_info": Bytes(), + "node_controller_info": Bytes(), + "capacity_info": Bytes(), + }, + optional=["capacity_info"], +) + +EUCALYPTUS_INFO_ERROR = Message("eucalyptus-info-error", {"error": Bytes()}) # The network-device message is split in two top level keys because we don't # support adding sub-keys in a backwards-compatible way (only top-level keys). @@ -484,17 +708,25 @@ # simply ignore the extra info.. NETWORK_DEVICE = Message( "network-device", - {"devices": List(KeyDict({"interface": Bytes(), - "ip_address": Bytes(), - "mac_address": Bytes(), - "broadcast_address": Bytes(), - "netmask": Bytes(), - "flags": Int()})), - - "device-speeds": List(KeyDict({"interface": Bytes(), - "speed": Int(), - "duplex": Bool()}))}, - optional=["device-speeds"]) + { + "devices": List( + KeyDict( + { + "interface": Bytes(), + "ip_address": Bytes(), + "mac_address": Bytes(), + "broadcast_address": Bytes(), + "netmask": Bytes(), + "flags": Int(), + }, + ), + ), + "device-speeds": List( + KeyDict({"interface": Bytes(), "speed": Int(), "duplex": Bool()}), + ), + }, + optional=["device-speeds"], +) NETWORK_ACTIVITY = Message( @@ -503,29 +735,66 @@ # an interface a is a list of 3-tuples (step, in, out), where 'step' is the # time interval and 'in'/'out' are number of bytes received/sent over the # interval. - {"activities": Dict(Bytes(), List(Tuple(Int(), Int(), Int())))}) + {"activities": Dict(Bytes(), List(Tuple(Int(), Int(), Int())))}, +) UPDATE_MANAGER_INFO = Message("update-manager-info", {"prompt": Unicode()}) COMPUTER_TAGS = Message( "computer-tags", - {"tags": Any(Unicode(), Constant(None))}) + {"tags": Any(Unicode(), Constant(None))}, +) + +UBUNTU_PRO_INFO = Message("ubuntu-pro-info", {"ubuntu-pro-info": Unicode()}) -UBUNTU_PRO_INFO = Message( - "ubuntu-pro-info", - {"ubuntu-pro-info": Unicode()}) +LISTENING_PORTS_INFO = Message( + "listening-ports-info", + {"ports": List(Dict(Bytes(), Unicode()))}, +) message_schemas = ( - ACTIVE_PROCESS_INFO, COMPUTER_UPTIME, CLIENT_UPTIME, - OPERATION_RESULT, COMPUTER_INFO, DISTRIBUTION_INFO, - HARDWARE_INVENTORY, HARDWARE_INFO, LOAD_AVERAGE, MEMORY_INFO, - RESYNCHRONIZE, MOUNT_ACTIVITY, MOUNT_INFO, FREE_SPACE, - REGISTER, REGISTER_3_3, - TEMPERATURE, PROCESSOR_INFO, USERS, PACKAGES, PACKAGE_LOCKS, - CHANGE_PACKAGES_RESULT, UNKNOWN_PACKAGE_HASHES, - ADD_PACKAGES, PACKAGE_REPORTER_RESULT, TEXT_MESSAGE, TEST, - CUSTOM_GRAPH, REBOOT_REQUIRED, APT_PREFERENCES, - NETWORK_DEVICE, NETWORK_ACTIVITY, - REBOOT_REQUIRED_INFO, UPDATE_MANAGER_INFO, CPU_USAGE, - CEPH_USAGE, SWIFT_USAGE, SWIFT_DEVICE_INFO, KEYSTONE_TOKEN, - JUJU_UNITS_INFO, CLOUD_METADATA, COMPUTER_TAGS, UBUNTU_PRO_INFO) + ACTIVE_PROCESS_INFO, + COMPUTER_UPTIME, + CLIENT_UPTIME, + OPERATION_RESULT, + COMPUTER_INFO, + DISTRIBUTION_INFO, + HARDWARE_INVENTORY, + HARDWARE_INFO, + LOAD_AVERAGE, + MEMORY_INFO, + RESYNCHRONIZE, + MOUNT_ACTIVITY, + MOUNT_INFO, + FREE_SPACE, + REGISTER, + REGISTER_3_3, + TEMPERATURE, + PROCESSOR_INFO, + USERS, + PACKAGES, + PACKAGE_LOCKS, + CHANGE_PACKAGES_RESULT, + UNKNOWN_PACKAGE_HASHES, + ADD_PACKAGES, + PACKAGE_REPORTER_RESULT, + TEXT_MESSAGE, + TEST, + CUSTOM_GRAPH, + REBOOT_REQUIRED, + APT_PREFERENCES, + NETWORK_DEVICE, + NETWORK_ACTIVITY, + REBOOT_REQUIRED_INFO, + UPDATE_MANAGER_INFO, + CPU_USAGE, + CEPH_USAGE, + SWIFT_USAGE, + SWIFT_DEVICE_INFO, + KEYSTONE_TOKEN, + JUJU_UNITS_INFO, + CLOUD_METADATA, + COMPUTER_TAGS, + UBUNTU_PRO_INFO, + LISTENING_PORTS_INFO, +) From ae7336ef50267bbd034b2273e80a10ad072e73b8 Mon Sep 17 00:00:00 2001 From: Juanmi Taboada Date: Sun, 26 Feb 2023 13:38:37 +0100 Subject: [PATCH 02/11] Original source code returned to its original format --- landscape/client/monitor/config.py | 41 +- landscape/message_schemas/server_bound.py | 991 ++++++++-------------- 2 files changed, 375 insertions(+), 657 deletions(-) diff --git a/landscape/client/monitor/config.py b/landscape/client/monitor/config.py index f1e3f0e56..b40e49cdf 100644 --- a/landscape/client/monitor/config.py +++ b/landscape/client/monitor/config.py @@ -1,28 +1,12 @@ from landscape.client.deployment import Configuration -ALL_PLUGINS = [ - "ActiveProcessInfo", - "ComputerInfo", - "LoadAverage", - "MemoryInfo", - "MountInfo", - "ProcessorInfo", - "Temperature", - "PackageMonitor", - "UserMonitor", - "RebootRequired", - "AptPreferences", - "NetworkActivity", - "NetworkDevice", - "UpdateManager", - "CPUUsage", - "SwiftUsage", - "CephUsage", - "ComputerTags", - "UbuntuProInfo", - "ListeningPorts", -] +ALL_PLUGINS = ["ActiveProcessInfo", "ComputerInfo", + "LoadAverage", "MemoryInfo", "MountInfo", "ProcessorInfo", + "Temperature", "PackageMonitor", "UserMonitor", + "RebootRequired", "AptPreferences", "NetworkActivity", + "NetworkDevice", "UpdateManager", "CPUUsage", "SwiftUsage", + "CephUsage", "ComputerTags", "UbuntuProInfo", "ListeningPorts"] class MonitorConfiguration(Configuration): @@ -33,15 +17,12 @@ def make_parser(self): Specialize L{Configuration.make_parser}, adding many monitor-specific options. """ - parser = super().make_parser() + parser = super(MonitorConfiguration, self).make_parser() - parser.add_option( - "--monitor-plugins", - metavar="PLUGIN_LIST", - help="Comma-delimited list of monitor plugins to " - "use. ALL means use all plugins.", - default="ALL", - ) + parser.add_option("--monitor-plugins", metavar="PLUGIN_LIST", + help="Comma-delimited list of monitor plugins to " + "use. ALL means use all plugins.", + default="ALL") return parser @property diff --git a/landscape/message_schemas/server_bound.py b/landscape/message_schemas/server_bound.py index fc0dd12c0..385005382 100644 --- a/landscape/message_schemas/server_bound.py +++ b/landscape/message_schemas/server_bound.py @@ -1,64 +1,27 @@ # Copyright 2017 Canonical Limited. All rights reserved. + +from landscape.lib.schema import ( + KeyDict, Dict, List, Tuple, + Bool, Int, Float, Bytes, Unicode, Constant, Any) from .message import Message -from landscape.lib.schema import Any -from landscape.lib.schema import Bool -from landscape.lib.schema import Bytes -from landscape.lib.schema import Constant -from landscape.lib.schema import Dict -from landscape.lib.schema import Float -from landscape.lib.schema import Int -from landscape.lib.schema import KeyDict -from landscape.lib.schema import List -from landscape.lib.schema import Tuple -from landscape.lib.schema import Unicode __all__ = [ - "ACTIVE_PROCESS_INFO", - "COMPUTER_UPTIME", - "CLIENT_UPTIME", - "OPERATION_RESULT", - "COMPUTER_INFO", - "DISTRIBUTION_INFO", - "HARDWARE_INVENTORY", - "HARDWARE_INFO", - "LOAD_AVERAGE", - "MEMORY_INFO", - "RESYNCHRONIZE", - "MOUNT_ACTIVITY", - "MOUNT_INFO", - "FREE_SPACE", - "REGISTER", - "REGISTER_3_3", - "TEMPERATURE", - "PROCESSOR_INFO", - "USERS", - "PACKAGES", - "PACKAGE_LOCKS", - "CHANGE_PACKAGES_RESULT", - "UNKNOWN_PACKAGE_HASHES", - "ADD_PACKAGES", - "PACKAGE_REPORTER_RESULT", - "TEXT_MESSAGE", - "TEST", - "CUSTOM_GRAPH", - "REBOOT_REQUIRED", - "APT_PREFERENCES", - "NETWORK_DEVICE", - "NETWORK_ACTIVITY", - "REBOOT_REQUIRED_INFO", - "UPDATE_MANAGER_INFO", - "CPU_USAGE", - "CEPH_USAGE", - "SWIFT_USAGE", - "SWIFT_DEVICE_INFO", - "KEYSTONE_TOKEN", - "JUJU_UNITS_INFO", - "CLOUD_METADATA", - "COMPUTER_TAGS", - "UBUNTU_PRO_INFO", + "ACTIVE_PROCESS_INFO", "COMPUTER_UPTIME", "CLIENT_UPTIME", + "OPERATION_RESULT", "COMPUTER_INFO", "DISTRIBUTION_INFO", + "HARDWARE_INVENTORY", "HARDWARE_INFO", "LOAD_AVERAGE", "MEMORY_INFO", + "RESYNCHRONIZE", "MOUNT_ACTIVITY", "MOUNT_INFO", "FREE_SPACE", + "REGISTER", "REGISTER_3_3", + "TEMPERATURE", "PROCESSOR_INFO", "USERS", "PACKAGES", "PACKAGE_LOCKS", + "CHANGE_PACKAGES_RESULT", "UNKNOWN_PACKAGE_HASHES", + "ADD_PACKAGES", "PACKAGE_REPORTER_RESULT", "TEXT_MESSAGE", "TEST", + "CUSTOM_GRAPH", "REBOOT_REQUIRED", "APT_PREFERENCES", + "NETWORK_DEVICE", "NETWORK_ACTIVITY", + "REBOOT_REQUIRED_INFO", "UPDATE_MANAGER_INFO", "CPU_USAGE", + "CEPH_USAGE", "SWIFT_USAGE", "SWIFT_DEVICE_INFO", "KEYSTONE_TOKEN", + "JUJU_UNITS_INFO", "CLOUD_METADATA", "COMPUTER_TAGS", "UBUNTU_PRO_INFO", "LISTENING_PORTS_INFO", -] + ] # When adding a new schema, which deprecates an older schema, the recommended @@ -69,234 +32,160 @@ # USERS_2_1 -process_info = KeyDict( - { - "pid": Int(), - "name": Unicode(), - "state": Bytes(), - "sleep-average": Int(), - "uid": Int(), - "gid": Int(), - "vm-size": Int(), - "start-time": Int(), - "percent-cpu": Float(), - }, - # Optional for backwards compatibility - optional=["vm-size", "sleep-average", "percent-cpu"], -) +process_info = KeyDict({"pid": Int(), + "name": Unicode(), + "state": Bytes(), + "sleep-average": Int(), + "uid": Int(), + "gid": Int(), + "vm-size": Int(), + "start-time": Int(), + "percent-cpu": Float()}, + # Optional for backwards compatibility + optional=["vm-size", "sleep-average", "percent-cpu"]) ACTIVE_PROCESS_INFO = Message( "active-process-info", - { - "kill-processes": List(Int()), - "kill-all-processes": Bool(), - "add-processes": List(process_info), - "update-processes": List(process_info), - }, + {"kill-processes": List(Int()), + "kill-all-processes": Bool(), + "add-processes": List(process_info), + "update-processes": List(process_info)}, # XXX Really we don't want all three of these keys to be optional: # we always want _something_... - optional=[ - "add-processes", - "update-processes", - "kill-processes", - "kill-all-processes", - ], -) + optional=["add-processes", "update-processes", "kill-processes", + "kill-all-processes"]) COMPUTER_UPTIME = Message( "computer-uptime", - {"startup-times": List(Int()), "shutdown-times": List(Int())}, + {"startup-times": List(Int()), + "shutdown-times": List(Int())}, # XXX Again, one or the other. - optional=["startup-times", "shutdown-times"], -) + optional=["startup-times", "shutdown-times"]) CLIENT_UPTIME = Message( "client-uptime", - {"period": Tuple(Float(), Float()), "components": List(Int())}, - optional=["components"], -) # just for backwards compatibility + {"period": Tuple(Float(), Float()), + "components": List(Int())}, + optional=["components"]) # just for backwards compatibility OPERATION_RESULT = Message( "operation-result", - { - "operation-id": Int(), - "status": Int(), - "result-code": Int(), - "result-text": Unicode(), - }, - optional=["result-code", "result-text"], -) + {"operation-id": Int(), + "status": Int(), + "result-code": Int(), + "result-text": Unicode()}, + optional=["result-code", "result-text"]) COMPUTER_INFO = Message( "computer-info", - { - "hostname": Unicode(), - "total-memory": Int(), - "total-swap": Int(), - "annotations": Dict(Unicode(), Unicode()), - }, + {"hostname": Unicode(), + "total-memory": Int(), + "total-swap": Int(), + "annotations": Dict(Unicode(), Unicode())}, # Not sure why these are all optional, but it's explicitly tested # in the server - optional=["hostname", "total-memory", "total-swap", "annotations"], -) + optional=["hostname", "total-memory", "total-swap", "annotations"]) DISTRIBUTION_INFO = Message( "distribution-info", - { - "distributor-id": Unicode(), - "description": Unicode(), - "release": Unicode(), - "code-name": Unicode(), - }, + {"distributor-id": Unicode(), + "description": Unicode(), + "release": Unicode(), + "code-name": Unicode()}, # all optional because the lsb-release file may not have all data. - optional=["distributor-id", "description", "release", "code-name"], -) + optional=["distributor-id", "description", "release", "code-name"]) CLOUD_METADATA = Message( "cloud-instance-metadata", - { - "instance-id": Unicode(), - "ami-id": Unicode(), - "instance-type": Unicode(), - }, -) + {"instance-id": Unicode(), + "ami-id": Unicode(), + "instance-type": Unicode()}) -hal_data = Dict( - Unicode(), - Any(Unicode(), List(Unicode()), Bool(), Int(), Float()), -) +hal_data = Dict(Unicode(), + Any(Unicode(), List(Unicode()), Bool(), Int(), Float())) -HARDWARE_INVENTORY = Message( - "hardware-inventory", - { - "devices": List( - Any( - Tuple(Constant("create"), hal_data), - Tuple( - Constant("update"), - Unicode(), # udi, - hal_data, # creates, - hal_data, # updates, - hal_data, - ), # deletes - Tuple(Constant("delete"), Unicode()), - ), - ), - }, -) +HARDWARE_INVENTORY = Message("hardware-inventory", { + "devices": List(Any(Tuple(Constant("create"), hal_data), + Tuple(Constant("update"), + Unicode(), # udi, + hal_data, # creates, + hal_data, # updates, + hal_data), # deletes + Tuple(Constant("delete"), + Unicode()), + ), + )}) -HARDWARE_INFO = Message("hardware-info", {"data": Unicode()}) +HARDWARE_INFO = Message("hardware-info", { + "data": Unicode()}) -juju_data = { - "environment-uuid": Unicode(), - "api-addresses": List(Unicode()), - "unit-name": Unicode(), - "private-address": Unicode(), -} +juju_data = {"environment-uuid": Unicode(), + "api-addresses": List(Unicode()), + "unit-name": Unicode(), + "private-address": Unicode()} # The copy of juju_data is needed because Message mutates the dictionary -JUJU_UNITS_INFO = Message( - "juju-units-info", - { - "juju-info-list": List( - KeyDict(juju_data.copy(), optional=["private-address"]), - ), - }, -) - -LOAD_AVERAGE = Message( - "load-average", - { - "load-averages": List(Tuple(Int(), Float())), - }, -) - -CPU_USAGE = Message( - "cpu-usage", - { - "cpu-usages": List(Tuple(Int(), Float())), - }, -) - -CEPH_USAGE = Message( - "ceph-usage", - { - "ring-id": Unicode(), - # Usage data points in the form (timestamp, size, avail, used) - "data-points": List(Tuple(Int(), Int(), Int(), Int())), - # Unused now, for backwards compatibility - "ceph-usages": List(None), - }, -) - -SWIFT_DEVICE_INFO = Message( - "swift-device-info", - { - "swift-device-info": List( - KeyDict({"device": Unicode(), "mounted": Bool()}), - ), - }, -) - -SWIFT_USAGE = Message( - "swift-usage", - { - # Usage data points in the form (timestamp, device, size, avail, used) - "data-points": List(Tuple(Int(), Unicode(), Int(), Int(), Int())), - }, -) - -KEYSTONE_TOKEN = Message( - "keystone-token", - {"data": Any(Bytes(), Constant(None))}, -) - -MEMORY_INFO = Message( - "memory-info", - { - "memory-info": List(Tuple(Float(), Int(), Int())), - }, -) +JUJU_UNITS_INFO = Message("juju-units-info", { + "juju-info-list": List(KeyDict(juju_data.copy(), + optional=["private-address"])) + }) + +LOAD_AVERAGE = Message("load-average", { + "load-averages": List(Tuple(Int(), Float())), + }) + +CPU_USAGE = Message("cpu-usage", { + "cpu-usages": List(Tuple(Int(), Float())), + }) + +CEPH_USAGE = Message("ceph-usage", { + "ring-id": Unicode(), + # Usage data points in the form (timestamp, size, avail, used) + "data-points": List(Tuple(Int(), Int(), Int(), Int())), + # Unused now, for backwards compatibility + "ceph-usages": List(None)}) + +SWIFT_DEVICE_INFO = Message("swift-device-info", { + "swift-device-info": List( + KeyDict({"device": Unicode(), "mounted": Bool()})) + }) + +SWIFT_USAGE = Message("swift-usage", { + # Usage data points in the form (timestamp, device, size, avail, used) + "data-points": List(Tuple(Int(), Unicode(), Int(), Int(), Int()))}) + +KEYSTONE_TOKEN = Message("keystone-token", { + "data": Any(Bytes(), Constant(None)) +}) + +MEMORY_INFO = Message("memory-info", { + "memory-info": List(Tuple(Float(), Int(), Int())), + }) RESYNCHRONIZE = Message( "resynchronize", {"operation-id": Int()}, # operation-id is only there if it's a response to a server-initiated # resynchronize. - optional=["operation-id"], -) + optional=["operation-id"]) -MOUNT_ACTIVITY = Message( - "mount-activity", - {"activities": List(Tuple(Float(), Unicode(), Bool()))}, -) +MOUNT_ACTIVITY = Message("mount-activity", { + "activities": List(Tuple(Float(), Unicode(), Bool()))}) -MOUNT_INFO = Message( - "mount-info", - { - "mount-info": List( - Tuple( - Float(), - KeyDict( - { - "mount-point": Unicode(), - "device": Unicode(), - "filesystem": Unicode(), - "total-space": Int(), - }, - ), - ), - ), - }, -) +MOUNT_INFO = Message("mount-info", { + "mount-info": List(Tuple(Float(), + KeyDict({"mount-point": Unicode(), + "device": Unicode(), + "filesystem": Unicode(), + "total-space": Int()}) + )), + }) -FREE_SPACE = Message( - "free-space", - {"free-space": List(Tuple(Float(), Unicode(), Int()))}, -) +FREE_SPACE = Message("free-space", { + "free-space": List(Tuple(Float(), Unicode(), Int()))}) REGISTER = Message( @@ -304,25 +193,16 @@ # The term used in the UI is actually 'registration_key', but we keep # the message schema field as 'registration_password' in case a new # client contacts an older server. - { - "registration_password": Any(Unicode(), Constant(None)), - "computer_title": Unicode(), - "hostname": Unicode(), - "account_name": Unicode(), - "tags": Any(Unicode(), Constant(None)), - "vm-info": Bytes(), - "container-info": Unicode(), - "access_group": Unicode(), - }, - optional=[ - "registration_password", - "hostname", - "tags", - "vm-info", - "container-info", - "access_group", - ], -) + {"registration_password": Any(Unicode(), Constant(None)), + "computer_title": Unicode(), + "hostname": Unicode(), + "account_name": Unicode(), + "tags": Any(Unicode(), Constant(None)), + "vm-info": Bytes(), + "container-info": Unicode(), + "access_group": Unicode()}, + optional=["registration_password", "hostname", "tags", "vm-info", + "container-info", "access_group"]) REGISTER_3_3 = Message( @@ -330,38 +210,23 @@ # The term used in the UI is actually 'registration_key', but we keep # the message schema field as 'registration_password' in case a new # client contacts an older server. - { - "registration_password": Any(Unicode(), Constant(None)), - "computer_title": Unicode(), - "hostname": Unicode(), - "account_name": Unicode(), - "tags": Any(Unicode(), Constant(None)), - "vm-info": Bytes(), - "container-info": Unicode(), - "juju-info": KeyDict( - { - "environment-uuid": Unicode(), - "api-addresses": List(Unicode()), - "machine-id": Unicode(), - }, - ), - "access_group": Unicode(), - "clone_secure_id": Any(Unicode(), Constant(None)), - "ubuntu_pro_info": Unicode(), - }, + {"registration_password": Any(Unicode(), Constant(None)), + "computer_title": Unicode(), + "hostname": Unicode(), + "account_name": Unicode(), + "tags": Any(Unicode(), Constant(None)), + "vm-info": Bytes(), + "container-info": Unicode(), + "juju-info": KeyDict({"environment-uuid": Unicode(), + "api-addresses": List(Unicode()), + "machine-id": Unicode()}), + "access_group": Unicode(), + "clone_secure_id": Any(Unicode(), Constant(None)), + "ubuntu_pro_info": Unicode()}, api=b"3.3", - optional=[ - "registration_password", - "hostname", - "tags", - "vm-info", - "container-info", - "access_group", - "juju-info", - "clone_secure_id", - "ubuntu_pro_info", - ], -) + optional=["registration_password", "hostname", "tags", "vm-info", + "container-info", "access_group", "juju-info", + "clone_secure_id", "ubuntu_pro_info"]) # XXX The register-provisioned-machine message is obsolete, it's kept around @@ -369,8 +234,7 @@ # to have it is 14.07). Eventually it shall be dropped. REGISTER_PROVISIONED_MACHINE = Message( "register-provisioned-machine", - {"otp": Bytes()}, -) + {"otp": Bytes()}) # XXX The register-cloud-vm message is obsolete, it's kept around just to not @@ -378,329 +242,242 @@ # is 14.07). Eventually it shall be dropped. REGISTER_CLOUD_VM = Message( "register-cloud-vm", - { - "hostname": Unicode(), - "otp": Any(Bytes(), Constant(None)), - "instance_key": Unicode(), - "account_name": Any(Unicode(), Constant(None)), - "registration_password": Any(Unicode(), Constant(None)), - "reservation_key": Unicode(), - "public_hostname": Unicode(), - "local_hostname": Unicode(), - "kernel_key": Any(Unicode(), Constant(None)), - "ramdisk_key": Any(Unicode(), Constant(None)), - "launch_index": Int(), - "image_key": Unicode(), - "tags": Any(Unicode(), Constant(None)), - "vm-info": Bytes(), - "public_ipv4": Unicode(), - "local_ipv4": Unicode(), - "access_group": Unicode(), - }, - optional=["tags", "vm-info", "public_ipv4", "local_ipv4", "access_group"], -) - - -TEMPERATURE = Message( - "temperature", - { - "thermal-zone": Unicode(), - "temperatures": List(Tuple(Int(), Float())), - }, -) + {"hostname": Unicode(), + "otp": Any(Bytes(), Constant(None)), + "instance_key": Unicode(), + "account_name": Any(Unicode(), Constant(None)), + "registration_password": Any(Unicode(), Constant(None)), + "reservation_key": Unicode(), + "public_hostname": Unicode(), + "local_hostname": Unicode(), + "kernel_key": Any(Unicode(), Constant(None)), + "ramdisk_key": Any(Unicode(), Constant(None)), + "launch_index": Int(), + "image_key": Unicode(), + "tags": Any(Unicode(), Constant(None)), + "vm-info": Bytes(), + "public_ipv4": Unicode(), + "local_ipv4": Unicode(), + "access_group": Unicode()}, + optional=["tags", "vm-info", "public_ipv4", "local_ipv4", "access_group"]) + + +TEMPERATURE = Message("temperature", { + "thermal-zone": Unicode(), + "temperatures": List(Tuple(Int(), Float())), + }) PROCESSOR_INFO = Message( "processor-info", - { - "processors": List( - KeyDict( - { - "processor-id": Int(), - "vendor": Unicode(), - "model": Unicode(), - "cache-size": Int(), - }, - optional=["vendor", "cache-size"], - ), - ), - }, -) - -user_data = KeyDict( - { - "uid": Int(), - "username": Unicode(), - "name": Any(Unicode(), Constant(None)), - "enabled": Bool(), - "location": Any(Unicode(), Constant(None)), - "home-phone": Any(Unicode(), Constant(None)), - "work-phone": Any(Unicode(), Constant(None)), - "primary-gid": Any(Int(), Constant(None)), - "primary-groupname": Unicode(), - }, - optional=["primary-groupname", "primary-gid"], -) - -group_data = KeyDict({"gid": Int(), "name": Unicode()}) + {"processors": List(KeyDict({"processor-id": Int(), + "vendor": Unicode(), + "model": Unicode(), + "cache-size": Int(), + }, + optional=["vendor", "cache-size"]))}) + +user_data = KeyDict({ + "uid": Int(), + "username": Unicode(), + "name": Any(Unicode(), Constant(None)), + "enabled": Bool(), + "location": Any(Unicode(), Constant(None)), + "home-phone": Any(Unicode(), Constant(None)), + "work-phone": Any(Unicode(), Constant(None)), + "primary-gid": Any(Int(), Constant(None)), + "primary-groupname": Unicode()}, + optional=["primary-groupname", "primary-gid"]) + +group_data = KeyDict({ + "gid": Int(), + "name": Unicode()}) USERS = Message( "users", - { - "operation-id": Int(), - "create-users": List(user_data), - "update-users": List(user_data), - "delete-users": List(Unicode()), - "create-groups": List(group_data), - "update-groups": List(group_data), - "delete-groups": List(Unicode()), - "create-group-members": Dict(Unicode(), List(Unicode())), - "delete-group-members": Dict(Unicode(), List(Unicode())), - }, + {"operation-id": Int(), + "create-users": List(user_data), + "update-users": List(user_data), + "delete-users": List(Unicode()), + "create-groups": List(group_data), + "update-groups": List(group_data), + "delete-groups": List(Unicode()), + "create-group-members": Dict(Unicode(), List(Unicode())), + "delete-group-members": Dict(Unicode(), List(Unicode())), + }, # operation-id is only there for responses, and all other are # optional as long as one of them is there (no way to say that yet) - optional=[ - "operation-id", - "create-users", - "update-users", - "delete-users", - "create-groups", - "update-groups", - "delete-groups", - "create-group-members", - "delete-group-members", - ], -) + optional=["operation-id", "create-users", "update-users", "delete-users", + "create-groups", "update-groups", "delete-groups", + "create-group-members", "delete-group-members"]) USERS_2_1 = Message( "users", - { - "operation-id": Int(), - "create-users": List(user_data), - "update-users": List(user_data), - "delete-users": List(Int()), - "create-groups": List(group_data), - "update-groups": List(group_data), - "delete-groups": List(Int()), - "create-group-members": Dict(Int(), List(Int())), - "delete-group-members": Dict(Int(), List(Int())), - }, + {"operation-id": Int(), + "create-users": List(user_data), + "update-users": List(user_data), + "delete-users": List(Int()), + "create-groups": List(group_data), + "update-groups": List(group_data), + "delete-groups": List(Int()), + "create-group-members": Dict(Int(), List(Int())), + "delete-group-members": Dict(Int(), List(Int())), + }, # operation-id is only there for responses, and all other are # optional as long as one of them is there (no way to say that yet) - optional=[ - "operation-id", - "create-users", - "update-users", - "delete-users", - "create-groups", - "update-groups", - "delete-groups", - "create-group-members", - "delete-group-members", - ], -) + optional=["operation-id", "create-users", "update-users", "delete-users", + "create-groups", "update-groups", "delete-groups", + "create-group-members", "delete-group-members"]) USERS_2_0 = Message( "users", - { - "operation-id": Int(), - "create-users": List(user_data), - "update-users": List(user_data), - "delete-users": List(Int()), - "create-groups": List(group_data), - "update-groups": List(group_data), - "delete-groups": List(Int()), - "create-group-members": Dict(Int(), List(Int())), - "delete-group-members": Dict(Int(), List(Int())), - }, + {"operation-id": Int(), + "create-users": List(user_data), + "update-users": List(user_data), + "delete-users": List(Int()), + "create-groups": List(group_data), + "update-groups": List(group_data), + "delete-groups": List(Int()), + "create-group-members": Dict(Int(), List(Int())), + "delete-group-members": Dict(Int(), List(Int())), + }, # operation-id is only there for responses, and all other are # optional as long as one of them is there (no way to say that yet) - optional=[ - "operation-id", - "create-users", - "update-users", - "delete-users", - "create-groups", - "update-groups", - "delete-groups", - "create-group-members", - "delete-group-members", - ], -) + optional=["operation-id", "create-users", "update-users", "delete-users", + "create-groups", "update-groups", "delete-groups", + "create-group-members", "delete-group-members"]) opt_str = Any(Unicode(), Constant(None)) OLD_USERS = Message( "users", - { - "users": List( - KeyDict( - { - "username": Unicode(), - "uid": Int(), - "realname": opt_str, - "location": opt_str, - "home-phone": opt_str, - "work-phone": opt_str, - "enabled": Bool(), - }, - optional=["location", "home-phone", "work-phone"], - ), - ), - "groups": List( - KeyDict( - {"gid": Int(), "name": Unicode(), "members": List(Unicode())}, - ), - ), - }, - optional=["groups"], -) + {"users": List(KeyDict({"username": Unicode(), + "uid": Int(), + "realname": opt_str, + "location": opt_str, + "home-phone": opt_str, + "work-phone": opt_str, + "enabled": Bool()}, + optional=["location", "home-phone", "work-phone"])), + "groups": List(KeyDict({"gid": Int(), + "name": Unicode(), + "members": List(Unicode())}))}, + optional=["groups"]) package_ids_or_ranges = List(Any(Tuple(Int(), Int()), Int())) PACKAGES = Message( "packages", - { - "installed": package_ids_or_ranges, - "available": package_ids_or_ranges, - "available-upgrades": package_ids_or_ranges, - "locked": package_ids_or_ranges, - "autoremovable": package_ids_or_ranges, - "not-autoremovable": package_ids_or_ranges, - "security": package_ids_or_ranges, - "not-installed": package_ids_or_ranges, - "not-available": package_ids_or_ranges, - "not-available-upgrades": package_ids_or_ranges, - "not-locked": package_ids_or_ranges, - "not-security": package_ids_or_ranges, - }, - optional=[ - "installed", - "available", - "available-upgrades", - "locked", - "not-available", - "not-installed", - "not-available-upgrades", - "not-locked", - "autoremovable", - "not-autoremovable", - "security", - "not-security", - ], -) + {"installed": package_ids_or_ranges, + "available": package_ids_or_ranges, + "available-upgrades": package_ids_or_ranges, + "locked": package_ids_or_ranges, + "autoremovable": package_ids_or_ranges, + "not-autoremovable": package_ids_or_ranges, + "security": package_ids_or_ranges, + "not-installed": package_ids_or_ranges, + "not-available": package_ids_or_ranges, + "not-available-upgrades": package_ids_or_ranges, + "not-locked": package_ids_or_ranges, + "not-security": package_ids_or_ranges}, + optional=["installed", "available", "available-upgrades", "locked", + "not-available", "not-installed", "not-available-upgrades", + "not-locked", "autoremovable", "not-autoremovable", "security", + "not-security"]) package_locks = List(Tuple(Unicode(), Unicode(), Unicode())) PACKAGE_LOCKS = Message( "package-locks", - {"created": package_locks, "deleted": package_locks}, - optional=["created", "deleted"], -) + {"created": package_locks, + "deleted": package_locks}, + optional=["created", "deleted"]) CHANGE_PACKAGE_HOLDS = Message( "change-package-holds", - {"created": List(Unicode()), "deleted": List(Unicode())}, - optional=["created", "deleted"], -) + {"created": List(Unicode()), + "deleted": List(Unicode())}, + optional=["created", "deleted"]) CHANGE_PACKAGES_RESULT = Message( "change-packages-result", - { - "operation-id": Int(), - "must-install": List(Any(Int(), Constant(None))), - "must-remove": List(Any(Int(), Constant(None))), - "result-code": Int(), - "result-text": Unicode(), - }, - optional=["result-text", "must-install", "must-remove"], -) - -UNKNOWN_PACKAGE_HASHES = Message( - "unknown-package-hashes", - { - "hashes": List(Bytes()), - "request-id": Int(), - }, -) + {"operation-id": Int(), + "must-install": List(Any(Int(), Constant(None))), + "must-remove": List(Any(Int(), Constant(None))), + "result-code": Int(), + "result-text": Unicode()}, + optional=["result-text", "must-install", "must-remove"]) + +UNKNOWN_PACKAGE_HASHES = Message("unknown-package-hashes", { + "hashes": List(Bytes()), + "request-id": Int(), + }) PACKAGE_REPORTER_RESULT = Message( - "package-reporter-result", - {"report-timestamp": Float(), "code": Int(), "err": Unicode()}, - optional=["report-timestamp"], -) - -ADD_PACKAGES = Message( - "add-packages", - { - "packages": List( - KeyDict( - { - "name": Unicode(), - "description": Unicode(), - "section": Unicode(), - "relations": List(Tuple(Int(), Unicode())), - "summary": Unicode(), - "installed-size": Any(Int(), Constant(None)), - "size": Any(Int(), Constant(None)), - "version": Unicode(), - "type": Int(), - }, - ), - ), - "request-id": Int(), - }, -) - -TEXT_MESSAGE = Message("text-message", {"message": Unicode()}) + "package-reporter-result", { + "report-timestamp": Float(), + "code": Int(), + "err": Unicode()}, + optional=["report-timestamp"]) + +ADD_PACKAGES = Message("add-packages", { + "packages": List(KeyDict({"name": Unicode(), + "description": Unicode(), + "section": Unicode(), + "relations": List(Tuple(Int(), Unicode())), + "summary": Unicode(), + "installed-size": Any(Int(), Constant(None)), + "size": Any(Int(), Constant(None)), + "version": Unicode(), + "type": Int(), + })), + "request-id": Int(), + }) + +TEXT_MESSAGE = Message("text-message", { + "message": Unicode()}) TEST = Message( "test", - { - "greeting": Bytes(), - "consistency-error": Bool(), - "echo": Bytes(), - "sequence": Int(), - }, - optional=["greeting", "consistency-error", "echo", "sequence"], -) + {"greeting": Bytes(), + "consistency-error": Bool(), + "echo": Bytes(), + "sequence": Int()}, + optional=["greeting", "consistency-error", "echo", "sequence"]) # The tuples are timestamp, value -GRAPH_DATA = KeyDict( - { - "values": List(Tuple(Float(), Float())), - "error": Unicode(), - "script-hash": Bytes(), - }, -) +GRAPH_DATA = KeyDict({"values": List(Tuple(Float(), Float())), + "error": Unicode(), + "script-hash": Bytes()}) -CUSTOM_GRAPH = Message("custom-graph", {"data": Dict(Int(), GRAPH_DATA)}) +CUSTOM_GRAPH = Message("custom-graph", { + "data": Dict(Int(), GRAPH_DATA)}) # XXX This is kept for backward compatibility, it can eventually be removed # when all clients will support REBOOT_REQUIRED_INFO -REBOOT_REQUIRED = Message("reboot-required", {"flag": Bool()}) +REBOOT_REQUIRED = Message( + "reboot-required", + {"flag": Bool()}) REBOOT_REQUIRED_INFO = Message( "reboot-required-info", - {"flag": Bool(), "packages": List(Unicode())}, - optional=["flag", "packages"], -) + {"flag": Bool(), + "packages": List(Unicode())}, + optional=["flag", "packages"]) APT_PREFERENCES = Message( "apt-preferences", - {"data": Any(Dict(Unicode(), Unicode()), Constant(None))}, -) + {"data": Any(Dict(Unicode(), Unicode()), Constant(None))}) EUCALYPTUS_INFO = Message( "eucalyptus-info", - { - "basic_info": Dict(Bytes(), Any(Bytes(), Constant(None))), - "walrus_info": Bytes(), - "cluster_controller_info": Bytes(), - "storage_controller_info": Bytes(), - "node_controller_info": Bytes(), - "capacity_info": Bytes(), - }, - optional=["capacity_info"], -) - -EUCALYPTUS_INFO_ERROR = Message("eucalyptus-info-error", {"error": Bytes()}) + {"basic_info": Dict(Bytes(), Any(Bytes(), Constant(None))), + "walrus_info": Bytes(), + "cluster_controller_info": Bytes(), + "storage_controller_info": Bytes(), + "node_controller_info": Bytes(), + "capacity_info": Bytes()}, + optional=["capacity_info"]) + +EUCALYPTUS_INFO_ERROR = Message( + "eucalyptus-info-error", + {"error": Bytes()}) # The network-device message is split in two top level keys because we don't # support adding sub-keys in a backwards-compatible way (only top-level keys). @@ -708,25 +485,17 @@ # simply ignore the extra info.. NETWORK_DEVICE = Message( "network-device", - { - "devices": List( - KeyDict( - { - "interface": Bytes(), - "ip_address": Bytes(), - "mac_address": Bytes(), - "broadcast_address": Bytes(), - "netmask": Bytes(), - "flags": Int(), - }, - ), - ), - "device-speeds": List( - KeyDict({"interface": Bytes(), "speed": Int(), "duplex": Bool()}), - ), - }, - optional=["device-speeds"], -) + {"devices": List(KeyDict({"interface": Bytes(), + "ip_address": Bytes(), + "mac_address": Bytes(), + "broadcast_address": Bytes(), + "netmask": Bytes(), + "flags": Int()})), + + "device-speeds": List(KeyDict({"interface": Bytes(), + "speed": Int(), + "duplex": Bool()}))}, + optional=["device-speeds"]) NETWORK_ACTIVITY = Message( @@ -735,66 +504,34 @@ # an interface a is a list of 3-tuples (step, in, out), where 'step' is the # time interval and 'in'/'out' are number of bytes received/sent over the # interval. - {"activities": Dict(Bytes(), List(Tuple(Int(), Int(), Int())))}, -) + {"activities": Dict(Bytes(), List(Tuple(Int(), Int(), Int())))}) UPDATE_MANAGER_INFO = Message("update-manager-info", {"prompt": Unicode()}) COMPUTER_TAGS = Message( "computer-tags", - {"tags": Any(Unicode(), Constant(None))}, -) + {"tags": Any(Unicode(), Constant(None))}) -UBUNTU_PRO_INFO = Message("ubuntu-pro-info", {"ubuntu-pro-info": Unicode()}) +UBUNTU_PRO_INFO = Message( + "ubuntu-pro-info", + {"ubuntu-pro-info": Unicode()}) LISTENING_PORTS_INFO = Message( "listening-ports-info", {"ports": List(Dict(Bytes(), Unicode()))}, ) - message_schemas = ( - ACTIVE_PROCESS_INFO, - COMPUTER_UPTIME, - CLIENT_UPTIME, - OPERATION_RESULT, - COMPUTER_INFO, - DISTRIBUTION_INFO, - HARDWARE_INVENTORY, - HARDWARE_INFO, - LOAD_AVERAGE, - MEMORY_INFO, - RESYNCHRONIZE, - MOUNT_ACTIVITY, - MOUNT_INFO, - FREE_SPACE, - REGISTER, - REGISTER_3_3, - TEMPERATURE, - PROCESSOR_INFO, - USERS, - PACKAGES, - PACKAGE_LOCKS, - CHANGE_PACKAGES_RESULT, - UNKNOWN_PACKAGE_HASHES, - ADD_PACKAGES, - PACKAGE_REPORTER_RESULT, - TEXT_MESSAGE, - TEST, - CUSTOM_GRAPH, - REBOOT_REQUIRED, - APT_PREFERENCES, - NETWORK_DEVICE, - NETWORK_ACTIVITY, - REBOOT_REQUIRED_INFO, - UPDATE_MANAGER_INFO, - CPU_USAGE, - CEPH_USAGE, - SWIFT_USAGE, - SWIFT_DEVICE_INFO, - KEYSTONE_TOKEN, - JUJU_UNITS_INFO, - CLOUD_METADATA, - COMPUTER_TAGS, - UBUNTU_PRO_INFO, - LISTENING_PORTS_INFO, -) + ACTIVE_PROCESS_INFO, COMPUTER_UPTIME, CLIENT_UPTIME, + OPERATION_RESULT, COMPUTER_INFO, DISTRIBUTION_INFO, + HARDWARE_INVENTORY, HARDWARE_INFO, LOAD_AVERAGE, MEMORY_INFO, + RESYNCHRONIZE, MOUNT_ACTIVITY, MOUNT_INFO, FREE_SPACE, + REGISTER, REGISTER_3_3, + TEMPERATURE, PROCESSOR_INFO, USERS, PACKAGES, PACKAGE_LOCKS, + CHANGE_PACKAGES_RESULT, UNKNOWN_PACKAGE_HASHES, + ADD_PACKAGES, PACKAGE_REPORTER_RESULT, TEXT_MESSAGE, TEST, + CUSTOM_GRAPH, REBOOT_REQUIRED, APT_PREFERENCES, + NETWORK_DEVICE, NETWORK_ACTIVITY, + REBOOT_REQUIRED_INFO, UPDATE_MANAGER_INFO, CPU_USAGE, + CEPH_USAGE, SWIFT_USAGE, SWIFT_DEVICE_INFO, KEYSTONE_TOKEN, + JUJU_UNITS_INFO, CLOUD_METADATA, COMPUTER_TAGS, UBUNTU_PRO_INFO, + LISTENING_PORTS_INFO) From 52d62c35408b7f88a34270f398861bc17a563021 Mon Sep 17 00:00:00 2001 From: Juanmi Taboada Date: Sun, 26 Feb 2023 14:07:54 +0100 Subject: [PATCH 03/11] docstring corrections --- landscape/client/monitor/tests/test_listeningports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/landscape/client/monitor/tests/test_listeningports.py b/landscape/client/monitor/tests/test_listeningports.py index cdcfa90ba..995968b24 100644 --- a/landscape/client/monitor/tests/test_listeningports.py +++ b/landscape/client/monitor/tests/test_listeningports.py @@ -37,13 +37,13 @@ def test_resynchronize(self): def test_run_interval(self): """ - The L{UpdateManager} plugin will be scheduled to run every hour. + The L{ListeningPorts} plugin will be scheduled to run every hour. """ self.assertEqual(60, self.plugin.run_interval) def test_run_immediately(self): """ - The L{UpdateManager} plugin will be run immediately at startup. + The L{ListeningPorts} plugin will be run immediately at startup. """ self.assertTrue(True, self.plugin.run_immediately) From 34d94ab331db59331114462cda57d9b26e0d2134 Mon Sep 17 00:00:00 2001 From: Juanmi Taboada Date: Tue, 28 Feb 2023 12:19:13 +0100 Subject: [PATCH 04/11] ListeningPort simplified with Pydantic --- landscape/client/monitor/listeningports.py | 2 +- .../monitor/tests/test_listeningports.py | 8 +-- landscape/lib/security.py | 70 ++++++++----------- landscape/lib/tests/test_security.py | 70 ++++++++++--------- 4 files changed, 69 insertions(+), 81 deletions(-) diff --git a/landscape/client/monitor/listeningports.py b/landscape/client/monitor/listeningports.py index eb44aa471..ca1954399 100644 --- a/landscape/client/monitor/listeningports.py +++ b/landscape/client/monitor/listeningports.py @@ -20,7 +20,7 @@ def send_message(self, urgent=False): message = { "type": "listening-ports-info", - "ports": [port.canonical for port in ports], + "ports": [port.dict() for port in ports], } logging.info( "Queueing message with updated " "listening-ports status.", diff --git a/landscape/client/monitor/tests/test_listeningports.py b/landscape/client/monitor/tests/test_listeningports.py index 995968b24..38e67820f 100644 --- a/landscape/client/monitor/tests/test_listeningports.py +++ b/landscape/client/monitor/tests/test_listeningports.py @@ -4,7 +4,7 @@ from landscape.client.tests.helpers import LandscapeTest from landscape.client.tests.helpers import MonitorHelper from landscape.lib.testing import LogKeeperHelper -from landscape.lib.tests.test_security import sample_listening_ports_canonical +from landscape.lib.tests.test_security import sample_listening_ports_dict from landscape.lib.tests.test_security import sample_subprocess_run @@ -73,14 +73,14 @@ def test_send_message(self): "Queueing message with updated listening-ports status.", self.logfile.getvalue(), ) - canonical_sample = sample_listening_ports_canonical() + dict_sample = sample_listening_ports_dict() self.assertMessages( self.mstore.get_pending_messages(), - [{"type": "listening-ports-info", "ports": canonical_sample}], + [{"type": "listening-ports-info", "ports": dict_sample}], ) self.mstore.delete_all_messages() self.plugin.send_message() self.assertMessages( self.mstore.get_pending_messages(), - canonical_sample, + dict_sample, ) diff --git a/landscape/lib/security.py b/landscape/lib/security.py index 030717e8d..85c494672 100644 --- a/landscape/lib/security.py +++ b/landscape/lib/security.py @@ -1,57 +1,34 @@ import subprocess +from pydantic import BaseModel, validator __all__ = ["get_listeningports"] +lsof_cmd = "/usr/bin/lsof" +awk_cmd = "/usr/bin/awk" -class ListeningPort: - """ - Details about a listeining port in the system - """ - lsof_cmd = "/usr/bin/lsof" - awk_cmd = "/usr/bin/awk" +class ListeningPort(BaseModel): + cmd: str + pid: str + user: str + kind: str + mode: str + port: str - def __init__(self, cmd, pid, user, kind, mode, port, *args): - self.cmd = cmd - self.pid = pid - self.user = user - self.kind = kind - self.mode = mode - self.port = port + @validator("pid") + def pid_must_be_integer(cls, v): # noqa: N805 + return str(int(v)) - @property - def canonical(self): - return { - "cmd": self.cmd, - "pid": self.pid, - "user": self.user, - "kind": self.kind, - "mode": self.mode, - "port": self.port, - } - - def __eq__(self, other): - return ( - self.cmd == other.cmd - and self.pid == other.pid - and self.user == other.user - and self.kind == other.kind - and self.mode == other.mode - and self.port == other.port - ) - - def __repr__(self): - return ( - f"{self.cmd} {self.pid} {self.user} " - f"{self.kind} {self.mode} {self.port}" - ) + @validator("port") + def port_must_be_integer(cls, v): # noqa:N805 + return str(int(v)) def get_listeningports(): # Launch lsof to find all ports being used ps = subprocess.run( - [ListeningPort.lsof_cmd, "-i", "-P", "-n"], + [lsof_cmd, "-i", "-P", "-n"], check=True, capture_output=True, ) @@ -60,7 +37,7 @@ def get_listeningports(): # select columns: COMMAND, PID, USER, TYPE, MODE, NAME ps2 = subprocess.run( [ - ListeningPort.awk_cmd, + awk_cmd, '$10 ~ "LISTEN" {n=split($9, a, ":"); ' 'print $1" "$2" "$3" "$5" "$8" "a[n]}', ], @@ -75,6 +52,15 @@ def get_listeningports(): ports = [] for line in output.splitlines(): elements = line.split(" ") - ports.append(ListeningPort(*elements)) + ports.append( + ListeningPort( + **dict( + zip( + ["cmd", "pid", "user", "kind", "mode", "port"], + elements, + ) + ) + ) + ) return ports diff --git a/landscape/lib/tests/test_security.py b/landscape/lib/tests/test_security.py index 37ce7c467..9f162a5f6 100644 --- a/landscape/lib/tests/test_security.py +++ b/landscape/lib/tests/test_security.py @@ -4,6 +4,8 @@ from unittest.mock import patch from landscape.lib import testing +from landscape.lib.security import lsof_cmd +from landscape.lib.security import awk_cmd from landscape.lib.security import get_listeningports from landscape.lib.security import ListeningPort @@ -225,12 +227,18 @@ def sample_listening_ports(): listening = [] for listeningport in EXPECTED_AWK_OUTPUT.splitlines(): args = listeningport.split(" ") - listening.append(ListeningPort(*args)) + listening.append( + ListeningPort( + **dict( + zip(["cmd", "pid", "user", "kind", "mode", "port"], args) + ) + ) + ) return listening -def sample_listening_ports_canonical(): - return [port.canonical for port in sample_listening_ports()] +def sample_listening_ports_dict(): + return [port.dict() for port in sample_listening_ports()] class BaseTestCase( @@ -246,43 +254,37 @@ class ListeningPortsTest(BaseTestCase): def test_analyze_object_behaviour(self): listening1 = ListeningPort( - "cmd", - "pid", - "user", - "kind", - "mode", - "port", + cmd="cmd", + pid=1234, + user="user", + kind="kind", + mode="mode", + port=5678, ) listening2 = ListeningPort( - "cmd", - "pid", - "user", - "kind", - "mode", - "port", + cmd="cmd", + pid=1234, + user="user", + kind="kind", + mode="mode", + port=5678, ) self.assertEqual(listening1, listening2) listening3 = ListeningPort( - "cmdX", - "pid", - "user", - "kind", - "mode", - "port", + cmd="cmdX", + pid=1234, + user="user", + kind="kind", + mode="mode", + port=5678, ) self.assertNotEqual(listening1, listening3) - can = listening1.canonical - for val in ["cmd", "pid", "user", "kind", "mode", "port"]: - attr = getattr(listening1, val) - self.assertEqual(attr, val) - self.assertEqual(attr, can[val]) - def test_cmd_exists_and_executable(self): assert os.access(echo_cmd, os.X_OK) - assert os.access(ListeningPort.lsof_cmd, os.X_OK) - assert os.access(ListeningPort.awk_cmd, os.X_OK) + assert os.access(lsof_cmd, os.X_OK) + assert os.access(awk_cmd, os.X_OK) @patch("landscape.lib.security.subprocess.run", sample_subprocess_run) def test_listeningports(self): @@ -291,10 +293,10 @@ def test_listeningports(self): listening = get_listeningports() self.assertEqual(listening, get_listeningports()) - listening_test_canonical = [port.canonical for port in listening_test] - listening_canonical = [port.canonical for port in listening] - self.assertEqual(listening_test_canonical, listening_canonical) + listening_test_dict = [port.dict() for port in listening_test] + listening_dict = [port.dict() for port in listening] + self.assertEqual(listening_test_dict, listening_dict) self.assertEqual( - sample_listening_ports_canonical(), - listening_canonical, + sample_listening_ports_dict(), + listening_dict, ) From 2b0026c91bb7957d6b1e4c601994d967ff1cdd12 Mon Sep 17 00:00:00 2001 From: Juanmi Taboada Date: Tue, 28 Feb 2023 17:38:11 +0100 Subject: [PATCH 05/11] Support for RKHunter reporting --- debian/control | 2 +- landscape/client/monitor/config.py | 3 +- landscape/client/monitor/rkhunter.py | 38 ++ .../monitor/tests/test_listeningports.py | 17 +- .../client/monitor/tests/test_rkhunter.py | 103 +++ landscape/lib/security.py | 139 +++- ...ity.py => test_security_listeningports.py} | 0 landscape/lib/tests/test_security_rkhunter.py | 609 ++++++++++++++++++ landscape/message_schemas/server_bound.py | 23 +- 9 files changed, 910 insertions(+), 24 deletions(-) create mode 100644 landscape/client/monitor/rkhunter.py create mode 100644 landscape/client/monitor/tests/test_rkhunter.py rename landscape/lib/tests/{test_security.py => test_security_listeningports.py} (100%) create mode 100644 landscape/lib/tests/test_security_rkhunter.py diff --git a/debian/control b/debian/control index 363cc76dd..f661c1f84 100644 --- a/debian/control +++ b/debian/control @@ -6,7 +6,7 @@ XSBC-Original-Maintainer: Landscape Team Build-Depends: debhelper (>= 11), po-debconf, libdistro-info-perl, dh-python, python3-dev, python3-distutils-extra, lsb-release, gawk, net-tools, - python3-apt, python3-twisted, python3-configobj + python3-apt, python3-twisted, python3-configobj, rkhunter Standards-Version: 4.4.0 Homepage: https://github.com/CanonicalLtd/landscape-client diff --git a/landscape/client/monitor/config.py b/landscape/client/monitor/config.py index b40e49cdf..473824e0a 100644 --- a/landscape/client/monitor/config.py +++ b/landscape/client/monitor/config.py @@ -6,7 +6,8 @@ "Temperature", "PackageMonitor", "UserMonitor", "RebootRequired", "AptPreferences", "NetworkActivity", "NetworkDevice", "UpdateManager", "CPUUsage", "SwiftUsage", - "CephUsage", "ComputerTags", "UbuntuProInfo", "ListeningPorts"] + "CephUsage", "ComputerTags", "UbuntuProInfo", "ListeningPorts", + "RKHunterInfo"] class MonitorConfiguration(Configuration): diff --git a/landscape/client/monitor/rkhunter.py b/landscape/client/monitor/rkhunter.py new file mode 100644 index 000000000..bc641489b --- /dev/null +++ b/landscape/client/monitor/rkhunter.py @@ -0,0 +1,38 @@ +import logging + +from landscape.client.monitor.plugin import MonitorPlugin +from landscape.lib.security import RKHunterLogReader # , RKHunterLiveInfo + + +class RKHunterInfo(MonitorPlugin): + """Plugin captures information about rkhunter results.""" + + persist_name = "rkhunter-info" + scope = "security" + run_interval = 86400 # 1 day + run_immediately = True + + def __init__(self, filename="/var/log/rkhunter.log"): + self._filename = filename + + def send_message(self, urgent=False): + rklog = RKHunterLogReader(filename=self._filename) + report = rklog.get_last_log() + if report == self._persist.get("report"): + return + self._persist.set("report", report) + + message = {"type": "rkhunter-info", "report": report.dict()} + logging.info( + "Queueing message with updated rkhunter status.", + ) + return self.registry.broker.send_message(message, self._session_id) + + def run(self, urgent=False): + """ + Send the rkhunter-info messages, if the server accepted them. + """ + return self.registry.broker.call_if_accepted( + "rkhunter-info", + self.send_message, + ) diff --git a/landscape/client/monitor/tests/test_listeningports.py b/landscape/client/monitor/tests/test_listeningports.py index 38e67820f..866267aa5 100644 --- a/landscape/client/monitor/tests/test_listeningports.py +++ b/landscape/client/monitor/tests/test_listeningports.py @@ -4,14 +4,18 @@ from landscape.client.tests.helpers import LandscapeTest from landscape.client.tests.helpers import MonitorHelper from landscape.lib.testing import LogKeeperHelper -from landscape.lib.tests.test_security import sample_listening_ports_dict -from landscape.lib.tests.test_security import sample_subprocess_run +from landscape.lib.tests.test_security_listeningports import ( + sample_listening_ports_dict, +) +from landscape.lib.tests.test_security_listeningports import ( + sample_subprocess_run, +) class ListeningPortsTest(LandscapeTest): """ - Tests relating to the L{ListeningPortsTes} monitoring plug-in, which should - notice changes to listening ports and report these back to + Tests relating to the L{ListeningPortsTest} monitoring plug-in, which + should notice changes to listening ports and report these back to landscape server. """ @@ -80,7 +84,4 @@ def test_send_message(self): ) self.mstore.delete_all_messages() self.plugin.send_message() - self.assertMessages( - self.mstore.get_pending_messages(), - dict_sample, - ) + self.assertMessages(self.mstore.get_pending_messages(), []) diff --git a/landscape/client/monitor/tests/test_rkhunter.py b/landscape/client/monitor/tests/test_rkhunter.py new file mode 100644 index 000000000..b7d10afda --- /dev/null +++ b/landscape/client/monitor/tests/test_rkhunter.py @@ -0,0 +1,103 @@ +import datetime +from unittest import mock +from zoneinfo import ZoneInfo + +from landscape.client.monitor.rkhunter import RKHunterInfo +from landscape.client.tests.helpers import LandscapeTest +from landscape.client.tests.helpers import MonitorHelper +from landscape.lib.testing import LogKeeperHelper +from landscape.lib.tests.test_security_rkhunter import ( + sample_subprocess_run_scan, + COMMON_VERSION, + COMMON_DATETIME, + SAMPLE_RKHUNTER_LOG_2, +) + + +class RKHunterTest(LandscapeTest): + """ + Tests relating to the L{RKHunterTest} monitoring plug-in, which should + notice changes to suspicious files and report these back to + landscape server. + """ + + helpers = [MonitorHelper, LogKeeperHelper] + + def setUp(self): + super().setUp() + self.plugin = RKHunterInfo(self.makeFile(SAMPLE_RKHUNTER_LOG_2)) + self.monitor.add(self.plugin) + self.mstore.set_accepted_types(["rkhunter-info"]) + + @mock.patch( + "landscape.lib.security.subprocess.run", sample_subprocess_run_scan + ) + def test_resynchronize(self): + """ + The "resynchronize" reactor message cause the plugin to send fresh + data. + """ + self.plugin.run() + self.reactor.fire("resynchronize", scopes=["security"]) + self.plugin.run() + messages = self.mstore.get_pending_messages() + self.assertEqual(len(messages), 2) + + def test_run_interval(self): + """ + The L{RKHunter} plugin will be scheduled to run every hour. + """ + self.assertEqual(86400, self.plugin.run_interval) + + def test_run_immediately(self): + """ + The L{RKHunter} plugin will be run immediately at startup. + """ + self.assertTrue(True, self.plugin.run_immediately) + + @mock.patch( + "landscape.lib.security.subprocess.run", sample_subprocess_run_scan + ) + def test_run(self): + """ + If the server can accept them, the plugin should send + C{listening-ports} messages. + """ + with mock.patch.object(self.remote, "send_message"): + self.plugin.run() + self.remote.send_message.assert_called_once_with( + mock.ANY, + mock.ANY, + ) + self.mstore.set_accepted_types([]) + self.plugin.run() + + @mock.patch( + "landscape.lib.security.subprocess.run", sample_subprocess_run_scan + ) + def test_send_message(self): + """ + A new C{"listening-ports-info"} message should be enqueued if and only + if the listening-ports status of the system has changed. + """ + self.plugin.send_message() + self.assertIn( + "Queueing message with updated rkhunter status.", + self.logfile.getvalue(), + ) + dict_sample = { + "version": COMMON_VERSION, + "files_checked": 145, + "files_suspect": 48, + "rootkit_checked": 478, + "rootkit_suspect": 1, + "timestamp": COMMON_DATETIME.isoformat(), + } + + self.assertMessages( + self.mstore.get_pending_messages(), + [{"type": "rkhunter-info", "report": dict_sample}], + ) + self.mstore.delete_all_messages() + self.plugin.send_message() + self.assertMessages(self.mstore.get_pending_messages(), []) diff --git a/landscape/lib/security.py b/landscape/lib/security.py index 85c494672..9c859b368 100644 --- a/landscape/lib/security.py +++ b/landscape/lib/security.py @@ -1,3 +1,7 @@ +import os +import re +from dateutil import parser +from datetime import datetime import subprocess from pydantic import BaseModel, validator @@ -5,23 +9,16 @@ lsof_cmd = "/usr/bin/lsof" awk_cmd = "/usr/bin/awk" +rkhunter_cmd = "/usr/bin/rkhunter" class ListeningPort(BaseModel): cmd: str - pid: str + pid: int user: str kind: str mode: str - port: str - - @validator("pid") - def pid_must_be_integer(cls, v): # noqa: N805 - return str(int(v)) - - @validator("port") - def port_must_be_integer(cls, v): # noqa:N805 - return str(int(v)) + port: int def get_listeningports(): @@ -64,3 +61,125 @@ def get_listeningports(): ) return ports + + +class RKHunterInfo(BaseModel): + timestamp: str + files_checked: int + files_suspect: int + rootkit_checked: int + rootkit_suspect: int + version: str + + @validator("timestamp", pre=True) + def timesgtamp_validate(cls, timestamp): # noqa: N805 + return timestamp.isoformat() + + +class RKHunterBase: + def get_version(self): + ps = subprocess.run( + [rkhunter_cmd, "--version"], + check=True, + capture_output=True, + ) + firstline = ps.stdout.decode("utf-8").split("\n")[0].strip() + return firstline.split(" ")[-1] + + def _extract(self, regex, line, is_timestamp): + found = re.search(regex, line) + if found: + if is_timestamp: + ts = " ".join(found.groups()[-1].split(" ")[-5:]) + return parser.parse(ts) + + else: + return int(found.groups()[-1]) + else: + return None + + def _analize(self, lines, from_log=False): + info = { + "files_checked": r"^((\[.*\])|)\ *Files checked: (.*?)$", + "files_suspect": r"^((\[.*\])|)\ *Suspect files: (.*?)$", + "rootkit_checked": r"^((\[.*\])|)\ *Rootkits checked\ : (.*?)$", + "rootkit_suspect": r"^((\[.*\])|)\ *Possible rootkits: (.*?)$", + } + + # Read timestamp from log + if from_log: + info["timestamp"] = r"^\[.*\]\ Info: End date is (.*?)$" + + result = {} + for line in lines: + if from_log: + line = line.split("\n")[0] + for key, value in info.items(): + if key not in result.keys(): + found = self._extract(value, line, key == "timestamp") + if found is not None: + result[key] = found + if len(result) == len(info): + # We got all of them + break + return result + + +class RKHunterLogReader(RKHunterBase): + def __init__(self, filename="/var/log/rkhunter.log"): + self._filename = filename + + def get_last_log(self): + + # Get version + version = self.get_version() + + # Get file size + size = os.stat(self._filename).st_size + + with open(self._filename, "r") as file: + + # Read last 1024 bytes or whatever we find if it less in reverse + file.seek(size - min(size, 1024)) + lines = file.readlines() + lines.reverse() + + # Analize lines + result = self._analize(lines, from_log=True) + + # We expect 5 fields found + if len(result) == 5: + return RKHunterInfo(version=version, **result) + else: + return None + + +class RKHunterLiveInfo(RKHunterBase): + WARNING_CHECK_LST = [ + "Checking for hidden files and directories", + "Checking for prerequisites", + "Checking if SSH root access is allowed", + ] + + def execute(self): + + # Get version + version = self.get_version() + + # Execute rkhunter + ps = subprocess.run( + [rkhunter_cmd, "-c", "--sk", "--nocolors", "--noappend-log"], + check=True, + capture_output=True, + ) + lines = ps.stdout.decode("utf-8").strip() + + result = self._analize(lines.split("\n")) + + # We expect 4 fields found + if len(result) == 4: + return RKHunterInfo( + timestamp=datetime.now(), version=version, **result + ) + else: + return None diff --git a/landscape/lib/tests/test_security.py b/landscape/lib/tests/test_security_listeningports.py similarity index 100% rename from landscape/lib/tests/test_security.py rename to landscape/lib/tests/test_security_listeningports.py diff --git a/landscape/lib/tests/test_security_rkhunter.py b/landscape/lib/tests/test_security_rkhunter.py new file mode 100644 index 000000000..8d169a401 --- /dev/null +++ b/landscape/lib/tests/test_security_rkhunter.py @@ -0,0 +1,609 @@ +import os +from datetime import datetime +from zoneinfo import ZoneInfo +from unittest import TestCase +from unittest.mock import patch +from subprocess import run as run_orig + +from landscape.lib import testing + +# from landscape.lib.security import get_listeningports +from landscape.lib.security import ( + RKHunterLogReader, + RKHunterLiveInfo, + RKHunterInfo, + rkhunter_cmd, +) + +COMMON_VERSION = "8.4.3" +COMMON_DATETIME = datetime(2028, 4, 28, 17, 44, 3, tzinfo=ZoneInfo("CET")) + +SAMPLE_RKHUNTER_VERSION = """Rootkit Hunter 8.4.3 + +This software was developed by the Rootkit Hunter project team. +Please review your rkhunter configuration files before using. +Please review the documentation before posting bug reports or questions. +To report bugs, provide patches or comments, please go to: +http://rkhunter.sourceforge.net + +To ask questions about rkhunter, please use the rkhunter-users mailing list. +Note this is a moderated list: please subscribe before posting. + +Rootkit Hunter comes with ABSOLUTELY NO WARRANTY. +This is free software, and you are welcome to redistribute it under the +terms of the GNU General Public License. See the LICENSE file for details. + +""" + +SAMPLE_RKHUNTER_LOG_1 = """[17:44:00] +[17:44:00] System checks summary +[17:44:00] ===================== +[17:44:00] +[17:44:00] File properties checks... +[17:44:00] Files checked: 145 +[17:44:00] Suspect files: 0 +[17:44:00] +[17:44:00] Rootkit checks... +[17:44:00] Rootkits checked : 478 +[17:44:00] Possible rootkits: 1 +[17:44:00rkhunter_info_log +[17:44:00] Applications checks... +[17:44:00] All checks skipped +[17:44:00] +[17:44:00] The system checks took: 2 minutes and 36 seconds +[17:44:00] +[17:44:00] Info: End date is fri 28 apr 2028 17:44:03 CET""" + + +SAMPLE_RKHUNTER_LOG_2 = """[17:44:00] +[17:44:00] System checks summary +[17:44:00] ===================== +[17:44:00] +[17:44:00] File properties checks... +[17:44:00] Files checked: 145 +[17:44:00] Suspect files: 48 +[17:44:00] +[17:44:00] Rootkit checks... +[17:44:00] Rootkits checked : 478 +[17:44:00] Possible rootkits: 1 +[17:44:00rkhunter_info_log +[17:44:00] Applications checks... +[17:44:00] All checks skipped +[17:44:00] +[17:44:00] The system checks took: 2 minutes and 36 seconds +[17:44:00] +[17:44:00] Info: End date is fri 28 apr 2028 17:44:03 CET""" + + +SAMPLE_RKHUNTER_LOG_PARTIAL_1 = """[17:44:00] +[17:44:00] System checks summary +[17:44:00] ===================== +[17:44:00] +[17:44:00] File properties checks... +[17:44:00] Files checked: 145 +[17:44:00] Suspect files: 0 +[17:44:00] +[17:44:00] Rootkit checks... +[17:44:00] Rootkits checked : 478 +[17:44:00] Possible rootkits: 1 +[17:44:00rkhunter_info_log +[17:44:00] Applications checks... +[17:44:00] All checks skipped +[17:44:00] +[17:44:00] The system checks took: 2 minutes and 36 seconds +[17:44:00]""" + +SAMPLE_RKHUNTER_LOG_PARTIAL_2 = """[17:44:00] +[17:44:00] Rootkit checks... +[17:44:00] Rootkits checked : 478 +[17:44:00] Possible rootkits: 1 +[17:44:00rkhunter_info_log +[17:44:00] Applications checks... +[17:44:00] All checks skipped +[17:44:00] +[17:44:00] The system checks took: 2 minutes and 36 seconds +[17:44:00] +[17:44:00] Info: End date is fri 28 apr 2028 17:44:03 CET""" + +SAMPLE_RKHUNTER_EXECUTION_OUTPUT = """[ Rootkit Hunter version 1.4.6 ] + +Checking system commands... + + Performing 'strings' command checks + Checking 'strings' command [ OK ] + + Performing 'shared libraries' checks + Checking for preloading variables [ None found ] + Checking for preloaded libraries [ None found ] + Checking LD_LIBRARY_PATH variable [ Not found ] + + Performing file properties checks + Checking for prerequisites [ OK ] + /usr/sbin/adduser [ OK ] + /usr/sbin/chroot [ OK ] + /usr/sbin/cron [ OK ] + /usr/sbin/depmod [ OK ] + /usr/sbin/fsck [ OK ] + /usr/sbin/groupadd [ OK ] + /usr/sbin/groupdel [ OK ] + /usr/sbin/groupmod [ OK ] + /usr/sbin/grpck [ OK ] + /usr/sbin/ifconfig [ OK ] + /usr/sbin/init [ OK ] + /usr/sbin/insmod [ OK ] + /usr/sbin/ip [ OK ] + /usr/sbin/lsmod [ OK ] + /usr/sbin/modinfo [ OK ] + /usr/sbin/modprobe [ OK ] + /usr/sbin/nologin [ OK ] + /usr/sbin/pwck [ OK ] + /usr/sbin/rmmod [ OK ] + /usr/sbin/route [ OK ] + /usr/sbin/rsyslogd [ OK ] + /usr/sbin/runlevel [ OK ] + /usr/sbin/sulogin [ OK ] + /usr/sbin/sysctl [ OK ] + /usr/sbin/useradd [ OK ] + /usr/sbin/userdel [ OK ] + /usr/sbin/usermod [ OK ] + /usr/sbin/vipw [ OK ] + /usr/sbin/unhide [ OK ] + /usr/sbin/unhide-linux [ OK ] + /usr/sbin/unhide-posix [ OK ] + /usr/sbin/unhide-tcp [ OK ] + /usr/bin/awk [ OK ] + /usr/bin/basename [ OK ] + /usr/bin/bash [ OK ] + /usr/bin/cat [ OK ] + /usr/bin/chattr [ OK ] + /usr/bin/chmod [ OK ] + /usr/bin/chown [ OK ] + /usr/bin/cp [ OK ] + /usr/bin/curl [ Warning ] +br0th3r@carmen:~/zonaprog/src/canonical/security$ cat rkhunter.output +[ Rootkit Hunter version 1.4.6 ] + +Checking system commands... + + Performing 'strings' command checks + Checking 'strings' command [ OK ] + + Performing 'shared libraries' checks + Checking for preloading variables [ None found ] + Checking for preloaded libraries [ None found ] + Checking LD_LIBRARY_PATH variable [ Not found ] + + Performing file properties checks + Checking for prerequisites [ OK ] + /usr/sbin/adduser [ OK ] + /usr/sbin/chroot [ OK ] + /usr/sbin/cron [ OK ] + /usr/sbin/depmod [ OK ] + /usr/sbin/fsck [ OK ] + /usr/sbin/groupadd [ OK ] + /usr/sbin/groupdel [ OK ] + /usr/sbin/groupmod [ OK ] + /usr/sbin/grpck [ OK ] + /usr/sbin/ifconfig [ OK ] + /usr/sbin/init [ OK ] + /usr/sbin/insmod [ OK ] + /usr/sbin/ip [ OK ] + /usr/sbin/lsmod [ OK ] + /usr/sbin/modinfo [ OK ] + /usr/sbin/modprobe [ OK ] + /usr/sbin/nologin [ OK ] + /usr/sbin/pwck [ OK ] + /usr/sbin/rmmod [ OK ] + /usr/sbin/route [ OK ] + /usr/sbin/rsyslogd [ OK ] + /usr/sbin/runlevel [ OK ] + /usr/sbin/sulogin [ OK ] + /usr/sbin/sysctl [ OK ] + /usr/sbin/useradd [ OK ] + /usr/sbin/userdel [ OK ] + /usr/sbin/usermod [ OK ] + /usr/sbin/vipw [ OK ] + /usr/sbin/unhide [ OK ] + /usr/sbin/unhide-linux [ OK ] + /usr/sbin/unhide-posix [ OK ] + /usr/sbin/unhide-tcp [ OK ] + /usr/bin/awk [ OK ] + /usr/bin/basename [ OK ] + /usr/bin/bash [ OK ] + /usr/bin/cat [ OK ] + /usr/bin/chattr [ OK ] + /usr/bin/chmod [ OK ] + /usr/bin/chown [ OK ] + /usr/bin/cp [ OK ] + /usr/bin/curl [ Warning ] + /usr/bin/cut [ OK ] + /usr/bin/date [ OK ] + /usr/bin/df [ OK ] + /usr/bin/diff [ OK ] + /usr/bin/dirname [ OK ] + /usr/bin/dmesg [ OK ] + /usr/bin/dpkg [ OK ] + /usr/bin/dpkg-query [ OK ] + /usr/bin/du [ OK ] + /usr/bin/echo [ OK ] + /usr/bin/ed [ OK ] + /usr/bin/egrep [ OK ] + /usr/bin/env [ OK ] + /usr/bin/fgrep [ OK ] + /usr/bin/file [ OK ] + /usr/bin/find [ OK ] + /usr/bin/fuser [ OK ] + /usr/bin/GET [ OK ] + /usr/bin/grep [ OK ] + /usr/bin/groups [ OK ] + /usr/bin/head [ OK ] + /usr/bin/id [ OK ] + /usr/bin/ip [ OK ] + /usr/bin/ipcs [ OK ] + /usr/bin/kill [ OK ] + /usr/bin/killall [ OK ] + /usr/bin/last [ OK ] + /usr/bin/lastlog [ OK ] + /usr/bin/ldd [ OK ] + /usr/bin/less [ OK ] + /usr/bin/locate [ OK ] + /usr/bin/logger [ OK ] + /usr/bin/login [ OK ] + /usr/bin/ls [ OK ] + /usr/bin/lsattr [ OK ] + /usr/bin/lsmod [ OK ] + /usr/bin/lsof [ OK ] + /usr/bin/mail [ OK ] + /usr/bin/md5sum [ OK ] + /usr/bin/mktemp [ OK ] + /usr/bin/more [ OK ] + /usr/bin/mount [ OK ] + /usr/bin/mv [ OK ] + /usr/bin/netstat [ OK ] + /usr/bin/newgrp [ OK ] + /usr/bin/passwd [ OK ] + /usr/bin/perl [ OK ] + /usr/bin/pgrep [ OK ] + /usr/bin/ping [ OK ] + /usr/bin/pkill [ OK ] + /usr/bin/ps [ OK ] + /usr/bin/pstree [ OK ] + /usr/bin/pwd [ OK ] + /usr/bin/readlink [ OK ] + /usr/bin/rkhunter [ OK ] + /usr/bin/runcon [ OK ] + /usr/bin/sed [ OK ] + /usr/bin/sh [ OK ] + /usr/bin/sha1sum [ OK ] + /usr/bin/sha224sum [ OK ] + /usr/bin/sha256sum [ OK ] + /usr/bin/sha384sum [ OK ] + /usr/bin/sha512sum [ OK ] + /usr/bin/size [ OK ] + /usr/bin/sort [ OK ] + /usr/bin/ssh [ OK ] + /usr/bin/stat [ OK ] + /usr/bin/strace [ OK ] + /usr/bin/strings [ OK ] + /usr/bin/su [ OK ] + /usr/bin/sudo [ OK ] + /usr/bin/tail [ OK ] + /usr/bin/telnet [ OK ] + /usr/bin/test [ OK ] + /usr/bin/top [ OK ] + /usr/bin/touch [ OK ] + /usr/bin/tr [ OK ] + /usr/bin/uname [ OK ] + /usr/bin/uniq [ OK ] + /usr/bin/users [ OK ] + /usr/bin/vmstat [ OK ] + /usr/bin/w [ OK ] + /usr/bin/watch [ OK ] + /usr/bin/wc [ OK ] + /usr/bin/wget [ OK ] + /usr/bin/whatis [ OK ] + /usr/bin/whereis [ OK ] + /usr/bin/which [ OK ] + /usr/bin/who [ OK ] + /usr/bin/whoami [ OK ] + /usr/bin/numfmt [ OK ] + /usr/bin/kmod [ OK ] + /usr/bin/systemd [ OK ] + /usr/bin/systemctl [ OK ] + /usr/bin/gawk [ OK ] + /usr/bin/lwp-request [ OK ] + /usr/bin/locate.findutils [ OK ] + /usr/bin/bsd-mailx [ OK ] + /usr/bin/dash [ OK ] + /usr/bin/x86_64-linux-gnu-size [ OK ] + /usr/bin/x86_64-linux-gnu-strings [ OK ] + /usr/bin/telnet.netkit [ OK ] + /usr/bin/which.debianutils [ OK ] + /usr/lib/systemd/systemd [ OK ] + +Checking for rootkits... + + Performing check of known rootkit files and directories + 55808 Trojan - Variant A [ Not found ] + ADM Worm [ Not found ] + AjaKit Rootkit [ Not found ] + Adore Rootkit [ Not found ] + aPa Kit [ Not found ] + Apache Worm [ Not found ] + Ambient (ark) Rootkit [ Not found ] + Balaur Rootkit [ Not found ] + BeastKit Rootkit [ Not found ] + beX2 Rootkit [ Not found ] + BOBKit Rootkit [ Not found ] + cb Rootkit [ Not found ] + CiNIK Worm (Slapper.B variant) [ Not found ] + Danny-Boy's Abuse Kit [ Not found ] + Devil RootKit [ Not found ] + Diamorphine LKM [ Not found ] + Dica-Kit Rootkit [ Not found ] + Dreams Rootkit [ Not found ] + Duarawkz Rootkit [ Not found ] + Ebury backdoor [ Not found ] + Enye LKM [ Not found ] + Flea Linux Rootkit [ Not found ] + Fu Rootkit [ Not found ] + Fuck`it Rootkit [ Not found ] + GasKit Rootkit [ Not found ] + Heroin LKM [ Not found ] + HjC Kit [ Not found ] + ignoKit Rootkit [ Not found ] + IntoXonia-NG Rootkit [ Not found ] + Irix Rootkit [ Not found ] + Jynx Rootkit [ Not found ] + Jynx2 Rootkit [ Not found ] + KBeast Rootkit [ Not found ] + Kitko Rootkit [ Not found ] + Knark Rootkit [ Not found ] + ld-linuxv.so Rootkit [ Not found ] + Li0n Worm [ Not found ] + Lockit / LJK2 Rootkit [ Not found ] + Mokes backdoor [ Not found ] + Mood-NT Rootkit [ Not found ] + MRK Rootkit [ Not found ] + Ni0 Rootkit [ Not found ] + Ohhara Rootkit [ Not found ] + Optic Kit (Tux) Worm [ Not found ] + Oz Rootkit [ Not found ] + Phalanx Rootkit [ Not found ] + Phalanx2 Rootkit [ Not found ] + Phalanx2 Rootkit (extended tests) [ Not found ] + Portacelo Rootkit [ Not found ] + R3dstorm Toolkit [ Not found ] + RH-Sharpe's Rootkit [ Not found ] + RSHA's Rootkit [ Not found ] + Scalper Worm [ Not found ] + Sebek LKM [ Not found ] + Shutdown Rootkit [ Not found ] + SHV4 Rootkit [ Not found ] + SHV5 Rootkit [ Not found ] + Sin Rootkit [ Not found ] + Slapper Worm [ Not found ] + Sneakin Rootkit [ Not found ] + 'Spanish' Rootkit [ Not found ] + Suckit Rootkit [ Not found ] + Superkit Rootkit [ Not found ] + TBD (Telnet BackDoor) [ Not found ] + TeLeKiT Rootkit [ Not found ] + T0rn Rootkit [ Not found ] + trNkit Rootkit [ Not found ] + Trojanit Kit [ Not found ] + Tuxtendo Rootkit [ Not found ] + URK Rootkit [ Not found ] + Vampire Rootkit [ Not found ] + VcKit Rootkit [ Not found ] + Volc Rootkit [ Not found ] + Xzibit Rootkit [ Not found ] + zaRwT.KiT Rootkit [ Not found ] + ZK Rootkit [ Not found ] + + Performing additional rootkit checks + Suckit Rootkit additional checks [ OK ] + Checking for possible rootkit files and directories [ None found ] + Checking for possible rootkit strings [ None found ] + + Performing malware checks + Checking running processes for suspicious files [ None found ] + Checking for login backdoors [ None found ] + Checking for sniffer log files [ None found ] + Checking for suspicious directories [ None found ] + Checking for suspicious (large) shared memory segments [ None found ] + Checking for Apache backdoor [ Not found ] + + Performing Linux specific checks + Checking loaded kernel modules [ OK ] + Checking kernel module names [ OK ] + +Checking the network... + + Performing checks on the network ports + Checking for backdoor ports [ None found ] + + Performing checks on the network interfaces + Checking for promiscuous interfaces [ None found ] + +Checking the local host... + + Performing system boot checks + Checking for local host name [ Found ] + Checking for system startup files [ Found ] + Checking system startup files for malware [ None found ] + + Performing group and account checks + Checking for passwd file [ Found ] + Checking for root equivalent (UID 0) accounts [ None found ] + Checking for passwordless accounts [ None found ] + Checking for passwd file changes [ None found ] + Checking for group file changes [ None found ] + Checking root account shell history files [ OK ] + + Performing system configuration file checks + Checking for an SSH configuration file [ Not found ] + Checking for a running system logging daemon [ Found ] + Checking for a system logging configuration file [ Found ] + Checking if syslog remote logging is allowed [ Not allowed ] + + Performing filesystem checks + Checking /dev for suspicious file types [ Warning ] + Checking for hidden files and directories [ Warning ] + + +System checks summary +===================== + +File properties checks... + Files checked: 145 + Suspect files: 1 + +Rootkit checks... + Rootkits checked : 478 + Possible rootkits: 0 + +Applications checks... + All checks skipped + +The system checks took: 3 minutes and 34 seconds + +All results have been written to the log file: /var/log/rkhunter.log + +One or more warnings have been found while checking the system. +Please check the log file (/var/log/rkhunter.log) + +""" + +echo_cmd = "/usr/bin/echo" + + +def sample_subprocess_run_scan( + *args, + **kwargs, +): + if args[0][1] == "-c": + args = ([echo_cmd, "-n", "-e", SAMPLE_RKHUNTER_EXECUTION_OUTPUT],) + elif args[0][1] == "--version": + args = ([echo_cmd, "-n", "-e", SAMPLE_RKHUNTER_VERSION],) + return run_orig(*args, **kwargs) + + +class BaseTestCase( + testing.TwistedTestCase, + testing.FSTestCase, + TestCase, +): + @patch("landscape.lib.security.subprocess.run", sample_subprocess_run_scan) + def test_cmd_version(self): + rklive = RKHunterLiveInfo() + self.assertEqual(rklive.get_version(), COMMON_VERSION) + + +class RKHunterLogTest(BaseTestCase): + """Test for parsing /var/log/rkhunter.log""" + + def test_read_empty_file(self): + filename = self.makeFile("") + rkinfo = RKHunterLogReader(filename) + self.assertEqual(rkinfo.get_last_log(), None) + + @patch( + "landscape.lib.security.datetime", + side_effect=lambda *args, **kw: datetime(*args, **kw), + ) + @patch("landscape.lib.security.subprocess.run", sample_subprocess_run_scan) + def test_read_rkhunter_info_log_1(self, mock_datetime): + mock_datetime.now.return_value = COMMON_DATETIME + filename = self.makeFile(SAMPLE_RKHUNTER_LOG_1) + rkinfo = RKHunterLogReader(filename) + self.assertEqual( + rkinfo.get_last_log(), + RKHunterInfo( + version=COMMON_VERSION, + files_checked=145, + files_suspect=0, + rootkit_checked=478, + rootkit_suspect=1, + timestamp=COMMON_DATETIME, + ), + ) + + @patch( + "landscape.lib.security.datetime", + side_effect=lambda *args, **kw: datetime(*args, **kw), + ) + @patch("landscape.lib.security.subprocess.run", sample_subprocess_run_scan) + def test_read_rkhunter_info_log_2(self, mock_datetime): + mock_datetime.now.return_value = COMMON_DATETIME + filename = self.makeFile(SAMPLE_RKHUNTER_LOG_2) + rkinfo = RKHunterLogReader(filename) + self.assertEqual( + rkinfo.get_last_log(), + RKHunterInfo( + version=COMMON_VERSION, + files_checked=145, + files_suspect=48, + rootkit_checked=478, + rootkit_suspect=1, + timestamp=COMMON_DATETIME, + ), + ) + + def test_read_rkhunter_info_log_partial_1(self): + filename = self.makeFile(SAMPLE_RKHUNTER_LOG_PARTIAL_1) + rkinfo = RKHunterLogReader(filename) + self.assertEqual(rkinfo.get_last_log(), None) + + def test_read_rkhunter_info_log_partial_2(self): + filename = self.makeFile(SAMPLE_RKHUNTER_LOG_PARTIAL_2) + rkinfo = RKHunterLogReader(filename) + self.assertEqual(rkinfo.get_last_log(), None) + + +def sample_subprocess_run_empty_scan( + *args, + **kwargs, +): + if args[0][1] == "-c": + args = ([echo_cmd, "-n", "-e", ""],) + elif args[0][1] == "--version": + args = ([echo_cmd, "-n", "-e", SAMPLE_RKHUNTER_VERSION],) + return run_orig(*args, **kwargs) + + +class RKHunterLiveTest(BaseTestCase): + """Test for parsing rkhunter's output.""" + + def test_base_cmd_exists_and_executable(self): + assert os.access(echo_cmd, os.X_OK) + assert os.access(rkhunter_cmd, os.X_OK) + + @patch( + "landscape.lib.security.subprocess.run", + sample_subprocess_run_empty_scan, + ) + def test_scan_failure_empty(self): + rklive = RKHunterLiveInfo() + self.assertEqual(rklive.execute(), None) + + @patch( + "landscape.lib.security.datetime", + side_effect=lambda *args, **kw: datetime(*args, **kw), + ) + @patch("landscape.lib.security.subprocess.run", sample_subprocess_run_scan) + def test_scan_working(self, mock_datetime): + mock_datetime.now.return_value = COMMON_DATETIME + + rklive = RKHunterLiveInfo() + self.assertEqual( + rklive.execute().dict(), + RKHunterInfo( + version=COMMON_VERSION, + files_checked=145, + files_suspect=1, + rootkit_checked=478, + rootkit_suspect=0, + timestamp=COMMON_DATETIME, + ).dict(), + ) diff --git a/landscape/message_schemas/server_bound.py b/landscape/message_schemas/server_bound.py index 385005382..5e22b098c 100644 --- a/landscape/message_schemas/server_bound.py +++ b/landscape/message_schemas/server_bound.py @@ -20,8 +20,7 @@ "REBOOT_REQUIRED_INFO", "UPDATE_MANAGER_INFO", "CPU_USAGE", "CEPH_USAGE", "SWIFT_USAGE", "SWIFT_DEVICE_INFO", "KEYSTONE_TOKEN", "JUJU_UNITS_INFO", "CLOUD_METADATA", "COMPUTER_TAGS", "UBUNTU_PRO_INFO", - "LISTENING_PORTS_INFO", - ] + "LISTENING_PORTS_INFO", "RKHUNTER_INFO"] # When adding a new schema, which deprecates an older schema, the recommended @@ -518,8 +517,24 @@ LISTENING_PORTS_INFO = Message( "listening-ports-info", - {"ports": List(Dict(Bytes(), Unicode()))}, + {"ports": List(KeyDict({"cmd": Unicode(), + "pid": Int(), + "user": Unicode(), + "kind": Unicode(), + "mode": Unicode(), + "port": Int()}))}, ) + +RKHUNTER_INFO = Message( + "rkhunter-info", + {"report": KeyDict({"timestamp": Unicode(), + "files_checked": Int(), + "files_suspect": Int(), + "rootkit_checked": Int(), + "rootkit_suspect": Int(), + "version": Unicode()})}, +) + message_schemas = ( ACTIVE_PROCESS_INFO, COMPUTER_UPTIME, CLIENT_UPTIME, OPERATION_RESULT, COMPUTER_INFO, DISTRIBUTION_INFO, @@ -534,4 +549,4 @@ REBOOT_REQUIRED_INFO, UPDATE_MANAGER_INFO, CPU_USAGE, CEPH_USAGE, SWIFT_USAGE, SWIFT_DEVICE_INFO, KEYSTONE_TOKEN, JUJU_UNITS_INFO, CLOUD_METADATA, COMPUTER_TAGS, UBUNTU_PRO_INFO, - LISTENING_PORTS_INFO) + LISTENING_PORTS_INFO, RKHUNTER_INFO) From 3e3fa1d974050185b5594dc2cb4a9e6d0f5a49d8 Mon Sep 17 00:00:00 2001 From: Juanmi Taboada Date: Tue, 28 Feb 2023 18:17:30 +0100 Subject: [PATCH 06/11] Zoneinfo removed and depencies corrected --- Makefile | 2 +- debian/control | 9 ++++++--- landscape/client/monitor/tests/test_rkhunter.py | 2 -- landscape/lib/tests/test_security_rkhunter.py | 12 ++++++------ 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index fe183ecbd..a5b9e84d5 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ depends2: .PHONY: depends3 depends3: - sudo apt-get -y install python3-twisted python3-distutils-extra python3-mock python3-configobj python3-netifaces python3-pycurl + sudo apt-get -y install python3-twisted python3-distutils-extra python3-mock python3-configobj python3-netifaces python3-pycurl python3-dateutil python3-pydantic all: build diff --git a/debian/control b/debian/control index f661c1f84..7fe8bdd04 100644 --- a/debian/control +++ b/debian/control @@ -6,7 +6,8 @@ XSBC-Original-Maintainer: Landscape Team Build-Depends: debhelper (>= 11), po-debconf, libdistro-info-perl, dh-python, python3-dev, python3-distutils-extra, lsb-release, gawk, net-tools, - python3-apt, python3-twisted, python3-configobj, rkhunter + python3-apt, python3-twisted, python3-configobj, + python3-dateutil, python3-pydantic, rkhunter Standards-Version: 4.4.0 Homepage: https://github.com/CanonicalLtd/landscape-client @@ -24,7 +25,9 @@ Depends: ${python3:Depends}, ${misc:Depends}, ${extra:Depends}, adduser, bc, lshw, - libpam-modules + libpam-modules, + python-dateutil, + python3-pydantic Description: Landscape administration system client - Common files Landscape is a web-based tool for managing Ubuntu systems. This package is necessary if you want your machine to be managed in a @@ -39,7 +42,7 @@ Architecture: any Depends: ${python3:Depends}, ${misc:Depends}, ${extra:Depends}, ${shlibs:Depends}, landscape-common (= ${binary:Version}), - python3-pycurl, + python3-pycurl, rkhunter Description: Landscape administration system client Landscape is a web-based tool for managing Ubuntu systems. This package is necessary if you want your machine to be managed in a diff --git a/landscape/client/monitor/tests/test_rkhunter.py b/landscape/client/monitor/tests/test_rkhunter.py index b7d10afda..d5d0ed4d4 100644 --- a/landscape/client/monitor/tests/test_rkhunter.py +++ b/landscape/client/monitor/tests/test_rkhunter.py @@ -1,6 +1,4 @@ -import datetime from unittest import mock -from zoneinfo import ZoneInfo from landscape.client.monitor.rkhunter import RKHunterInfo from landscape.client.tests.helpers import LandscapeTest diff --git a/landscape/lib/tests/test_security_rkhunter.py b/landscape/lib/tests/test_security_rkhunter.py index 8d169a401..2479f444e 100644 --- a/landscape/lib/tests/test_security_rkhunter.py +++ b/landscape/lib/tests/test_security_rkhunter.py @@ -1,6 +1,6 @@ import os from datetime import datetime -from zoneinfo import ZoneInfo +from dateutil import parser from unittest import TestCase from unittest.mock import patch from subprocess import run as run_orig @@ -16,7 +16,7 @@ ) COMMON_VERSION = "8.4.3" -COMMON_DATETIME = datetime(2028, 4, 28, 17, 44, 3, tzinfo=ZoneInfo("CET")) +COMMON_DATETIME = parser.parse("28 apr 2028 17:44:03 CET") SAMPLE_RKHUNTER_VERSION = """Rootkit Hunter 8.4.3 @@ -518,7 +518,7 @@ def test_read_rkhunter_info_log_1(self, mock_datetime): filename = self.makeFile(SAMPLE_RKHUNTER_LOG_1) rkinfo = RKHunterLogReader(filename) self.assertEqual( - rkinfo.get_last_log(), + rkinfo.get_last_log().dict(), RKHunterInfo( version=COMMON_VERSION, files_checked=145, @@ -526,7 +526,7 @@ def test_read_rkhunter_info_log_1(self, mock_datetime): rootkit_checked=478, rootkit_suspect=1, timestamp=COMMON_DATETIME, - ), + ).dict(), ) @patch( @@ -539,7 +539,7 @@ def test_read_rkhunter_info_log_2(self, mock_datetime): filename = self.makeFile(SAMPLE_RKHUNTER_LOG_2) rkinfo = RKHunterLogReader(filename) self.assertEqual( - rkinfo.get_last_log(), + rkinfo.get_last_log().dict(), RKHunterInfo( version=COMMON_VERSION, files_checked=145, @@ -547,7 +547,7 @@ def test_read_rkhunter_info_log_2(self, mock_datetime): rootkit_checked=478, rootkit_suspect=1, timestamp=COMMON_DATETIME, - ), + ).dict(), ) def test_read_rkhunter_info_log_partial_1(self): From 46b1630575ebb231a11d57dc3f277cf395e49219 Mon Sep 17 00:00:00 2001 From: Juanmi Taboada Date: Tue, 28 Feb 2023 18:25:04 +0100 Subject: [PATCH 07/11] Module renamed --- landscape/client/monitor/{rkhunter.py => rkhunterinfo.py} | 0 landscape/client/monitor/tests/test_rkhunter.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename landscape/client/monitor/{rkhunter.py => rkhunterinfo.py} (100%) diff --git a/landscape/client/monitor/rkhunter.py b/landscape/client/monitor/rkhunterinfo.py similarity index 100% rename from landscape/client/monitor/rkhunter.py rename to landscape/client/monitor/rkhunterinfo.py diff --git a/landscape/client/monitor/tests/test_rkhunter.py b/landscape/client/monitor/tests/test_rkhunter.py index d5d0ed4d4..719acd5d2 100644 --- a/landscape/client/monitor/tests/test_rkhunter.py +++ b/landscape/client/monitor/tests/test_rkhunter.py @@ -1,6 +1,6 @@ from unittest import mock -from landscape.client.monitor.rkhunter import RKHunterInfo +from landscape.client.monitor.rkhunterinfo import RKHunterInfo from landscape.client.tests.helpers import LandscapeTest from landscape.client.tests.helpers import MonitorHelper from landscape.lib.testing import LogKeeperHelper From 660a14efe070f405e9aff44b735b9c86bed60656 Mon Sep 17 00:00:00 2001 From: Juanmi Taboada Date: Tue, 28 Feb 2023 18:50:05 +0100 Subject: [PATCH 08/11] Improved error support --- landscape/lib/security.py | 30 ++++++++++++++----- landscape/lib/tests/test_security_rkhunter.py | 8 +++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/landscape/lib/security.py b/landscape/lib/security.py index 9c859b368..89a42c9cc 100644 --- a/landscape/lib/security.py +++ b/landscape/lib/security.py @@ -1,5 +1,6 @@ import os import re +import logging from dateutil import parser from datetime import datetime import subprocess @@ -135,17 +136,30 @@ def get_last_log(self): version = self.get_version() # Get file size - size = os.stat(self._filename).st_size + try: + size = os.stat(self._filename).st_size + except FileNotFoundError as e: + logging.warning(f"RKHunter log not found at {self._filename}: {e}") + size = None + except PermissionError as e: + logging.warning( + "Couldn't read RKHunter's log. Permission denied while " + f"accesing to {self._filename}: {e}" + ) + size = None - with open(self._filename, "r") as file: + if size is not None: + with open(self._filename, "r") as file: - # Read last 1024 bytes or whatever we find if it less in reverse - file.seek(size - min(size, 1024)) - lines = file.readlines() - lines.reverse() + # Read last 1024 bytes or whatever is left in reverse + file.seek(size - min(size, 1024)) + lines = file.readlines() + lines.reverse() - # Analize lines - result = self._analize(lines, from_log=True) + # Analize lines + result = self._analize(lines, from_log=True) + else: + result = [] # We expect 5 fields found if len(result) == 5: diff --git a/landscape/lib/tests/test_security_rkhunter.py b/landscape/lib/tests/test_security_rkhunter.py index 2479f444e..3f248b46c 100644 --- a/landscape/lib/tests/test_security_rkhunter.py +++ b/landscape/lib/tests/test_security_rkhunter.py @@ -503,6 +503,14 @@ def test_cmd_version(self): class RKHunterLogTest(BaseTestCase): """Test for parsing /var/log/rkhunter.log""" + def test_read_non_existing_file(self): + rkinfo = RKHunterLogReader("ABC") + self.assertEqual(rkinfo.get_last_log(), None) + + def test_read_non_permissions_file(self): + rkinfo = RKHunterLogReader("/root/abc") + self.assertEqual(rkinfo.get_last_log(), None) + def test_read_empty_file(self): filename = self.makeFile("") rkinfo = RKHunterLogReader(filename) From e5d463e5e4d5860adaa1c644a31ba9f20fb099bf Mon Sep 17 00:00:00 2001 From: Juanmi Taboada Date: Tue, 28 Feb 2023 19:31:57 +0100 Subject: [PATCH 09/11] RKHunter missing in depends inside Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a5b9e84d5..cf420994d 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ depends2: .PHONY: depends3 depends3: - sudo apt-get -y install python3-twisted python3-distutils-extra python3-mock python3-configobj python3-netifaces python3-pycurl python3-dateutil python3-pydantic + sudo apt-get -y install python3-twisted python3-distutils-extra python3-mock python3-configobj python3-netifaces python3-pycurl python3-dateutil python3-pydantic rkhunter all: build From f538b6e494e0e54404d478a236dcd1c0f655cad5 Mon Sep 17 00:00:00 2001 From: Juanmi Taboada Date: Tue, 28 Feb 2023 19:50:11 +0100 Subject: [PATCH 10/11] Added tzinfos support for datutils.parser --- landscape/lib/security.py | 10 ++++++++-- landscape/lib/tests/test_security_rkhunter.py | 4 +++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/landscape/lib/security.py b/landscape/lib/security.py index 89a42c9cc..fff675db7 100644 --- a/landscape/lib/security.py +++ b/landscape/lib/security.py @@ -1,7 +1,7 @@ import os import re import logging -from dateutil import parser +from dateutil import parser, tz from datetime import datetime import subprocess from pydantic import BaseModel, validator @@ -78,6 +78,12 @@ def timesgtamp_validate(cls, timestamp): # noqa: N805 class RKHunterBase: + + tzmapping = { + "CET": tz.gettz("Europe/Berlin"), + "CEST": tz.gettz("Europe/Berlin"), + } + def get_version(self): ps = subprocess.run( [rkhunter_cmd, "--version"], @@ -92,7 +98,7 @@ def _extract(self, regex, line, is_timestamp): if found: if is_timestamp: ts = " ".join(found.groups()[-1].split(" ")[-5:]) - return parser.parse(ts) + return parser.parse(ts, tzinfos=self.tzmapping) else: return int(found.groups()[-1]) diff --git a/landscape/lib/tests/test_security_rkhunter.py b/landscape/lib/tests/test_security_rkhunter.py index 3f248b46c..c1347f878 100644 --- a/landscape/lib/tests/test_security_rkhunter.py +++ b/landscape/lib/tests/test_security_rkhunter.py @@ -16,7 +16,9 @@ ) COMMON_VERSION = "8.4.3" -COMMON_DATETIME = parser.parse("28 apr 2028 17:44:03 CET") +COMMON_DATETIME = parser.parse( + "28 apr 2028 17:44:03 CET", tzinfos=RKHunterLogReader.tzmapping +) SAMPLE_RKHUNTER_VERSION = """Rootkit Hunter 8.4.3 From 36ff0fb96e8d017c8d7c5125fc2aa297542369a0 Mon Sep 17 00:00:00 2001 From: Juanmi Taboada Date: Wed, 24 May 2023 07:19:34 +0200 Subject: [PATCH 11/11] Message type changed from 'rkhunter-info' to 'rootkit-scan-info' --- landscape/client/monitor/rkhunterinfo.py | 10 +++---- .../client/monitor/tests/test_rkhunter.py | 21 ++++++++------ landscape/lib/security.py | 29 +++++++++++-------- landscape/lib/tests/test_security_rkhunter.py | 24 +++++++-------- landscape/message_schemas/server_bound.py | 8 ++--- 5 files changed, 50 insertions(+), 42 deletions(-) diff --git a/landscape/client/monitor/rkhunterinfo.py b/landscape/client/monitor/rkhunterinfo.py index bc641489b..1bf9ba6f2 100644 --- a/landscape/client/monitor/rkhunterinfo.py +++ b/landscape/client/monitor/rkhunterinfo.py @@ -7,7 +7,7 @@ class RKHunterInfo(MonitorPlugin): """Plugin captures information about rkhunter results.""" - persist_name = "rkhunter-info" + persist_name = "rootkit-scan-info" scope = "security" run_interval = 86400 # 1 day run_immediately = True @@ -22,17 +22,17 @@ def send_message(self, urgent=False): return self._persist.set("report", report) - message = {"type": "rkhunter-info", "report": report.dict()} + message = {"type": "rootkit-scan-info", "report": report.dict()} logging.info( - "Queueing message with updated rkhunter status.", + "Queueing message with updated rootkit-scan status.", ) return self.registry.broker.send_message(message, self._session_id) def run(self, urgent=False): """ - Send the rkhunter-info messages, if the server accepted them. + Send the rootkit-scan-info messages, if the server accepted them. """ return self.registry.broker.call_if_accepted( - "rkhunter-info", + "rootkit-scan-info", self.send_message, ) diff --git a/landscape/client/monitor/tests/test_rkhunter.py b/landscape/client/monitor/tests/test_rkhunter.py index 719acd5d2..ec2b4dc4c 100644 --- a/landscape/client/monitor/tests/test_rkhunter.py +++ b/landscape/client/monitor/tests/test_rkhunter.py @@ -4,11 +4,11 @@ from landscape.client.tests.helpers import LandscapeTest from landscape.client.tests.helpers import MonitorHelper from landscape.lib.testing import LogKeeperHelper +from landscape.lib.tests.test_security_rkhunter import COMMON_DATETIME +from landscape.lib.tests.test_security_rkhunter import COMMON_VERSION +from landscape.lib.tests.test_security_rkhunter import SAMPLE_RKHUNTER_LOG_2 from landscape.lib.tests.test_security_rkhunter import ( sample_subprocess_run_scan, - COMMON_VERSION, - COMMON_DATETIME, - SAMPLE_RKHUNTER_LOG_2, ) @@ -25,10 +25,11 @@ def setUp(self): super().setUp() self.plugin = RKHunterInfo(self.makeFile(SAMPLE_RKHUNTER_LOG_2)) self.monitor.add(self.plugin) - self.mstore.set_accepted_types(["rkhunter-info"]) + self.mstore.set_accepted_types(["rootkit-scan-info"]) @mock.patch( - "landscape.lib.security.subprocess.run", sample_subprocess_run_scan + "landscape.lib.security.subprocess.run", + sample_subprocess_run_scan, ) def test_resynchronize(self): """ @@ -54,7 +55,8 @@ def test_run_immediately(self): self.assertTrue(True, self.plugin.run_immediately) @mock.patch( - "landscape.lib.security.subprocess.run", sample_subprocess_run_scan + "landscape.lib.security.subprocess.run", + sample_subprocess_run_scan, ) def test_run(self): """ @@ -71,7 +73,8 @@ def test_run(self): self.plugin.run() @mock.patch( - "landscape.lib.security.subprocess.run", sample_subprocess_run_scan + "landscape.lib.security.subprocess.run", + sample_subprocess_run_scan, ) def test_send_message(self): """ @@ -80,7 +83,7 @@ def test_send_message(self): """ self.plugin.send_message() self.assertIn( - "Queueing message with updated rkhunter status.", + "Queueing message with updated rootkit-scan status.", self.logfile.getvalue(), ) dict_sample = { @@ -94,7 +97,7 @@ def test_send_message(self): self.assertMessages( self.mstore.get_pending_messages(), - [{"type": "rkhunter-info", "report": dict_sample}], + [{"type": "rootkit-scan-info", "report": dict_sample}], ) self.mstore.delete_all_messages() self.plugin.send_message() diff --git a/landscape/lib/security.py b/landscape/lib/security.py index fff675db7..c567f3788 100644 --- a/landscape/lib/security.py +++ b/landscape/lib/security.py @@ -1,10 +1,13 @@ +import logging import os import re -import logging -from dateutil import parser, tz -from datetime import datetime import subprocess -from pydantic import BaseModel, validator +from datetime import datetime + +from dateutil import parser +from dateutil import tz +from pydantic import BaseModel +from pydantic import validator __all__ = ["get_listeningports"] @@ -56,15 +59,15 @@ def get_listeningports(): zip( ["cmd", "pid", "user", "kind", "mode", "port"], elements, - ) - ) - ) + ), + ), + ), ) return ports -class RKHunterInfo(BaseModel): +class RootkitScanInfo(BaseModel): timestamp: str files_checked: int files_suspect: int @@ -150,7 +153,7 @@ def get_last_log(self): except PermissionError as e: logging.warning( "Couldn't read RKHunter's log. Permission denied while " - f"accesing to {self._filename}: {e}" + f"accesing to {self._filename}: {e}", ) size = None @@ -169,7 +172,7 @@ def get_last_log(self): # We expect 5 fields found if len(result) == 5: - return RKHunterInfo(version=version, **result) + return RootkitScanInfo(version=version, **result) else: return None @@ -198,8 +201,10 @@ def execute(self): # We expect 4 fields found if len(result) == 4: - return RKHunterInfo( - timestamp=datetime.now(), version=version, **result + return RootkitScanInfo( + timestamp=datetime.now(), + version=version, + **result, ) else: return None diff --git a/landscape/lib/tests/test_security_rkhunter.py b/landscape/lib/tests/test_security_rkhunter.py index c1347f878..00f77e2dc 100644 --- a/landscape/lib/tests/test_security_rkhunter.py +++ b/landscape/lib/tests/test_security_rkhunter.py @@ -1,23 +1,23 @@ import os from datetime import datetime -from dateutil import parser +from subprocess import run as run_orig from unittest import TestCase from unittest.mock import patch -from subprocess import run as run_orig + +from dateutil import parser from landscape.lib import testing +from landscape.lib.security import rkhunter_cmd +from landscape.lib.security import RKHunterLiveInfo +from landscape.lib.security import RKHunterLogReader +from landscape.lib.security import RootkitScanInfo # from landscape.lib.security import get_listeningports -from landscape.lib.security import ( - RKHunterLogReader, - RKHunterLiveInfo, - RKHunterInfo, - rkhunter_cmd, -) COMMON_VERSION = "8.4.3" COMMON_DATETIME = parser.parse( - "28 apr 2028 17:44:03 CET", tzinfos=RKHunterLogReader.tzmapping + "28 apr 2028 17:44:03 CET", + tzinfos=RKHunterLogReader.tzmapping, ) SAMPLE_RKHUNTER_VERSION = """Rootkit Hunter 8.4.3 @@ -529,7 +529,7 @@ def test_read_rkhunter_info_log_1(self, mock_datetime): rkinfo = RKHunterLogReader(filename) self.assertEqual( rkinfo.get_last_log().dict(), - RKHunterInfo( + RootkitScanInfo( version=COMMON_VERSION, files_checked=145, files_suspect=0, @@ -550,7 +550,7 @@ def test_read_rkhunter_info_log_2(self, mock_datetime): rkinfo = RKHunterLogReader(filename) self.assertEqual( rkinfo.get_last_log().dict(), - RKHunterInfo( + RootkitScanInfo( version=COMMON_VERSION, files_checked=145, files_suspect=48, @@ -608,7 +608,7 @@ def test_scan_working(self, mock_datetime): rklive = RKHunterLiveInfo() self.assertEqual( rklive.execute().dict(), - RKHunterInfo( + RootkitScanInfo( version=COMMON_VERSION, files_checked=145, files_suspect=1, diff --git a/landscape/message_schemas/server_bound.py b/landscape/message_schemas/server_bound.py index 1e0f6de6e..f65574423 100644 --- a/landscape/message_schemas/server_bound.py +++ b/landscape/message_schemas/server_bound.py @@ -60,7 +60,7 @@ "LIVEPATCH", "UBUNTU_PRO_REBOOT_REQUIRED", "LISTENING_PORTS_INFO", - "RKHUNTER_INFO", + "ROOTKIT_SCAN_INFO", ] @@ -770,8 +770,8 @@ }, ) -RKHUNTER_INFO = Message( - "rkhunter-info", +ROOTKIT_SCAN_INFO = Message( + "rootkit-scan-info", { "report": KeyDict( { @@ -838,5 +838,5 @@ LIVEPATCH, UBUNTU_PRO_REBOOT_REQUIRED, LISTENING_PORTS_INFO, - RKHUNTER_INFO, + ROOTKIT_SCAN_INFO, )