diff --git a/README.md b/README.md index abd9c7bea..eb403b9a2 100644 --- a/README.md +++ b/README.md @@ -390,6 +390,7 @@ The aa-notify tool's Python dependencies can be satisfied by installing the following packages (Debian package names, other distros may vary): * python3-notify2 * python3-psutil +* python3-sqlite (part of the python3.NN-stdlib package) * python3-tk * python3-ttkthemes * python3-gi diff --git a/utils/apparmor/notify.py b/utils/apparmor/notify.py index 08e381716..f4d883ced 100644 --- a/utils/apparmor/notify.py +++ b/utils/apparmor/notify.py @@ -15,12 +15,35 @@ import os import struct +import sqlite3 from apparmor.common import AppArmorBug, DebugLogger debug_logger = DebugLogger('apparmor.notify') +def get_last_login_timestamp(username, filename='/var/log/wtmp', lastlog2_db='/var/lib/lastlog/lastlog2.db'): + """Get last login for user as epoch timestamp""" + + if os.access(lastlog2_db, os.R_OK): + return get_last_login_timestamp_lastlog2(username, lastlog2_db) + else: + return get_last_login_timestamp_wtmp(username, filename) + + +def get_last_login_timestamp_lastlog2(username, lastlog2_db='/var/lib/lastlog/lastlog2.db'): + """Execute lastlog2 and get last login for user as epoch timestamp""" + + db = sqlite3.connect('file:%s?mode=ro' % lastlog2_db, uri=True) + cur = db.cursor() + timestamp = cur.execute('SELECT Time FROM Lastlog2 WHERE Name == ?;', [username]).fetchone() + + if timestamp: + return timestamp[0] + else: + return 0 + + def sane_timestamp(timestamp): """Check if the given timestamp is in a date range that makes sense for a wtmp file""" @@ -32,7 +55,7 @@ def sane_timestamp(timestamp): return True -def get_last_login_timestamp(username, filename='/var/log/wtmp'): +def get_last_login_timestamp_wtmp(username, filename='/var/log/wtmp'): """Directly read wtmp and get last login for user as epoch timestamp""" timestamp = 0 last_login = 0 diff --git a/utils/test/test-notify.py b/utils/test/test-notify.py index 45572157d..4afd35439 100644 --- a/utils/test/test-notify.py +++ b/utils/test/test-notify.py @@ -12,10 +12,46 @@ import unittest from apparmor.common import AppArmorBug -from apparmor.notify import get_last_login_timestamp, sane_timestamp +from apparmor.notify import get_last_login_timestamp, get_last_login_timestamp_wtmp, sane_timestamp from common_test import AATest, setup_all_loops +class TestGet_last_login_timestamp(AATest): + tests = ( + # wtmp file lastlog2 db user expected login timestamp + (('wtmp-x86_64', 'lastlog2.db', 'root'), 1723749426), # Thu Aug 15 19:17:06 UTC 2024 + (('wtmp-x86_64', 'lastlog2.db', 'whoever'), 0), + (('wtmp-x86_64', 'lastlog2.db', 'cb'), 1726995194), # Sun Sep 22 08:53:14 UTC 2024 + (('wtmp-x86_64', 'lastlog2.db', 'sddm'), 1721084423), # Mon Jul 15 23:00:23 UTC 2024 + + (('wtmp-x86_64', 'does-not-exist', 'root'), 1635070346), # Sun Oct 24 12:12:26 CEST 2021 + (('wtmp-x86_64', 'does-not-exist', 'whoever'), 0), + + (('wtmp-s390x', 'lastlog2.db', 'root'), 1723749426), # Thu Aug 15 19:17:06 UTC 2024 + (('wtmp-s390x', 'lastlog2.db', 'whoever'), 0), + (('wtmp-s390x', 'does-not-exist', 'linux1'), 1626368772), # Thu Jul 15 19:06:12 CEST 2021 + (('wtmp-s390x', 'does-not-exist', 'whoever'), 0), + + (('wtmp-aarch64', 'lastlog2.db', 'whoever'), 0), + (('wtmp-aarch64', 'does-not-exist', 'guillaume'), 1611562789), # Mon Jan 25 09:19:49 CET 2021 + (('wtmp-aarch64', 'does-not-exist', 'whoever'), 0), + + (('wtmp-truncated', 'does-not-exist', 'root'), 0), + (('wtmp-truncated', 'does-not-exist', 'whoever'), 0), + ) + + def _run_test(self, params, expected): + wtmpdb, lastlog2_db, user = params + wtmpdb = 'wtmp-examples/' + wtmpdb + lastlog2_db = 'wtmp-examples/' + lastlog2_db + self.assertEqual(get_last_login_timestamp(user, wtmpdb, lastlog2_db), 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', 'wtmp-examples/does-not-exist') + + class TestSane_timestamp(AATest): tests = ( (2524704400, False), # Sun Jan 2 03:46:40 CET 2050 @@ -27,7 +63,7 @@ class TestSane_timestamp(AATest): self.assertEqual(sane_timestamp(params), expected) -class TestGet_last_login_timestamp(AATest): +class TestGet_last_login_timestamp_wtmp(AATest): tests = ( (('wtmp-x86_64', 'root'), 1635070346), # Sun Oct 24 12:12:26 CEST 2021 (('wtmp-x86_64', 'whoever'), 0), @@ -43,12 +79,12 @@ class TestGet_last_login_timestamp(AATest): def _run_test(self, params, expected): filename, user = params filename = 'wtmp-examples/' + filename - self.assertEqual(get_last_login_timestamp(user, filename), expected) + self.assertEqual(get_last_login_timestamp_wtmp(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') + get_last_login_timestamp_wtmp('root', 'wtmp-examples/wtmp-x86_64-past') setup_all_loops(__name__) diff --git a/utils/test/wtmp-examples/lastlog2.db b/utils/test/wtmp-examples/lastlog2.db new file mode 100644 index 000000000..42ecc58ef Binary files /dev/null and b/utils/test/wtmp-examples/lastlog2.db differ