#!/usr/local/bin/python2.7
#
#Copyright (c) 2003, 2004, 2005, 2006 Olivier Sessink
#All rights reserved.
#
#Redistribution and use in source and binary forms, with or without
#modification, are permitted provided that the following conditions 
#are met:
#  * Redistributions of source code must retain the above copyright 
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above 
#    copyright notice, this list of conditions and the following 
#    disclaimer in the documentation and/or other materials provided 
#    with the distribution.
#  * The names of its contributors may not be used to endorse or 
#    promote products derived from this software without specific 
#    prior written permission.
#
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
#"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
#LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
#FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
#COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
#INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
#BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
#LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
#CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
#LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
#ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
#POSSIBILITY OF SUCH DAMAGE.
#

import ConfigParser
import sys
import os
import string
from stat import *
import hashlib
import getopt
import random
import stat

INIPREFIX='/etc/jailkit'
LIBDIR='/usr/local/share/jailkit'
EXEPREFIX='/usr/local'
sys.path.append(LIBDIR)
import jk_lib

def testchrootdir(config, path):
	if (config['verbose']):
		print "testing basedir "+path
	jk_lib.chroot_is_safe(path)
	
def md5Digest(filename, startAt):
	"""Returns md5 digest of a 1024 bytes blobk in filename, at position startAt."""
	t = 0
	m = hashlib.md5()
	f=open(filename, 'rb')
	f.seek(startAt)
	buf = f.read(1024)
	f.close()
	m.update(buf)
	return m.digest()

def file_in_list(thelist, file):
	for tmp in thelist:
		if (tmp == file):
			return 1
	return 0

def filebasedir_in_list(thelist, file):
	for ipath in thelist:
#		print 'is '+ipath+'=='+file[:len(ipath)]
		if (file[:len(ipath)] == ipath):
			return 1
	return 0


def testfilepermissions(config,path):
	try:
		statbuf = os.lstat(path)
	except OSError:
		sys.stderr.write('ERROR: cannot lstat() '+path+'\n')
		return 1
	if (config['verbose']):
		print "testing file permissions for "+path
	if (statbuf[ST_UID] == 0 or statbuf[ST_GID] == 0):
#		 we have a root:root file
		if (statbuf[ST_MODE] & S_ISUID or statbuf[ST_MODE] & S_ISGID):
#			it is setuid or setgid
			if (statbuf[ST_MODE] & S_IXOTH):
				if (file_in_list(config['ignoresetuidexecuteforothers'], path)==0):
					print "WARNING: "+path+" is executable for others and setuid root!!"
			if (statbuf[ST_MODE] & S_IXGRP):
				if (file_in_list(config['ignoresetuidexecuteforgroup'], path)==0):
					print "WARNING: "+path+" is executable for group and setuid root!!"
			if (statbuf[ST_MODE] & S_IXUSR):
				if (file_in_list(config['ignoresetuidexecuteforuser'], path)==0):
					print "WARNING: "+path+" is executable for user and setuid root!!"

def testdirpermissions(config,path):
	try:
		statbuf = os.lstat(path)
	except OSError:
		sys.stderr.write('ERROR: cannot lstat() '+path+'\n')
		return 1
	if (config['verbose']):
		print "testing directory permissions for "+path
	if (statbuf[ST_MODE] & S_IWOTH):
		if (filebasedir_in_list(config['ignorewritableforothers'], path) == 0):
			print "WARNING: "+path+" is writable for others!"
	if (statbuf[ST_MODE] & S_IWGRP):
		if (filebasedir_in_list(config['ignorewritableforgroup'], path) == 0):
			print "WARNING: "+path+" is writable for group!"

def comparefiles(config,first, second):
	if (config['verbose']):
		print "comparing  "+first+" and "+second
	try:
		sb1 = os.lstat(first)
		sb2 = os.lstat(second)
		if (sb1[stat.ST_SIZE] != sb2[stat.ST_SIZE]):
			sys.stderr.write('ERROR: '+first+' and '+second+' have a different size!\n')
			return
		if (stat.S_ISLNK(sb1[stat.ST_MODE]) != stat.S_ISLNK(sb1[stat.ST_MODE])):
			print 'ERROR: '+first+' and '+second+' are not both symlinks!'
			return
		if (stat.S_ISLNK(sb1[stat.ST_MODE])):
			if (os.readlink(first) != os.readlink(second)):
				print 'ERROR: symlinks '+first+' and '+second+' point to different files!'
			return
		else:
			if (sb1[stat.ST_SIZE] > 1024):
				startAt = random.randint(0,sb1[stat.ST_SIZE]-1024)
			else:
				startAt = 0
			if (md5Digest(first, startAt) != md5Digest(second, startAt)):
				print 'ERROR: '+first+' and '+second+' are not the same!'
	except IOError:
		print 'ERROR: cannot read '+first+' or '+second+' !'
	except OSError:
		print 'ERROR: cannot stat() '+first+' or '+second+' !'

def testchrootfiles(config, chroothome, path=''):
	try:
		if (not os.path.exists(chroothome+path)):
			print 'cannot check files in '+chroothome+path+', is does not exist'
			return
		testdirpermissions(config,chroothome+path)
		for f in os.listdir(chroothome+path):
			chrootpath = chroothome+path+f
			if (filebasedir_in_list(config['ignorepatheverywhere'],chrootpath) == 0):
				if os.path.isfile(chrootpath):
					if (filebasedir_in_list(config['ignorepathoncompare'],chrootpath) == 0):
						realpath = '/'+path+f
						comparefiles(config,chrootpath, realpath)
					testfilepermissions(config,chrootpath)
				elif os.path.isdir(chrootpath) and not os.path.islink(chrootpath):
					testchrootfiles(config, chroothome, path+f+'/')
				else:
					if (config['verbose']):
						print "ignoring path "+chrootpath
	except OSError:
		print 'ERROR, failed to test any files in '+chroothome+path

