Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Security monitor #139

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ depends2:

.PHONY: depends3
depends3:
sudo apt-get -y install python3-twisted python3-distutils-extra python3-mock python3-configobj python3-netifaces python3-pycurl python3-pip
sudo apt-get -y install python3-twisted python3-distutils-extra python3-mock python3-configobj python3-netifaces python3-pycurl python3-pip python3-dateutil python3-pydantic rkhunter
pip3 install pre-commit
pre-commit install

Expand Down
9 changes: 6 additions & 3 deletions debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ XSBC-Original-Maintainer: Landscape Team <[email protected]>
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,
python3-dateutil, python3-pydantic, rkhunter
Standards-Version: 4.4.0
Homepage: https://github.com/CanonicalLtd/landscape-client

Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions landscape/client/monitor/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"UbuntuProInfo",
"LivePatch",
"UbuntuProRebootRequired",
"ListeningPorts",
"RKHunterInfo",
]


Expand Down
37 changes: 37 additions & 0 deletions landscape/client/monitor/listeningports.py
Original file line number Diff line number Diff line change
@@ -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.dict() 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,
)
38 changes: 38 additions & 0 deletions landscape/client/monitor/rkhunterinfo.py
Original file line number Diff line number Diff line change
@@ -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 = "rootkit-scan-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": "rootkit-scan-info", "report": report.dict()}
logging.info(
"Queueing message with updated rootkit-scan status.",
)
return self.registry.broker.send_message(message, self._session_id)

def run(self, urgent=False):
"""
Send the rootkit-scan-info messages, if the server accepted them.
"""
return self.registry.broker.call_if_accepted(
"rootkit-scan-info",
self.send_message,
)
87 changes: 87 additions & 0 deletions landscape/client/monitor/tests/test_listeningports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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_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{ListeningPortsTest} 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{ListeningPorts} plugin will be scheduled to run every hour.
"""
self.assertEqual(60, self.plugin.run_interval)

def test_run_immediately(self):
"""
The L{ListeningPorts} 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(),
)
dict_sample = sample_listening_ports_dict()
self.assertMessages(
self.mstore.get_pending_messages(),
[{"type": "listening-ports-info", "ports": dict_sample}],
)
self.mstore.delete_all_messages()
self.plugin.send_message()
self.assertMessages(self.mstore.get_pending_messages(), [])
104 changes: 104 additions & 0 deletions landscape/client/monitor/tests/test_rkhunter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from unittest import mock

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 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,
)


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(["rootkit-scan-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 rootkit-scan 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": "rootkit-scan-info", "report": dict_sample}],
)
self.mstore.delete_all_messages()
self.plugin.send_message()
self.assertMessages(self.mstore.get_pending_messages(), [])
Loading