2
0
mirror of https://gitlab.com/apparmor/apparmor synced 2025-09-02 07:15:18 +00:00

Add support for lastlog2 to get last login

lastlog2 is the 2038-safe replacement for wtmp, and in the meantime
became part of util-linux.

This commit switches from trying to parse the lastlog2 output to
directly reading lastlog2.db with sqlite3.

Adjust get_last_login_timestamp() to use the lastlog2 database
(/var/lib/lastlog/lastlog2.db) if it exists, and adjust
get_last_login_timestamp_lastlog2() to actually do that.

(If lastlog2.db doesn't exist, aa-notify will read wtmp as usual.)

Unfortunately lastlog2 doesn't have a way to get machine-readable output
(for example json), therefore - after trying and failing to parse the
lastlog2 output - directly read from lastlog2.db. Let's hope the format
never changes ;-)

Fixes: https://bugzilla.opensuse.org/show_bug.cgi?id=1228378

Fixes: https://bugzilla.opensuse.org/show_bug.cgi?id=1216660

Fixes: https://gitlab.com/apparmor/apparmor/-/issues/372
This commit is contained in:
Christian Boltz
2024-09-22 20:27:54 +02:00
parent 371a9ff9ec
commit 45e4c27cf0
5 changed files with 39 additions and 84 deletions

View File

@@ -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

View File

@@ -14,54 +14,35 @@
# ----------------------------------------------------------------------
import os
import re
import struct
import sqlite3
from datetime import datetime
from apparmor.common import AppArmorBug, AppArmorException, DebugLogger, cmd
from apparmor.common import AppArmorBug, DebugLogger
debug_logger = DebugLogger('apparmor.notify')
def get_last_login_timestamp(username, filename='/var/log/wtmp', lastlog2='/usr/bin/lastlog2'):
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, os.X_OK):
return get_last_login_timestamp_lastlog2(username, lastlog2)
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='/usr/bin/lastlog2'):
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"""
retval, out = cmd([lastlog2, '-u', username])
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 retval == 1 and ' does not exist.' in out:
raise AppArmorException(out)
if retval != 0:
raise AppArmorBug('Executing lastlog2 failed:\n' + out)
if '**Never logged in**' in out:
if timestamp:
return timestamp[0]
else:
return 0
lines = out.rstrip().split('\n')
if len(lines) != 2:
raise AppArmorBug('Unexpected lastlog2 output:\n' + out)
# in lastlog2 output, timestamp starts at column 70, with at least one space in column 69
# if this ever changes, we'll need a different/better regex...
RE_LASTLOG2 = re.compile('^.{68} +' + '(?P<timestamp>.+)')
match = RE_LASTLOG2.search(lines[1])
timestamp = match.group('timestamp')
unixtime = int(datetime.strptime(timestamp, '%a %b %d %H:%M:%S %z %Y').strftime('%s'))
return unixtime
def sane_timestamp(timestamp):
"""Check if the given timestamp is in a date range that makes sense for a wtmp file"""

View File

@@ -1,33 +0,0 @@
#!/bin/bash
test "$1" = "-u" || {
echo 'invalid parameter' >&2
exit 42
}
case "$2" in
"root")
echo 'Username Port From Latest'
echo 'root tty4 Tue Jul 16 00:53:40 +0200 2024'
;;
"cb")
echo 'Username Port From Latest'
echo 'cb tty2 :0 Mon Jul 1 10:35:46 +0200 2024'
;;
"net")
echo 'Username Port From Latest'
echo 'net ssh 192.168.0.103 Sun Jul 28 14:26:17 +0200 2024'
;;
"whoever")
echo 'Username Port From Latest'
echo 'whoever **Never logged in**'
;;
"notexist")
echo "lastlog2: User 'notexist' does not exist." >&2
exit 1
;;
*)
echo 'invalid parameter' >&2
exit 42
;;
esac

View File

@@ -11,39 +11,45 @@
import unittest
from apparmor.common import AppArmorBug, AppArmorException
from apparmor.common import AppArmorBug
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-x86_64', './fake_lastlog2', 'root'), 1721084020), # Tue Jul 16 00:53:40 CEST 2024
(('wtmp-x86_64', './fake_lastlog2', 'whoever'), 0),
(('wtmp-x86_64', './fake_lastlog2', 'cb'), 1719822946), # Mon Jul 1 10:35:46 CEST 2024
(('wtmp-x86_64', './fake_lastlog2', 'net'), 1722169577), # Sun Jul 28 14:26:17 CEST 2024
(('wtmp-s390x', './fake_lastlog2', 'root'), 1721084020), # Tue Jul 16 00:53:40 CEST 2024
(('wtmp-s390x', './does-not-exist', 'linux1'), 1626368772), # Thu Jul 15 19:06:12 CEST 2021
(('wtmp-s390x', './fake_lastlog2', 'whoever'), 0),
(('wtmp-aarch64', './does-not-exist', 'guillaume'), 1611562789), # Mon Jan 25 09:19:49 CET 2021
(('wtmp-aarch64', './fake_lastlog2', 'whoever'), 0),
(('wtmp-truncated', './fake_lastlog2', 'root'), 1721084020), # Tue Jul 16 00:53:40 CEST 2024
(('wtmp-truncated', './fake_lastlog2', 'whoever'), 0),
# 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):
filename, fake_lastlog, user = params
filename = 'wtmp-examples/' + filename
self.assertEqual(get_last_login_timestamp(user, filename, fake_lastlog), 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', './does-not-exist')
def test_unknown_user(self):
with self.assertRaises(AppArmorException):
get_last_login_timestamp('notexist', 'wtmp-examples/wtmp-x86_64', './fake_lastlog2')
get_last_login_timestamp('root', 'wtmp-examples/wtmp-x86_64-past', 'wtmp-examples/does-not-exist')
class TestSane_timestamp(AATest):

Binary file not shown.