[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