[redhat-lspp] [RFC] RBACPP self-test program v2 in python
George C. Wilson
ltcgcw at us.ibm.com
Fri Apr 7 17:20:19 UTC 2006
This is my first attempt at a real python program. It is very procedural and
return code -ish rather than object oriented and exception -ish. Instructions
are embedded. I should probably change those to a docstring. It also needs
to do more SELinux verifications. Anyway, for better or worse, here it is.
Once it is acceptable, I'll write a manpage.
#!/usr/bin/python
################################################################################
# #
# Fettle - The RBACPP Self Test #
# #
# Performs various tests on the system to verify RBACPP compliance. #
# #
# Copyright (C) 2006 IBM Corporation #
# Licensed under GNU General Public License #
# #
# This program is free software; you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation; either version 2 of the License, or #
# (at your option) any later version. #
# #
# 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. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program; see the file COPYING. If not, write to #
# the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. #
# #
# Thanks to Paul Nasrat whose lovely terse slides provided an invaluable #
# RPM Python reference: http://www.ukuug.org/events/linux2004/programme/ \ #
# paper-PNasrat-1/rpm-python-slides.pdf #
# #
# Author: George C. Wilson <gcwilson at us.ibm.com> #
# #
# History: #
# 04 Apr 2006 - Initial version #
# #
################################################################################
# #
# USE #
# #
# 1. First take a snapshot of MD5 digests of files RPM has designated #
# as configuration files. #
# #
# rbacpp-self-test --snapshot --verbose #
# #
# There will be some files that are not found. Make sure they #
# look reasonable. #
# #
# 2. Run the the self test in normal mode. You will see some #
# failures. Make sure they look reasonable. #
# #
# rbacpp-self-test --verbose #
# #
# 3. Run the self test in learning mode. This will add all the failures #
# to the excludes files. #
# #
# rbacpp-self-test --learn --verbose #
# #
# 4. Run the command in normal mode again to verify correct operation. #
# #
# rbacpp-self-test --verbose #
# #
# 5. Add the command to your crontab. #
# #
# rbacpp-self-test #
# #
# 6. Check the audit and system logs for failures over the next few #
# days. Make sure they are indeed false positives. If so, add #
# them to the excludes files as well. #
# #
# rbacpp-self-test --learn --verbose #
# #
# 7. Review the excludes files to ensure all excludes look reasonable. #
# #
# 8. To clean up the data files, rm /etc/security/rbac-*.txt. #
# #
################################################################################
import string
import re
import os
import os.path
import sys
import pwd
import syslog
import socket
import md5
import selinux
import audit
import rpm
#
# Usage
#
# Returns: 0 - Success
#
def usage():
print program_name + ': [--snapshot|--learn] [--verbose]'
return(0)
#
# Parse args
#
# Returns: 0 - Success, 1 - Failure
#
def parse_args():
global opt_snapshot
global opt_learn
global opt_verbose
rc = 0
for opt in sys.argv[1:]:
if opt == '--snapshot' or opt == '-s':
opt_snapshot = True
elif opt == '--learn' or opt == '-l':
opt_learn = 1
elif opt == '--verbose' or opt == '-v':
opt_verbose = True
else:
rc = 1
return(rc)
#
# Log an audit message
#
# Returns: 0 - Successfully logged audit message, 1 - Failure
#
def log_error(message):
global opt_verbose
rc = 0
try:
hostname = socket.gethostname()
try:
hostaddr = socket.gethostbyname(hostname)
except:
hostaddr = 'unknown'
except:
hostname = 'unknown'
try:
ttyname = os.readlink('/proc/self/fd/0')
if ttyname.find('/dev') != 0:
ttyname = 'notatty'
except:
ttyname = 'unknown'
message = program_name + ': ' + message
try:
audit.audit_log_user_message(audit_fd,
audit.AUDIT_ANOM_RBAC_FAIL,
message,
hostname,
hostaddr,
ttyname,
0)
except:
print 'Attention: Cannot log audit record'
rc = 1
try:
syslog.openlog('Security')
syslog.syslog(syslog.LOG_AUTH|syslog.LOG_EMERG,
'Attention: Cannot log audit record (message was: ' + message + ')')
syslog.closelog()
except:
print 'Attention: Cannot log syslog record'
if opt_verbose == True:
print message
return(rc)
#
# Initialize audit.
#
# Returns: An audit handle; Non-negative integer - Success, -1 - Failure
#
def open_audit():
try:
fd = audit.audit_open()
except:
log_error('Cannot open audit')
fd = -1
return(fd)
#
# De-initialize audit.
#
# Returns: 0 - Success, 1 - Failure
# fd: Open audit handle to be closed.
#
def close_audit(fd):
rc = 0
try:
audit.audit_close(fd)
except:
log_error('Cannot close audit')
rc = 1
return(rc)
#
# Verify SELinux is enabled, enforcing, and MLS.
#
# Returns: 0 - Success, 1 - Failure
#
def verify_selinux():
rc = 0
if selinux.is_selinux_enabled() != 1:
log_error('SELinux is not enabled')
rc = 1
if rc == 0 and selinux.security_getenforce() != 1:
log_error('SELinux is not in enforcing mode')
rc = 1
if rc == 0 and selinux.is_selinux_mls_enabled() != 1:
log_error('SELinux MLS is not enabled')
rc = 1
return(rc)
#
# MD5 digest a file.
#
# Returns: MD5 digest of a file - Success, empty string - Failure
# filename: String containing name of file to be digested.
#
def md5digest_file(filename):
final_digest = ''
rc = 0
try:
f = open(filename,'r')
except:
log_error('Cannot open file for MD5 digest: ' + filename)
rc = 1
if rc == 0:
try:
data = f.read()
except:
log_error('Cannot read file for MD5 digest: ' + filename)
rc = 1
if rc == 0:
try:
f.close()
except:
log_error('Cannot close file for MD5 digest: ' + filename)
rc = 1
if rc == 0:
md5digest = md5.new()
md5digest.update(data)
final_digest = md5digest.hexdigest()
return(final_digest)
#
# Append list to end of file.
#
# Returns: 0 - Success, 1 - Failure
# filename: Name of file to which to append excludes list.
# excludes: List of items to be appended to file.
#
def append_list_to_file(filename, item_list):
rc = 0
try:
exclude_file = open(filename, 'a')
except:
log_error('Cannot open file for append: ' + filename)
exclude_file = -1
if exclude_file >= 0:
for item in item_list:
item = item.strip()
item = item + '\n'
try:
exclude_file.write(item)
except:
log_error('Cannot write to file: ' + filename)
rc = 1
exclude_file.close()
return(rc)
#
# Initialize file list.
#
# Returns: List of files from a file - Success, empty list - Failure
# filename: String name of file containing list of filenames.
#
def read_list_from_file(filename):
item_list = []
try:
exclude_file = open(filename, 'r')
except:
log_error('Cannot open file for read: ' + filename)
exclude_file = -1
if exclude_file >= 0:
for item in exclude_file.readlines():
item = item.strip()
item_list.append(item)
exclude_file.close()
return(item_list)
#
# Compare an MD5 to the current one of a given file.
#
# Returns: List of single file failing MD5 comparison.
# old_md5: Original MD5.
# filename: Name of file to MD5 and compare against old_md5.
#
def list_md5_failure(old_md5, filename):
filename_list = []
new_md5 = md5digest_file(filename)
if old_md5 != '':
if old_md5 != new_md5:
log_error('MD5 digest does not match RPM database for file: ' + filename)
filename_list.append(filename)
else:
log_error('File does not exist: ' + filename)
filename_list.append(filename)
return(filename_list)
#
# List files in RPM DB.
#
# Returns: List of files in RPM database matching mask, unless md5check
# is true (see below).
#
# mask: RPM DB file type selection mask. Following are the constants
# I've found. They may not all be meaningful in this context.
#
# RPMFILE_STATE_NORMAL
# RPMFILE_STATE_REPLACED
# RPMFILE_STATE_NOTINSTALLED
# RPMFILE_STATE_NETSHARED
# RPMFILE_STATE_WRONGCOLOR
# RPMFILE_CONFIG
# RPMFILE_DOC
# RPMFILE_ICON
# RPMFILE_MISSINGOK
# RPMFILE_NOREPLACE
# RPMFILE_GHOST
# RPMFILE_LICENSE
# RPMFILE_README
# RPMFILE_EXCLUDE
# RPMFILE_UNPATCHED
# RPMFILE_PUBKEY
#
# md5check: Optional Boolean indicating whether to compare file MD5 w/RPM DB
# If true, list returned is files that failed the MD5 test but
# were not excluded.
#
# excludes_filename: Optional file of filenames to exclude from MD5 check.
#
def list_rpm_files(mask, md5check = False, excludes_filename = ''):
filelist = []
transaction_set = rpm.TransactionSet()
match = transaction_set.dbMatch()
if md5check:
excluded_filenames = set(read_list_from_file(excludes_filename))
for header in match:
filenames = header['filenames']
fileflags = header['fileflags']
filemd5s = header['filemd5s']
for i in range(len(filenames)):
if (fileflags[i] & mask):
if md5check == True:
if filenames[i] not in excluded_filenames:
filelist += (list_md5_failure(filemd5s[i], filenames[i]))
else:
filelist.append(filenames[i])
return(filelist)
#
# Take a snapshot of the MD5 digests of configuration files.
#
# Returns: 0 - Success, 1 - Failure
# snapshot_filename: Name of file containing snapshot of MD5 file digest.
# excludes_filename: Name of file containing list of files to be excluded.
#
def take_snapshot(snapshot_filename, excludes_filename):
rc = 0
failures = []
try:
snapshot = open(snapshot_filename, 'w')
except:
log_error('Cannot open snapshot file for writing: ' + snapshot_filename)
snapshot = -1
if snapshot >= 0:
config_filenames = list_rpm_files(rpm.RPMFILE_CONFIG)
if config_filenames != []:
exclude_filenames = read_list_from_file(excludes_filename)
config_filenames = set(config_filenames) - set(exclude_filenames)
for filename in config_filenames:
md5digest = md5digest_file(filename)
if md5digest != '':
try:
snapshot.write(md5digest + ' ' + filename + '\n')
except:
log_error('Cannot write to snapshot file: ' + snapshot_filename)
rc = 1
else:
log_error('Ignoring file for which MD5 cannot be taken: ' + filename)
failures.append(filename)
snapshot.close()
else:
rc = 1
return(rc)
#
# Return list of files whose MD5s do not match those stored in snapshot file.
#
# Returns: List of file with changed MD5s.
# snapshot_filename: Name of file containing snapshot of MD5 file digest.
#
def list_snapshot_failures(snapshot_filename, excludes_filename):
failures = []
try:
snapshot = open(snapshot_filename, 'r');
except:
log_error('Cannot open snapshot file for reading (try --snapshot?): ' + snapshot_filename)
failures = ['dummy']
snapshot = -1
excludes = set(read_list_from_file(excludes_filename))
if snapshot >= 0:
for line in snapshot.readlines():
(old_md5digest, filename) = line.split(' ')
old_md5digest = old_md5digest.strip()
filename = filename.strip()
if filename not in excludes:
md5digest = md5digest_file(filename)
if md5digest != '':
if md5digest != old_md5digest:
log_error('A configuration file has changed since the snapshot was taken: ' + filename)
failures.append(filename)
else:
log_error('A configuration file has disappeared since the snapshot was taken: ' + filename)
failures.append(filename)
snapshot.close()
return(failures)
#
# Verify the MD5s of configuration files.
#
# Returns: 0 - Success, 1 - Failure
# snapshot_filename: Name of file containing snapshot of MD5 file digest.
#
def verify_snapshot(snapshot_filename, excludes_filename):
global opt_learn
failures = list_snapshot_failures(snapshot_filename, excludes_filename)
if opt_learn:
append_list_to_file(excludes_filename, failures)
return(failures != [])
#
# Verify MD5s of non-configuration files against RPM DB.
#
# Returns: False (0) - Success, True (1) - Failure
#
def verify_rpm(excludes_filename):
global opt_learn
mask = ~(rpm.RPMFILE_CONFIG |
rpm.RPMFILE_DOC |
rpm.RPMFILE_MISSINGOK |
rpm.RPMFILE_GHOST |
rpm.RPMFILE_EXCLUDE)
failures = list_rpm_files(mask, True, excludes_filename)
if opt_learn:
append_list_to_file(excludes_filename, failures)
return(failures != [])
#
# Main
#
# Exits: 0 - Success, 1 - Failure
#
SNAPSHOT_FILENAME = '/etc/security/rbac-sums.txt'
SNAPSHOT_EXCLUDES_FILENAME = '/etc/security/rbac-snap-excludes.txt'
RPMMD5_EXCLUDES_FILENAME = '/etc/security/rbac-rpm-excludes.txt'
rc = 0
audit_fd = -1
opt_snapshot = False
opt_verbose = False
opt_learn = False
program_name = os.path.basename(sys.argv[0])
if rc == 0:
rc = parse_args()
if rc != 0:
usage()
if rc == 0:
audit_fd = open_audit()
if audit_fd < 0:
rc = 1
if rc == 0 and opt_snapshot == True:
rc = take_snapshot(SNAPSHOT_FILENAME, SNAPSHOT_EXCLUDES_FILENAME)
else:
if rc == 0:
rc = verify_selinux()
if rc == 0:
rc = verify_snapshot(SNAPSHOT_FILENAME, SNAPSHOT_EXCLUDES_FILENAME)
if rc == 0:
rc = verify_rpm(RPMMD5_EXCLUDES_FILENAME)
if rc == 0 and opt_verbose == True:
print 'The self test found none of the deficiencies for which it tests.'
if audit_fd >= 0:
close_audit(audit_fd)
sys.exit(rc)
--
George Wilson <ltcgcw at us.ibm.com>
IBM Linux Technology Center
More information about the redhat-lspp
mailing list