def testjailusers(config, chroothome):
	if (os.path.exists(chroothome+'/etc/passwd')):
		jailep = open(chroothome+'/etc/passwd','r')
		realep = open('/etc/passwd','r')
		jline = jailep.readline()
		while (len(jline)>0):
			jpw = string.split(jline,':')
			if (jpw[0] == 'root'):
				if (config['verbose']):
					print 'ignoring entry for user root in jail/etc/passwd'
			else:
				if (config['verbose']):
					print 'comparing jailed user '+jpw[0]+' with the real user'
				# we test for the username, uid and primary gid
				found = 0
				realep.seek(0)
				rline = realep.readline()
				while (len(rline)>0 and found ==0):
					rpw = string.split(rline,':')
					if (rpw[0] == jpw[0]):
						found = 1
						if (rpw[2] != jpw[2] or rpw[3] != jpw[3]):
							print 'ERROR: user '+rpw[0]+' is changed in jail '+chroothome
						# now check if the jail is correct
						if (rpw[5] != chroothome+'.'+jpw[5]):
							print 'ERROR: the real homedir and jail homedir for user '+jpw[0]+' do not correspond in jail '+chroothome
					## test for jk_lsh and test if the user/group is in the config file
					rline = realep.readline()
				if (found == 0):
					print 'ERROR: user '+jpw[0]+' in jail '+chroothome+' does not exist on the real system'
			jline = jailep.readline()

def testrealpasswd():
	"""reads the real /etc/passwd and looks for users that have a jail configured, returns a list of jails"""
	rep = open('/etc/passwd')
	rline = rep.readline()
	retval = []
	while (len(rline)>0):
		rpw = string.split(rline, ':')
		if (len(rpw)>=6):
			# test if the entry has any jail characteristics
			jaildot = string.find(rpw[5], '/./')
			if (jaildot != -1):
				chroot = rpw[5][:jaildot+1]
				retval += [chroot]
				# test if the user actually exists in the chroot
				if (not jk_lib.test_user_exist(rpw[0], chroot+'/etc/passwd')):
					print 'ERROR: user '+rpw[0]+' does not exist in '+chroot+'/etc/passwd'
				chrootsh = os.path.join(EXEPREFIX, 'sbin/jk_chrootsh')
				if (rpw[6].strip() != chrootsh):
					print 'ERROR: user '+rpw[0]+' has a /./ but does not have the '+chrootsh+' shell'
		rline = rep.readline()
	return retval
			
def get_list_option(cfgparser, sectionname, optionname):
	retval = []
	if (cfgparser.has_option(sectionname,optionname)):
		inputstr = cfgparser.get(sectionname,optionname)
		for tmp in string.split(inputstr, ','):
			retval += [string.strip(tmp)]
	return retval

def activateConfig(configfile, verbose):
	cfg = ConfigParser.ConfigParser()
	if (os.path.isfile(configfile)):
		cfg.read([configfile])
	else:
		print 'WARNING: '+configfile+' does not exist, only checking jails found in /etc/passwd'

	jails = testrealpasswd()
	for jail in jails:
		if ((jail not in cfg.sections()) and (jail[:-1] not in cfg.sections())):
			cfg.add_section(jail)

	for section in cfg.sections():
		chrootdir = section
		if (chrootdir[-1:] != '/'): chrootdir += '/'
		config = {}
		config['verbose'] = verbose
		config['ignorepathoncompare'] = get_list_option(cfg,section, 'ignorepathoncompare')
		config['ignoresetuidexecuteforuser'] = get_list_option(cfg,section, 'ignoresetuidexecuteforuser')
		config['ignoresetuidexecuteforgroup'] = get_list_option(cfg,section, 'ignoresetuidexecuteforgroup')
		config['ignoresetuidexecuteforothers'] = get_list_option(cfg,section, 'ignoresetuidexecuteforothers')
		config['ignorewritableforothers'] = get_list_option(cfg,section, 'ignorewritableforothers')
		config['ignorewritableforgroup'] = get_list_option(cfg,section, 'ignorewritableforgroup')
		config['ignorepatheverywhere'] = get_list_option(cfg,section, 'ignorepatheverywhere')
		testchrootdir(config, chrootdir)
		testchrootfiles(config, chrootdir)
		testjailusers(config, chrootdir)

def clean_exit(errno, message):
	print "** FAILURE **"
	print message
	print ""
	sys.exit(errno)

def usage():
	print "Usage: "+sys.argv[0]+" [OPTIONS]"
	print ""
	print "-h --help              : this help screen"
	print "-c, --configfile=FILE  : specify configfile location"
	print "-v, --verbose          : show what is being tested"
	print ""

def main():
	try:
		opts, args = getopt.getopt(sys.argv[1:], "vhc:", ["help", "configfile=", "verbose"])
	except getopt.GetoptError:
		usage()
		sys.exit(1)
	configfile = INIPREFIX+'/jk_check.ini'
	verbose = 0
	for o, a in opts:
		if o in ("-h", "--help"):
			usage()
			sys.exit()
		if o in ("-c", "--configfile"):
			configfile = a
		if o in ("-v", "--verbose"):
			verbose = 1
	activateConfig(configfile, verbose)

if __name__ == "__main__":
    main()
