diff --git a/utils/aa-notify b/utils/aa-notify index 6cb7c393f..4bb65abad 100755 --- a/utils/aa-notify +++ b/utils/aa-notify @@ -34,7 +34,6 @@ import os import re import sys import time -import struct import notify2 import psutil import pwd @@ -45,6 +44,7 @@ import apparmor.ui as aaui import apparmor.config as aaconfig from apparmor.common import DebugLogger, open_file_read from apparmor.fail import enable_aa_exception_handler +from apparmor.notify import get_last_login_timestamp from apparmor.translations import init_translation import LibAppArmor # C-library to parse one log line @@ -61,48 +61,6 @@ def get_user_login(): return username -def get_last_login_timestamp(username): - '''Directly read wtmp and get last login for user as epoch timestamp''' - timestamp = 0 - filename = '/var/log/wtmp' - last_login = 0 - - debug_logger.debug('Username: {}'.format(username)) - - with open(filename, "rb") as wtmp_file: - offset = 0 - wtmp_filesize = os.path.getsize(filename) - debug_logger.debug('WTMP filesize: {}'.format(wtmp_filesize)) - while offset < wtmp_filesize: - wtmp_file.seek(offset) - offset += 384 # Increment for next entry - - type = struct.unpack(" +# Copyright (C) 2021 Christian Boltz +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of version 2 of the GNU General Public +# License as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# ---------------------------------------------------------------------- + +import os +import struct + +from apparmor.common import AppArmorBug, DebugLogger + +debug_logger = DebugLogger('apparmor.notify') + + +def sane_timestamp(timestamp): + ''' Check if the given timestamp is in a date range that makes sense for a wtmp file ''' + + if timestamp < 946681200: # 2000-01-01 + return False + elif timestamp > 2524604400: # 2050-01-01 + return False + + return True + +def get_last_login_timestamp(username, filename='/var/log/wtmp'): + '''Directly read wtmp and get last login for user as epoch timestamp''' + timestamp = 0 + last_login = 0 + + debug_logger.debug('Username: {}'.format(username)) + + with open(filename, "rb") as wtmp_file: + offset = 0 + wtmp_filesize = os.path.getsize(filename) + debug_logger.debug('WTMP filesize: {}'.format(wtmp_filesize)) + + if wtmp_filesize < 356: + return 0 # (nearly) empty wtmp file, no entries + + # detect architecture based on utmp format differences + wtmp_file.seek(340) # first possible timestamp position + timestamp_x86_64 = struct.unpack("L", wtmp_file.read(4))[0] + debug_logger.debug('WTMP timestamps: x86_64 %s, aarch64 %s, s390x %s' % (timestamp_x86_64, timestamp_aarch64, timestamp_s390x)) + + if sane_timestamp(timestamp_x86_64): + endianness = '<' # little endian + extra_offset_before = 0 + extra_offset_after = 0 + elif sane_timestamp(timestamp_aarch64): + endianness = '<' # little endian + extra_offset_before = 4 + extra_offset_after = 12 + elif sane_timestamp(timestamp_s390x): + endianness = '>' # big endian + extra_offset_before = 8 + extra_offset_after = 8 + else: + raise AppArmorBug('Your /var/log/wtmp is broken or has an unknown format. Please open a bugreport with /var/log/wtmp and the output of "last" attached!') + + while offset < wtmp_filesize: + wtmp_file.seek(offset) + offset += 384 + extra_offset_before + extra_offset_after # Increment for next entry + + type = struct.unpack('%sH' % endianness, wtmp_file.read(2))[0] + debug_logger.debug('WTMP entry type: {}'.format(type)) + wtmp_file.read(2) # skip padding + + # Only parse USER lines + if type == 7: + # Read each item and move pointer forward + pid = struct.unpack(" +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of version 2 of the GNU General Public +# License published by the Free Software Foundation. +# +# ------------------------------------------------------------------ + +import unittest +from common_test import AATest, setup_all_loops + +from apparmor.common import AppArmorBug +from apparmor.notify import get_last_login_timestamp, sane_timestamp + +class TestSane_timestamp(AATest): + tests = [ + (2524704400, False), # Sun Jan 2 03:46:40 CET 2050 + ( 944780400, False), # Fri Dec 10 00:00:00 CET 1999 + (1635026400, True ), # Sun Oct 24 00:00:00 CEST 2021 + ] + + def _run_test(self, params, expected): + self.assertEqual(sane_timestamp(params), expected) + +class TestGet_last_login_timestamp(AATest): + tests = [ + (['wtmp-x86_64', 'root' ], 1635070346), # Sun Oct 24 12:12:26 CEST 2021 + (['wtmp-x86_64', 'whoever' ], 0), + (['wtmp-s390x', 'root' ], 1626368763), # Thu Jul 15 19:06:03 CEST 2021 + (['wtmp-s390x', 'linux1' ], 1626368772), # Thu Jul 15 19:06:12 CEST 2021 + (['wtmp-s390x', 'whoever' ], 0), + (['wtmp-aarch64', 'guillaume' ], 1611562789), # Mon Jan 25 09:19:49 CET 2021 + (['wtmp-aarch64', 'whoever' ], 0), + (['wtmp-truncated', 'root' ], 0), + (['wtmp-truncated', 'whoever' ], 0), + ] + + def _run_test(self, params, expected): + filename, user = params + filename = 'wtmp-examples/%s' % filename + self.assertEqual(get_last_login_timestamp(user, filename), expected) + + def test_date_1999(self): + with self.assertRaises(AppArmorBug): + # wtmp-x86_64-past is hand-edited to Thu Dec 30 00:00:00 CET 1999, which is outside the expected data range + get_last_login_timestamp('root', 'wtmp-examples/wtmp-x86_64-past') + + +setup_all_loops(__name__) +if __name__ == '__main__': + unittest.main(verbosity=1) diff --git a/utils/test/wtmp-examples/wtmp-aarch64 b/utils/test/wtmp-examples/wtmp-aarch64 new file mode 100644 index 000000000..3703b5fc4 Binary files /dev/null and b/utils/test/wtmp-examples/wtmp-aarch64 differ diff --git a/utils/test/wtmp-examples/wtmp-aarch64-expected-output b/utils/test/wtmp-examples/wtmp-aarch64-expected-output new file mode 100644 index 000000000..d3caeecf2 --- /dev/null +++ b/utils/test/wtmp-examples/wtmp-aarch64-expected-output @@ -0,0 +1,5 @@ +guillaum pts/3 192.168.0.2 Mon Jan 25 08:19 - 09:36 (01:17) + +Example and expected output taken from https://bugzilla.opensuse.org/show_bug.cgi?id=1181155 + +On openSUSE, aarch64 is little endian. diff --git a/utils/test/wtmp-examples/wtmp-s390x b/utils/test/wtmp-examples/wtmp-s390x new file mode 100644 index 000000000..9e218ea59 Binary files /dev/null and b/utils/test/wtmp-examples/wtmp-s390x differ diff --git a/utils/test/wtmp-examples/wtmp-s390x-expected-output b/utils/test/wtmp-examples/wtmp-s390x-expected-output new file mode 100644 index 000000000..de4d5fff6 --- /dev/null +++ b/utils/test/wtmp-examples/wtmp-s390x-expected-output @@ -0,0 +1,13 @@ +linux1@opensuse03:~> last +linux1 pts/0 77.21.253.246 Thu Jul 15 13:06 still logged in +root pts/0 77.21.253.246 Thu Jul 15 13:06 - 13:06 (00:00) +linux1 pts/0 77.21.253.246 Thu Jul 15 13:01 - 13:05 (00:04) +linux1 pts/0 94.134.117.140 Thu Jul 15 08:15 - 08:16 (00:01) +linux1 pts/0 10.6.22.160 Tue Jul 13 07:42 - 07:42 (00:00) +reboot system boot 5.3.18-24.67-def Tue Jul 13 07:41 still running +linux1 pts/0 10.6.22.160 Tue Jul 13 07:41 - 07:41 (00:00) +linux1 pts/0 10.6.22.160 Tue Jul 13 07:37 - 07:41 (00:03) +reboot system boot 5.3.18-24.64-def Tue Jul 13 07:30 - 07:41 (00:11) + +wtmp beginnt Tue Jul 13 07:30:36 2021 +linux1@opensuse03:~> diff --git a/utils/test/wtmp-examples/wtmp-truncated b/utils/test/wtmp-examples/wtmp-truncated new file mode 100644 index 000000000..3b34f439c Binary files /dev/null and b/utils/test/wtmp-examples/wtmp-truncated differ diff --git a/utils/test/wtmp-examples/wtmp-x86_64 b/utils/test/wtmp-examples/wtmp-x86_64 new file mode 100644 index 000000000..7620d49d5 Binary files /dev/null and b/utils/test/wtmp-examples/wtmp-x86_64 differ diff --git a/utils/test/wtmp-examples/wtmp-x86_64-expected b/utils/test/wtmp-examples/wtmp-x86_64-expected new file mode 100644 index 000000000..643b40817 --- /dev/null +++ b/utils/test/wtmp-examples/wtmp-x86_64-expected @@ -0,0 +1,3 @@ +root pts/0 monitor.infra.op Sun Oct 24 12:12 gone - no logout + +wtmp-x86_64 begins Sun Oct 24 12:12:25 2021 diff --git a/utils/test/wtmp-examples/wtmp-x86_64-past b/utils/test/wtmp-examples/wtmp-x86_64-past new file mode 100644 index 000000000..4cdbf6939 Binary files /dev/null and b/utils/test/wtmp-examples/wtmp-x86_64-past differ diff --git a/utils/test/wtmp-examples/wtmp-x86_64-past-expected b/utils/test/wtmp-examples/wtmp-x86_64-past-expected new file mode 100644 index 000000000..1352ccbf7 --- /dev/null +++ b/utils/test/wtmp-examples/wtmp-x86_64-past-expected @@ -0,0 +1,3 @@ +root pts/0 blast.from.the.p Thu Dec 30 00:00 gone - no logout + +wtmp-x86_64-past begins Thu Dec 30 00:00:00 1999