2018-04-10 15:33:32 -04:00
|
|
|
#!/usr/bin/env python
|
|
|
|
|
|
|
|
# Supports CentOS 6.9 and up, untested on lower versions.
|
|
|
|
# Definitely probably won't work on 5.x since they use MD5(?), and 6.5? and up
|
|
|
|
# use SHA256.
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import copy
|
|
|
|
import datetime
|
|
|
|
import hashlib
|
|
|
|
import os
|
2018-04-19 13:41:48 -04:00
|
|
|
import re
|
2018-04-10 15:33:32 -04:00
|
|
|
from sys import version_info as py_ver
|
|
|
|
try:
|
|
|
|
import rpm
|
|
|
|
except ImportError:
|
|
|
|
exit('This script only runs on RHEL/CentOS/other RPM-based distros.')
|
|
|
|
|
|
|
|
# Thanks, dude!
|
|
|
|
# https://blog.fpmurphy.com/2011/08/programmatically-retrieve-rpm-package-details.html
|
|
|
|
|
|
|
|
class PkgChk(object):
|
2018-04-19 13:41:48 -04:00
|
|
|
def __init__(self, dirpath, symlinks = True, pkgs = None):
|
2018-04-10 15:33:32 -04:00
|
|
|
self.path = dirpath
|
|
|
|
self.pkgs = pkgs
|
2018-04-19 13:41:48 -04:00
|
|
|
self.symlinks = symlinks
|
2018-04-10 15:33:32 -04:00
|
|
|
self.orig_pkgs = copy.deepcopy(pkgs)
|
|
|
|
self.pkgfilemap = {}
|
|
|
|
self.flatfiles = []
|
|
|
|
self.flst = {}
|
|
|
|
self.trns = rpm.TransactionSet()
|
|
|
|
self.getFiles()
|
|
|
|
self.getActualFiles()
|
|
|
|
|
|
|
|
def getFiles(self):
|
|
|
|
if not self.pkgs:
|
|
|
|
for p in self.trns.dbMatch():
|
|
|
|
self.pkgs.append(p['name'])
|
|
|
|
for p in self.pkgs:
|
|
|
|
for pkg in self.trns.dbMatch('name', p):
|
|
|
|
# Get the canonical package name
|
|
|
|
_pkgnm = pkg.sprintf('%{NAME}')
|
|
|
|
self.pkgfilemap[_pkgnm] = {}
|
|
|
|
# Get the list of file(s) and their MD5 hash(es)
|
|
|
|
for f in pkg.fiFromHeader():
|
|
|
|
if not f[0].startswith(self.path):
|
|
|
|
continue
|
2018-05-21 12:24:50 -04:00
|
|
|
if f[12] == '0' * 64:
|
2018-04-10 15:33:32 -04:00
|
|
|
_hash = None
|
|
|
|
else:
|
|
|
|
_hash = f[12]
|
|
|
|
self.pkgfilemap[_pkgnm][f[0]] = {'hash': _hash,
|
|
|
|
'date': f[3],
|
|
|
|
'size': f[1]}
|
|
|
|
self.flatfiles.append(f[0])
|
|
|
|
return()
|
|
|
|
|
|
|
|
def getActualFiles(self):
|
|
|
|
print('Getting a list of local files and their hashes.')
|
|
|
|
print('Please wait...\n')
|
|
|
|
for root, dirs, files in os.walk(self.path):
|
|
|
|
for f in files:
|
|
|
|
_fpath = os.path.join(root, f)
|
|
|
|
_stat = os.stat(_fpath)
|
|
|
|
if _fpath in self.flatfiles:
|
|
|
|
_hash = hashlib.sha256()
|
|
|
|
with open(_fpath, 'rb') as r:
|
|
|
|
for chunk in iter(lambda: r.read(4096), b''):
|
|
|
|
_hash.update(chunk)
|
|
|
|
self.flst[_fpath] = {'hash': str(_hash.hexdigest()),
|
|
|
|
'date': int(_stat.st_mtime),
|
|
|
|
'size': _stat.st_size}
|
|
|
|
else:
|
|
|
|
# It's not even in the package, so don't waste time
|
|
|
|
# with generating hashes or anything else.
|
|
|
|
self.flst[_fpath] = {'hash': None}
|
|
|
|
return()
|
|
|
|
|
|
|
|
def compareFiles(self):
|
|
|
|
for f in self.flst.keys():
|
|
|
|
if f not in self.flatfiles:
|
|
|
|
if not self.orig_pkgs:
|
|
|
|
print(('{0} is not installed by any package.').format(f))
|
|
|
|
else:
|
|
|
|
print(('{0} is not installed by package(s) ' +
|
|
|
|
'specified.').format(f))
|
|
|
|
else:
|
|
|
|
for p in self.pkgs:
|
|
|
|
if f not in self.pkgfilemap[p].keys():
|
|
|
|
continue
|
|
|
|
if (f in self.flst.keys() and
|
|
|
|
(self.flst[f]['hash'] !=
|
|
|
|
self.pkgfilemap[p][f]['hash'])):
|
2018-04-19 13:41:48 -04:00
|
|
|
if not self.symlinks:
|
2018-11-17 03:24:38 -05:00
|
|
|
if ((not self.pkgfilemap[p][f]['hash'])
|
|
|
|
or re.search('^0+$',
|
|
|
|
self.pkgfilemap[p][f]['hash'])):
|
2018-04-19 13:41:48 -04:00
|
|
|
continue
|
2018-04-10 15:33:32 -04:00
|
|
|
r_time = datetime.datetime.fromtimestamp(
|
|
|
|
self.pkgfilemap[p][f]['date'])
|
|
|
|
r_hash = self.pkgfilemap[p][f]['hash']
|
|
|
|
r_size = self.pkgfilemap[p][f]['size']
|
|
|
|
l_time = datetime.datetime.fromtimestamp(
|
|
|
|
self.flst[f]['date'])
|
|
|
|
l_hash = self.flst[f]['hash']
|
|
|
|
l_size = self.flst[f]['size']
|
|
|
|
r_str = ('\n{0} differs per {1}:\n' +
|
|
|
|
'\tRPM:\n' +
|
|
|
|
'\t\tSHA256: {2}\n' +
|
|
|
|
'\t\tBYTES: {3}\n' +
|
|
|
|
'\t\tDATE: {4}').format(f, p,
|
|
|
|
r_hash,
|
|
|
|
r_size,
|
|
|
|
r_time)
|
|
|
|
l_str = ('\tLOCAL:\n' +
|
|
|
|
'\t\tSHA256: {0}\n' +
|
|
|
|
'\t\tBYTES: {1}\n' +
|
|
|
|
'\t\tDATE: {2}').format(l_hash,
|
|
|
|
l_size,
|
|
|
|
l_time)
|
|
|
|
print(r_str)
|
|
|
|
print(l_str)
|
|
|
|
# Now we print missing files
|
|
|
|
for f in sorted(list(set(self.flatfiles))):
|
|
|
|
if not os.path.exists(f):
|
|
|
|
print('{0} was deleted from the filesystem.'.format(f))
|
|
|
|
return()
|
|
|
|
|
|
|
|
def parseArgs():
|
|
|
|
def dirchk(path):
|
|
|
|
p = os.path.abspath(path)
|
|
|
|
if not os.path.isdir(p):
|
|
|
|
raise argparse.ArgumentTypeError(('{0} is not a valid ' +
|
|
|
|
'directory').format(path))
|
|
|
|
return(p)
|
|
|
|
args = argparse.ArgumentParser(description = ('Get a list of config ' +
|
|
|
|
'files that have changed ' +
|
|
|
|
'from the package\'s ' +
|
|
|
|
'defaults'))
|
2018-04-19 13:41:48 -04:00
|
|
|
args.add_argument('-l', '--ignore-symlinks',
|
|
|
|
dest = 'symlinks',
|
|
|
|
action = 'store_false',
|
|
|
|
help = ('If specified, don\'t track files that are ' +
|
|
|
|
'symlinks in the RPM'))
|
2018-04-10 15:33:32 -04:00
|
|
|
args.add_argument('-p', '--package',
|
|
|
|
dest = 'pkgs',
|
|
|
|
#nargs = 1,
|
|
|
|
metavar = 'PKGNAME',
|
|
|
|
action = 'append',
|
|
|
|
default = [],
|
|
|
|
help = ('If specified, restrict the list of ' +
|
|
|
|
'packages to check against to only this ' +
|
|
|
|
'package. Can be specified multiple times. ' +
|
|
|
|
'HIGHLY RECOMMENDED'))
|
|
|
|
args.add_argument('dirpath',
|
|
|
|
type = dirchk,
|
|
|
|
metavar = 'path/to/directory',
|
|
|
|
help = ('The path to the directory containing the ' +
|
|
|
|
'configuration files to check against (e.g. ' +
|
|
|
|
'"/etc/ssh")'))
|
|
|
|
return(args)
|
|
|
|
|
|
|
|
def main():
|
|
|
|
args = vars(parseArgs().parse_args())
|
|
|
|
p = PkgChk(**args)
|
|
|
|
p.compareFiles()
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|