#!/usr/bin/python2.7 -E # # Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved. # # # This program provides information about installed compliance # benchmarks and allows the user to verify against them # and generate result reports and compliance guides. # # It has the following commands: # list [-a] [-b] [-t] [-v] [-p] # list -b [-v] [-p] [benchmark...] # list -a [-v] [assessment...] # guide [-p profile] [-b benchmark] [-o file] # guide -a # assess [-p profile] [-b benchmark] [-a assessment] # assess -t tailoring [-a assessment] # report [-f format] [-s what] [-a assessment] [-o file] # delete assessment # tailor [-t tailoring] [subcommand] # compliance set-policy [-b benchmark [-p profile]] [-t tailoring] # compliance get-policy # help # import errno import fcntl import getopt import gettext import locale import os import os.path as path import re import signal import shutil import solaris.zones as zones import sys import time # import compliance modules from compliance.common import * import compliance.smf_util as smf_util import compliance.tailor as tailor import compliance.xccdf_util as xccdf_util _ = gettext.gettext opt_verbose = 0 def usage(m): f = sys.stderr p = progname sys.stdout.flush() if m: print >> f, m print >> f, _("Usage:") print >> f, _("\t%s list [-a] [-b] [-p] [-t] [-v]") % p print >> f, _("\t%s list -b [-v] [-p] [benchmark ...]") % p print >> f, _("\t%s list -a [-v] [assessment ...]") % p print >> f, _("\t%s guide [-p profile] [-b benchmark] [-o file]") % p print >> f, _("\t%s guide -a") % p print >> f, _("\t%s assess [-b benchmark] [-p profile] [-a assessment]") % p print >> f, _("\t%s assess -t tailoring [-a assessment]") % p fmt = _("\t%s report [-f format] [-s what] [-a assessment] [-o file]") print >> f, fmt % p print >> f, _("\t%s delete assessment") % p print >> f, _("\t%s tailor [-t tailoring] [subcommand]") % p fmt = _("\t%s set-policy [-b benchmark [-p profile]] [-t tailoring]") print >> f, fmt % p print >> f, _("\t%s get-policy") % p exit(m and 1) # # get options for the particular command # def getcmdopt(options): try: opts, pargs = getopt.getopt(sys.argv[2:], options) except getopt.GetoptError as e: usage(e) return (opts, pargs) # # compliance list [-b [-p] [profile]] [-a assessment] # def do_list(): """ process list command """ exit_val = 0 opts, pargs = getcmdopt("abptv") opt_a = False opt_b = False opt_p = False opt_t = False global opt_verbose for opt, arg in opts: if opt == '-a': opt_a = True elif opt == '-b': opt_b = True elif opt == '-p': opt_p = True elif opt == '-t': opt_t = True elif opt == '-v': opt_verbose += 1 if pargs: if not any((opt_b, opt_a)): usage(_("Extra arguments require -b or -a")) if opt_a and any((opt_b, opt_p)): usage(_("Extra arguments used with -a and either -b or -p")) if opt_t: usage(_("Extra arguments can't be used with -t")) # list benchmarks if any((opt_b, opt_p)) or not any((opt_a, opt_t)): benchinfo = [] indent = "" sepr = " " if not any((opt_b, opt_p)) or any((opt_a, opt_t)): print _("Benchmarks:") indent = "\t" sepr = "\n" if opt_p or opt_verbose: indent = "" sepr = "\n" benchmarks = xccdf_util.bench_list() if len(pargs): for arg in pargs: if benchmarks.count(arg) == 0: if path.isabs(arg): benchmarks.append(arg) else: print >> sys.stderr, _("No benchmark '%s' found") % arg exit_val = 1 for benchmark in benchmarks: xccdf = benchmark if not path.isabs(benchmark): xccdf = xccdf_util.bench_path(benchmark) if len(pargs) and pargs.count(benchmark) == 0: continue info = benchmark try: collections = xccdf_util.xccdf_collect(filename=xccdf) except Exception as e: print >> sys.stderr, e continue if opt_p: info += ":\t" proflist = [p.id for p in collections["Profile"] if p.id] if len(proflist): info += strjoin(", ", proflist) else: info += _("") if opt_verbose: b = collections["Benchmark"][0] if b.title: info += "\n\t\t" + b.title if opt_verbose > 1: info += "\n\t" infolist = [r.title for r in collections["Rule"] if r.title] if len(infolist): info += strjoin("\n\t", infolist) benchinfo.append(info) if len(benchmarks): if len(benchinfo): benchinfo.sort() print indent + strjoin(sepr + indent, benchinfo) else: print indent + _("No benchmarks available") # list assessments if opt_a or not any((opt_b, opt_p, opt_t)): indent = "" sepr = "\n" if not opt_a or any((opt_b, opt_p, opt_t)): print _("Assessments:") indent = "\t" if opt_verbose: indent = "" try: assessments = os.listdir(ASSESSMENTS) except: assessments = [] if len(pargs): for arg in pargs: if assessments.count(arg) == 0: print >> sys.stderr, _("No assessment '%s' found") % arg exit_val = 1 assessmentinfo = [] for assessment in assessments: if len(pargs) and pargs.count(assessment) == 0: continue info = assessment if opt_verbose: info += ":\t" try: reports = os.listdir(path.join(ASSESSMENTS, assessment)) except: reports = [] if len(reports): reports.sort() info += strjoin(" ", reports) else: info += _("No reports have been generated") assessmentinfo.append(info) if len(assessments): if len(assessmentinfo): assessmentinfo.sort() print indent + strjoin(sepr + indent, assessmentinfo) else: print indent + _("No assessments available") # list tailorings if opt_t: if any((opt_a, opt_b, opt_p)): print _("Tailorings:") tailor.list_tailorings(opt_verbose) exit(exit_val) # get file mtime def getmtime(fpath, mtime=0): try: statbuf = os.stat(fpath) mtime = statbuf.st_mtime except: pass return mtime # # create a new process # def newproc(): try: child = os.fork() except Exception as e: fatal(3, _("Cannot create child process: %s") % e) return child # # print exec file name and arguments # def printexecargs(f, a): print >> f, "+", a[0], strjoin(" ", a[1:], "'") # # compliance guide [-a] [-p profile] [-b benchmark] [-o file] # def do_guide(): """ process guide command """ opts, pargs = getcmdopt("ab:o:p:qv") opt_a = None opt_b = None opt_o = None opt_p = None global opt_verbose for opt, arg in opts: if (opt == '-a'): opt_a = True if (opt == '-b'): opt_b = arg elif (opt == '-o'): opt_o = arg elif (opt == '-p'): opt_p = arg elif (opt == '-v'): opt_verbose += 1 if not service and len(pargs) > 0: usage(_("Unrecognized parameters specified")) if opt_a and (opt_b or opt_o or opt_p): usage(_("Option -a cannot be used with other options")) guides_locked = [] def lock_guides(): if guides_locked: return lock = path.join(GUIDES, ".lock") try: lockf = open(lock, 'w') fcntl.lockf(lockf.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB, 1, 1, 0) guides_locked.append(True) except Exception as e: # no service error fatal(serviceOK(1), _("Cannot lock guides storage: %s") % str(e)) def guide_path(benchmark, profile): gfile = benchmark if profile: gfile += "." + profile gfile += ".html" return path.join(GUIDES, gfile) def gen_guide(xccdf, profile, gfile): myargv = [OSCAP, "xccdf", "generate"] myargv.append("guide") if profile: myargv.extend(["--profile", profile]) myargv.extend(["--output", gfile, xccdf]) if opt_verbose: printexecargs(sys.stderr, myargv) status = os.spawnv(os.P_WAIT, myargv[0], myargv) return os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0 if opt_a or service: # # process all installed benchmarks and associated profiles # as a service process, silently give up # if we can't create directories or lock guides # if not path.isdir(GUIDES): create_shared_dirs(GUIDES, "guides") lock_guides() benchmarks = xccdf_util.bench_list() for benchmark in benchmarks: xccdf = xccdf_util.bench_path(benchmark) xccdftime = getmtime(xccdf, time.time()) guidefile = guide_path(benchmark, None) guidetime = getmtime(guidefile) if guidetime > xccdftime: if not service: print guidefile elif gen_guide(xccdf, None, guidefile): print guidefile else: continue proflist = xccdf_util.benchmark_profiles(xccdf) for profile in proflist: guidefile = guide_path(benchmark, profile) guidetime = getmtime(guidefile) if guidetime > xccdftime: if not service: print guidefile elif gen_guide(xccdf, profile, guidefile): print guidefile return # # generate a single guide specified by options # if not opt_b: opt_b = "solaris" xccdf = opt_b if not path.isabs(opt_b): xccdf = xccdf_util.bench_path(opt_b) if not path.isfile(xccdf): fatal(1, _("Benchmark tests file not found: %s") % xccdf) if opt_p: proflist = xccdf_util.benchmark_profiles(xccdf) if opt_p not in proflist: fatal(1, _("Benchmark %(b)s has no '%(p)s' profile") % {"b": opt_b, "p": opt_p}) if opt_o: guidefile = opt_o ruid = os.getuid() if os.geteuid() != ruid: # drop privileges before accessing user-specified pathname os.setreuid(ruid, ruid) else: if not path.isdir(GUIDES): create_shared_dirs(GUIDES, "guides") gfile = opt_b if opt_p: gfile += "." + opt_p gfile += ".html" guidefile = path.join(GUIDES, gfile) if not path.isfile(guidefile): lock_guides() if not opt_o and path.isfile(guidefile) or gen_guide(xccdf, opt_p, guidefile): print guidefile exit(0) else: print _("Unable to create guide file: %s") % guidefile exit(1) # # compliance assess [-p profile] [-b benchmark] [-a assessment] # def do_assess(): """ process assess command """ opts, pargs = getcmdopt("a:b:p:qt:v") opt_a = None opt_b = None opt_p = None opt_q = False opt_t = None global opt_verbose for opt, arg in opts: if (opt == '-a'): opt_a = arg elif (opt == '-b'): opt_b = arg elif (opt == '-p'): opt_p = arg elif (opt == '-q'): opt_q = True elif (opt == '-t'): opt_t = arg elif (opt == '-v'): opt_verbose += 1 user = realuser() if not haveauth(AUTH_ASSESS, user): fatal(1, _("User %(u)s lacks authorization %(a)s") % {"u": user, "a": AUTH_ASSESS}) if not any((opt_b, opt_p, opt_t)): (opt_b, opt_p, opt_t) = smf_util.get_policy() msgs = [] if opt_t: if opt_b: opt_t = "%s/%s" % (opt_b, opt_t) opt_b = None if opt_p: usage(_("Option -t cannot be used with -p option")) # compute benchmark and profile from tailoring (opt_b, opt_p, tpath, msgs) = tailor.getassessopts(opt_t) if not opt_b or not opt_p: fatal(1, _("Unable to use tailoring '%(t)s': %(m)s") % {"t": opt_t, "m": msgs}) if not opt_b: opt_b = "solaris" xccdf = xccdf_util.bench_path(opt_b) if not path.isfile(xccdf): fatal(1, _("Benchmark tests file not found: %s") % xccdf) if not opt_p: proflist = xccdf_util.benchmark_profiles(xccdf) if len(proflist): opt_p = proflist[0] if len(pargs) > 0: usage(_("Unrecognized parameters specified")) if not opt_a: timept = time.time() now = time.localtime(timept) if opt_t: opt_a = re.sub("/", "-", opt_t) else: opt_a = opt_b if opt_p: opt_a += "." + opt_p opt_a += time.strftime(".%F,%R", now) if not opt_q: print _("Assessment will be named '%s'") % opt_a if re.search("/", opt_a): fatal(1, _("Invalid assessment name: '%s'") % opt_a) assessmentrep = path.join(ASSESSMENTS, opt_a) if path.isdir(assessmentrep): fatal(1, _("Assessment repository already exists: %s") % assessmentrep) else: create_shared_dirs(COMPLIANCE_VAR, "compliance") # make the assessments and assessment directories mode 0711 create_shared_dirs(assessmentrep, "assessment", umask=0066) try: # set isglobalzone indicator file isgzpath = path.join(COMPLIANCE_VAR, ".isglobalzone") if zones.getzoneid() == 0: isgzfile = open(isgzpath, "w+") isgzfile.close() else: os.remove(isgzpath) except: pass log = path.join(assessmentrep, "log") results = path.join(assessmentrep, RESULTS) report = path.join(assessmentrep, REPORT) evalargv = [OSCAP, "xccdf", "eval"] if opt_t: evalargv.extend(["--tailoring-file", tpath]) if opt_p: evalargv.extend(["--profile", opt_p]) evalargv.extend(["--results", results]) evalargv.extend(["--report", report]) evalargv.append(xccdf) sys.stdout.flush() logf = None try: logf = open(log, 'w') os.fchmod(logf.fileno(), 0644) # lock usage - # byte 0: assessment active # byte 1: directory contents being updated fcntl.lockf(logf.fileno(), fcntl.LOCK_EX, 2, 0, 0) printexecargs(logf, evalargv) for m in msgs: print >> logf, m logf.flush() except: fatal(3, _("Can't establish log: '%s'") % log) for m in msgs: print >> sys.stderr, m sys.stderr.flush() if not opt_q: teeargv = ["/usr/bin/tee", "-ai", log] teeinfd, evaloutfd = os.pipe() evalout = os.fdopen(evaloutfd, "w") teechild = newproc() if teechild == 0: os.dup2(teeinfd, sys.stdin.fileno()) evalout.close() try: os.execv(teeargv[0], teeargv) except Exception, e: fatal(3, _("Failed to execute: %(c)s: %(e)s") % {"c": strjoin(" ", teeargv, "'"), "e": e}) os.close(teeinfd) else: evalout = logf evalchild = newproc() if evalchild == 0: os.dup2(evalout.fileno(), sys.stdout.fileno()) if not opt_q: os.dup2(evalout.fileno(), sys.stderr.fileno()) try: os.execv(evalargv[0], evalargv) except Exception, e: fatal(3, _("Failed to execute: %(c)s: %(e)s") % {"c": strjoin(" ", evalargv, "'"), "e": e}) evalout.close() # wait for teechild, evalchild sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) if not opt_q: _proc, status = os.waitpid(teechild, 0) _proc, status = os.waitpid(evalchild, 0) signal.signal(signal.SIGINT, sigint_handler) if not os.WIFEXITED(status): fatal(3, _("Execution failure: %s") % strjoin(" ", evalargv, "'")) eval_status = os.WEXITSTATUS(status) if not opt_q: print _("Assessment %s completed") % opt_a elif eval_status == 2: # oscap's exit code for successful run with failures eval_status = 0 exit(eval_status) # # compliance report [-f format] [-a assessment] # def do_report(): """ process report command """ opts, pargs = getcmdopt("a:f:o:s:v") opt_a = None opt_f = None opt_o = None opt_s = None global opt_verbose for opt, arg in opts: if (opt == '-a'): opt_a = arg elif (opt == '-f'): opt_f = arg elif (opt == '-o'): opt_o = arg elif (opt == '-s'): opt_s = arg elif (opt == '-v'): opt_verbose += 1 def bad_assessment(_rfile): print (_("The assessment '%s' has a corrupted repository.") % opt_a) print (_("Please delete and retake the assessment.")) return 0 def do_html(rfile): xccdf = path.join(assessmentrep, RESULTS) myargv = [OSCAP, "xccdf", "generate"] myargv.append("report") if opt_s: myargv.extend(["--show", opt_s]) myargv.extend(["--output", rfile, xccdf]) if opt_verbose: printexecargs(sys.stderr, myargv) status = os.spawnv(os.P_WAIT, myargv[0], myargv) return os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0 FORMATS = ( # (option, filename, function) ("html", REPORT, do_html), ("log", "log", bad_assessment), ("xccdf", RESULTS, bad_assessment)) if not opt_f: opt_f = "html" if opt_s and opt_f != "html": usage(_("Use of -s option only permitted with html format")) try: _fname, ffile, ffunc = [f for f in FORMATS if f[0] == opt_f][0] except Exception as _e: fnames = [f[0] for f in FORMATS] usage(_("Unrecognized format request '%(o)s', valid choices: %(c)s") % {"o": opt_f, "c": strjoin(", ", fnames)}) if len(pargs) > 0: usage(_("Unrecognized parameters specified")) if not opt_a: # scan assessments, find newest log newesttime = None try: assessments = os.listdir(ASSESSMENTS) except: assessments = [] for assessment in assessments: try: logpath = path.join(ASSESSMENTS, assessment, "log") logtime = path.getctime(logpath) except: continue if not newesttime or logtime > newesttime: opt_a = assessment newesttime = logtime if not opt_a: fatal(1, _("No assessments available")) if re.search("/", opt_a): fatal(1, _("Invalid assessment name: '%s'") % opt_a) assessmentrep = path.join(ASSESSMENTS, opt_a) if not path.isdir(assessmentrep): fatal(1, _("Assessment repository does not exist: %s") % assessmentrep) if opt_s: exact = len(opt_s) and opt_s[0] == '=' sl = opt_s[exact:].split(",") sl.sort() opt_s = (exact and "=" or "") + reduce(lambda a, b: a + "," + b, sl) ffile = "report." + opt_s + ".html" reportfile = path.join(assessmentrep, ffile) log = path.join(assessmentrep, "log") logf = None try: if opt_o or path.isfile(reportfile): logf = open(log, 'r') fcntl.lockf(logf.fileno(), fcntl.LOCK_SH | fcntl.LOCK_NB, 1, 0, 0) else: logf = open(log, 'r+') fcntl.lockf(logf.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB, 1, 1, 0) statbuf = os.fstat(logf.fileno()) if not statbuf or statbuf.st_size == 0: raise EnvironmentError except: fatal(1, _("Assessment '%s' is not finished") % opt_a) if opt_o: logf.close() ruid = os.getuid() if os.geteuid() != ruid: # drop privileges before accessing user-specified pathname os.setreuid(ruid, ruid) reportfile = opt_o if not opt_o and path.isfile(reportfile) or ffunc(reportfile): print reportfile exit(0) else: print _("Unable to create report file: %s") % reportfile exit(1) # # compliance delete assessment # def do_delete(): """ process delete command """ _opts, pargs = getcmdopt("") if len(pargs) < 1: usage(_("Missing argument: assessment")) if len(pargs) > 1: usage(_("More than one assessment specified")) user = realuser() if not haveauth(AUTH_ASSESS, user): fatal(1, _("User %(u)s lacks authorization %(a)s") % {"u": user, "a": AUTH_ASSESS}) assessment = pargs[0] if re.search("/", assessment) or len(assessment) == 0: fatal(1, _("Invalid assessment name: '%s'") % assessment) assessmentrep = path.join(ASSESSMENTS, assessment) shutil.rmtree(assessmentrep, True) def do_get_policy(): (benchmark, profile, tailoring) = smf_util.get_policy() print _("Benchmark:\t%s") % benchmark print _("Profile:\t%s") % profile print _("Tailoring:\t%s") % tailoring def do_set_policy(): opts, pargs = getcmdopt("b:p:t:") opt_b = "" opt_p = "" opt_t = "" for opt, arg in opts: if (opt == '-b'): opt_b = arg if (opt == '-p'): opt_p = arg if (opt == '-t'): opt_t = arg if pargs: usage(_("Extra arguments for set-policy")) if opt_t: if opt_p: usage(_("Option -t cannot be used with -p")) if opt_b: tailoring = "%s/%s" % (opt_b, opt_t) else: tailoring = opt_t # compute benchmark and profile from tailoring (benchmark, profile, _tpath, msgs) = tailor.getassessopts(tailoring) if not benchmark or not profile: fatal(1, _("Tailoring not found '%(t)s': %(m)s") % {"t": tailoring, "m": msgs}) elif opt_b: xccdf = xccdf_util.bench_path(opt_b) if not path.isfile(xccdf): fatal(1, _("Benchmark tests file not found: %s") % xccdf) if opt_p: proflist = xccdf_util.benchmark_profiles(xccdf) if opt_p not in proflist: fatal(1, _("Benchmark %(b)s has no '%(p)s' profile") % {"b": opt_b, "p": opt_p}) elif opt_p: usage(_("Option -p cannot be used without -b")) if not any((opt_b, opt_p, opt_t)): usage(_("No policy parameters specified")) smf_util.set_policy(opt_b, opt_p, opt_t) # # main program # try: locale.setlocale(locale.LC_ALL, "") except locale.Error: pass gettext.textdomain("compliance") if (len(sys.argv) == 1): usage(_("No command specified")) # # main program dispatch # if (command == "smf-service"): if not os.environ.get("SMF_FMRI"): fatal(1, _("Service functionality invoked outside service context")) if len(sys.argv) < 2: fatal(1, _("Service method not specified")) service = sys.argv[2] service_set(service) # propagate to common namespace if service not in ("start", "refresh"): fatal(1, _("Invalid service method: %s") % service) do_guide() elif (command == "list"): do_list() elif (command == "guide"): do_guide() elif (command == "assess"): do_assess() elif (command == "report"): do_report() elif (command == "delete"): do_delete() elif (command == "tailor"): tailor.do_tailor() elif (command == "get-policy"): do_get_policy() elif (command == "set-policy"): do_set_policy() elif re.search("help", command, re.IGNORECASE): usage(None) else: usage(_("Command '%s' not recognized") % command)