Revision $Id: ftimes-process-bimvw.base,v 1.10 2007/10/07 04:30:10 klm Exp $ Purpose This recipe demonstrates how to generate profile-specific HTML reports based on the output produced by ftimes_bimvw. First, individual reports are generated for each profile. Then, the individual reports are summarized and linked together to form an index report, which shows the current status of all clients. Motivation Getting email alerts from ftimes_bimvw is good, and in some cases, it is sufficient. However, there may be times when you want to view the change status of all your clients at a glance. At other times, you may want to browse through the current set of change reports. Generally, these things are easier to do through a web interface. Requirements The server must be running UNIX and have basic system utilities, Apache, and WebJob (1.7.0 or higher) installed. The commands presented throughout this recipe were designed to be executed within a Bourne shell (i.e., sh or bash). This recipe assumes that you have read and implemented the following recipes: http://webjob.sourceforge.net/Files/Recipes/ftimes-bimvw.txt http://webjob.sourceforge.net/Files/Recipes/webjob-triggers-compress-upload.txt Time to Implement Assuming that you have satisfied all the requirements/prerequisites, this recipe should take less than one hour to implement. Solution The following steps describe how to implement this recipe. 1. Extract ftimes_bimvw_wrapper and ftimes_bimvw_cmp2web from this recipe (Appendices 1 and 2), and install them in a suitable bin directory on your WebJob server. Once the files are in place, set their ownership and permissions to 0:0 and 755, respectively. # BIN_DIR=/usr/local/bin # sed -e '1,/^--- ftimes_bimvw_wrapper ---$/d; /^--- ftimes_bimvw_wrapper ---$/,$d' ftimes-process-bimvw.txt > ftimes_bimvw_wrapper # cp ftimes_bimvw_wrapper ${BIN_DIR} # chown 0:0 ${BIN_DIR}/ftimes_bimvw_wrapper # chmod 755 ${BIN_DIR}/ftimes_bimvw_wrapper # sed -e '1,/^--- ftimes_bimvw_cmp2web ---$/d; /^--- ftimes_bimvw_cmp2web ---$/,$d' ftimes-process-bimvw.txt > ftimes_bimvw_cmp2web # cp ftimes_bimvw_cmp2web ${BIN_DIR} # chown 0:0 ${BIN_DIR}/ftimes_bimvw_cmp2web # chmod 755 ${BIN_DIR}/ftimes_bimvw_cmp2web The purpose of ftimes_bimvw_wrapper is to control the execution of ftimes_bimvw, ftimes_bimvw_cmp2web, and webjob-compress-upload. This script has the following usage: ftimes_bimvw_wrapper [-b] [-c {bzip2|compress|gzip|none}] [-d db] [-e address[,address]] [-g gpg-keyring-path] [-H ftimes-home] [-i global-ignore-file] [-j job] [-k key=value[,key=value[,...]]] [-m compare-mask] [-T time-period] [-w web-dir] [-x ext[,ext[,...]]] -r rdy-file where '-b' forces the script to compare the current snapshot to the first (or oldest) recorded baseline (typically baseline.1st); '-c' enables bzip2, compress, gzip, or no compression (gzip is the default); '-d' is path to a WebJob MLDBM database (no database is used by default); '-e' is a comma delimited list of recipients that should receive a copy of all email alerts (alerts are not sent by default); '-g' enables GPG encryption of email alerts and specifies the location where the GPG key rings are located; '-H' is a path that points to the FTimes home directory (/usr/local/ftimes is the default); '-i' specifies the name of a global ignore file that contains egrep-style regular expressions used to filter out noise or false positives (each profile uses its own set of ignore rules by default -- this option can be used to supplement those local rules); '-j' is the job name used in the MLDBM database; '-k' is a comma delimited list of key/value pairs to set in the MLDBM database; '-m' is the compare mask that you want to use when performing change analysis ("none+md5" is the default); '-r' is the full path of the client's .rdy file; '-T' is the expected snapshot time period in seconds (3600 is the default); '-w' is the full path to the directory where you would like the HTML reports to reside (the default is /usr/local/www/data/snapshots); and '-x' is a comma delimited list of extra file extensions to compress. The purpose of ftimes_bimvw_cmp2web is to produce HTML reports and summary pages based on the output produced by ftimes_bimvw. This script has the following usage: ftimes_bimvw_cmp2web [-i global-ignore-file] [-m compare-mask] [-T time-period] [-w web-dir] -r rdy-file where '-i' specifies the name of a global ignore file that contains egrep-style regular expressions used to filter out noise or false positives (each profile uses its own set of ignore rules by default -- this option can be used to supplement those local rules); '-m' is the compare mask that was used when ftimes_bimvw performed change analysis ("none+md5" is the default); '-r' is the full path of the client's .rdy file; '-T' is the expected snapshot time period in seconds (3600 is the default); and '-w' is the full path to the directory where you would like the HTML reports to reside (the default is /usr/local/www/data/snapshots). 2. Modify the config file overrides for the "all" and "sys" profiles. Start by setting the following variables in your shell. # WEBJOB_BASE_DIR="/var/webjob" # WEB_DIR="/usr/local/www/data/snapshots" # ALL_HTML_REPORTS="${WEB_DIR}/ftimes_all" # SYS_HTML_REPORTS="${WEB_DIR}/ftimes_sys" Create one directory for each profile in your web directory. This is where the web pages will reside. Make sure these directories are writable by the web user. # mkdir -p ${ALL_HTML_REPORTS} ${SYS_HTML_REPORTS} # chown apache:apache ${ALL_HTML_REPORTS} ${SYS_HTML_REPORTS} # chmod 0755 ${ALL_HTML_REPORTS} ${SYS_HTML_REPORTS} The following instructions assume that you have implemented the ftimes_bimvw according to the posted recipe. Edit the "all" profile's config file override and replace the existing PutTriggerCommandLine. The old entry (according to the recipe) should be: PutTriggerCommandLine=ftimes_bimvw -c gzip -r %rdy Therefore, the new entry would become: PutTriggerCommandLine=ftimes_bimvw_wrapper -c gzip -w ${ALL_HTML_REPORTS} -r %rdy -x .cmp,.cmp.filtered The following command will perform the necessary transformation: # perl -p -i -e "s,ftimes_bimvw,ftimes_bimvw_wrapper,; s#%rdy#%rdy -w ${ALL_HTML_REPORTS} -x .cmp,.cmp.filtered#;" ${WEBJOB_BASE_DIR}/config/nph-webjob/commands/c_hlc_ftimes_all/nph-webjob.cfg Edit the "sys" profile's config file override and replace the existing PutTriggerCommandLine. The old entry (according to the recipe) should be: PutTriggerCommandLine=ftimes_bimvw -c gzip -e "root@localhost" -r %rdy Therefore, the new entry would become: PutTriggerCommandLine=ftimes_bimvw_wrapper -c gzip -e "root@localhost" -w ${SYS_HTML_REPORTS} -r %rdy -x .cmp,.cmp.filtered The following command will perform the necessary transformation: # perl -p -i -e "s,ftimes_bimvw,ftimes_bimvw_wrapper,; s#%rdy#%rdy -w ${SYS_HTML_REPORTS} -x .cmp,.cmp.filtered#;" ${WEBJOB_BASE_DIR}/config/nph-webjob/commands/c_hlc_ftimes_sys/nph-webjob.cfg Note: You will need to manually expand the ${ALL_HTML_REPORTS} and ${SYS_HTML_REPORTS} variables in the nph-webjob.cfg config files. The Perl transformation commands do this step for you, but only if you define those variables in your environment. Note: Any options specified in the original ftimes_bimvw command need to be carried through to the ftimes_bimvw_wrapper command. If this is not done, the web pages may not be accurate. For example, if you used a global ignore.rules file in ftimes_bimvw, you need to pass that same option to ftimes_bimvw_wrapper. Closing Remarks This recipe is a work in progress. Currently, the HTML reports only show the most recent violations. In the future, it would be better if the user could view a history of change reports. This would make it possible to detect trends. Credits This recipe was brought to you by Klayton Monroe and Jason Smith. References FTimes is available here: http://ftimes.sourceforge.net/FTimes/ WebJob is available here: http://webjob.sourceforge.net/WebJob/ Appendix 1 The following command may be used to extract this Appendix: $ sed -e '1,/^--- ftimes_bimvw_wrapper ---$/d; /^--- ftimes_bimvw_wrapper ---$/,$d' ftimes-process-bimvw.txt > ftimes_bimvw_wrapper --- ftimes_bimvw_wrapper --- #!/bin/sh ###################################################################### # # $Id: ftimes_bimvw_wrapper,v 1.6 2007/10/07 04:10:48 klm Exp $ # ###################################################################### # # Copyright 2006-2006 The FTimes Project, All Rights Reserved. # ###################################################################### # # Purpose: Wrapper for Basic Integrity Monitoring Via WebJob (BIMVW) # ###################################################################### IFS=' ' PATH=/sbin:/usr/sbin:/usr/local/sbin:/bin:/usr/bin:/usr/local/bin PROGRAM=`basename $0` ###################################################################### # # Usage # ###################################################################### Usage() { echo 1>&2 echo "Usage: ${PROGRAM} [-b] [-c {bzip2|compress|gzip|none}] [-d db] [-e address[,address]] [-g gpg-keyring-path] [-H ftimes-home] [-i global-ignore-file] [-j job] [-k key=value[,key=value[,...]]] [-m compare-mask] [-T time-period] [-w web-dir] [-x ext[,ext[,...]]] -r rdy-file" 1>&2 echo 1>&2 exit 1 } ###################################################################### # # Main # ###################################################################### ALERT_RECIPIENTS= COMPRESSION_METHOD=gzip COMPARE_MASK=none+md5 EXTRA_EXTENSIONS= GLOBAL_IGNORE_FILE= GPG_KEY_DIR= RDY_FILE= TIME_PERIOD=3600 USE_1ST_BASELINE=0 WEB_DIR=/usr/local/www/data/ftimes WEBJOB_DB= while getopts "bc:d:e:g:H:i:j:k:m:r:T:w:x:" OPTION ; do case "${OPTION}" in b) USE_1ST_BASELINE=1 ;; c) COMPRESSION_METHOD="${OPTARG}" ;; d) WEBJOB_DB="${OPTARG}" ;; e) ALERT_RECIPIENTS="${OPTARG}" ;; g) GPG_KEY_DIR="${OPTARG}" ;; H) FTIMES_HOME="${OPTARG}" ;; i) GLOBAL_IGNORE_FILE="${OPTARG}" ;; j) JOB="${OPTARG}" ;; k) KVPS="${OPTARG}" ;; m) COMPARE_MASK="${OPTARG}" ;; T) TIME_PERIOD="${OPTARG}" ;; r) RDY_FILE="${OPTARG}" ;; w) WEB_DIR="${OPTARG}" ;; x) EXTRA_EXTENSIONS="${OPTARG}" ;; *) Usage ;; esac done if [ ${OPTIND} -le $# ] ; then Usage fi PATH=${FTIMES_HOME=/usr/local/ftimes}/bin:${PATH} export PATH if [ -z "${RDY_FILE}" ] ; then Usage else if [ ! -f "${RDY_FILE}" ] ; then echo "${PROGRAM}: Error='The specified file (${RDY_FILE}) does not exist or is not regular.'" 1>&2 exit 2 fi fi BIMVW_OPTIONS="-c none" BIMVW_CMP2WEB_OPTIONS="" WEBJOB_COMPRESS_UPLOAD_OPTIONS="" if [ ${USE_1ST_BASELINE} -eq 1 ] ; then BIMVW_OPTIONS="${BIMVW_OPTIONS} -b" fi if [ -n "${ALERT_RECIPIENTS}" ] ; then BIMVW_OPTIONS="${BIMVW_OPTIONS} -e ${ALERT_RECIPIENTS}" fi if [ -n "${COMPARE_MASK}" ] ; then BIMVW_OPTIONS="${BIMVW_OPTIONS} -m ${COMPARE_MASK}" BIMVW_CMP2WEB_OPTIONS="${BIMVW_CMP2WEB_OPTIONS} -m ${COMPARE_MASK}" fi if [ -n "${COMPRESSION_METHOD}" ] ; then WEBJOB_COMPRESS_UPLOAD_OPTIONS="${WEBJOB_COMPRESS_UPLOAD_OPTIONS} -m ${COMPRESSION_METHOD}" fi if [ -n "${EXTRA_EXTENSIONS}" ] ; then WEBJOB_COMPRESS_UPLOAD_OPTIONS="${WEBJOB_COMPRESS_UPLOAD_OPTIONS} -x ${EXTRA_EXTENSIONS}" fi if [ -n "${FTIMES_HOME}" ] ; then BIMVW_OPTIONS="${BIMVW_OPTIONS} -H ${FTIMES_HOME}" fi if [ -n "${GLOBAL_IGNORE_FILE}" ] ; then BIMVW_OPTIONS="${BIMVW_OPTIONS} -i ${GLOBAL_IGNORE_FILE}" BIMVW_CMP2WEB_OPTIONS="${BIMVW_CMP2WEB_OPTIONS} -i ${GLOBAL_IGNORE_FILE}" fi if [ -n "${GPG_KEY_DIR}" ] ; then BIMVW_OPTIONS="${BIMVW_OPTIONS} -g ${GPG_KEY_DIR}" fi if [ -n "${JOB}" ] ; then BIMVW_CMP2WEB_OPTIONS="${BIMVW_CMP2WEB_OPTIONS} -j ${JOB}" fi if [ -n "${KVPS}" ] ; then BIMVW_CMP2WEB_OPTIONS="${BIMVW_CMP2WEB_OPTIONS} -k ${KVPS}" fi if [ -n "${TIME_PERIOD}" ] ; then BIMVW_CMP2WEB_OPTIONS="${BIMVW_CMP2WEB_OPTIONS} -T ${TIME_PERIOD}" fi if [ -n "${WEB_DIR}" ] ; then BIMVW_CMP2WEB_OPTIONS="${BIMVW_CMP2WEB_OPTIONS} -w ${WEB_DIR}" fi if [ -n "${WEBJOB_DB}" ] ; then BIMVW_CMP2WEB_OPTIONS="${BIMVW_CMP2WEB_OPTIONS} -d ${WEBJOB_DB}" fi eval ftimes_bimvw ${BIMVW_OPTIONS} -r ${RDY_FILE} if [ $? -ne 0 ] ; then echo "${PROGRAM}: Error='ftimes_bimvw did not complete successfully.'" 1>&2 fi eval ftimes_bimvw_cmp2web ${BIMVW_CMP2WEB_OPTIONS} -r ${RDY_FILE} if [ $? -ne 0 ] ; then echo "${PROGRAM}: Error='ftimes_bimvw_cmp2web did not complete successfully.'" 1>&2 fi eval webjob-compress-upload ${WEBJOB_COMPRESS_UPLOAD_OPTIONS} -r ${RDY_FILE} if [ $? -ne 0 ] ; then echo "${PROGRAM}: Error='webjob-compress-upload did not complete successfully.'" 1>&2 fi --- ftimes_bimvw_wrapper --- Appendix 2 The following command may be used to extract this Appendix: $ sed -e '1,/^--- ftimes_bimvw_cmp2web ---$/d; /^--- ftimes_bimvw_cmp2web ---$/,$d' ftimes-process-bimvw.txt > ftimes_bimvw_cmp2web --- ftimes_bimvw_cmp2web --- #!/usr/bin/perl -w ###################################################################### # # $Id: ftimes_bimvw_cmp2web,v 1.32 2007/02/09 06:58:41 klm Exp $ # ###################################################################### # # Copyright 2006-2006 The FTimes Project, All Rights Reserved. # ###################################################################### # # Purpose: Generate HTML reports based on ftimes-bimvw output. # ###################################################################### use strict; use Fcntl qw(:flock); use File::Basename; use File::Copy; use File::Path; use Getopt::Std; use Time::Local; BEGIN { #################################################################### # # The Properties hash is essentially private. Those parts of the # program that wish to access or modify the data in this hash need # to call GetProperties() to obtain a reference. # #################################################################### my (%hProperties); sub GetProperties { return \%hProperties; } } ###################################################################### # # Main Routine # ###################################################################### my ($phProperties, $sProgram); $phProperties = GetProperties(); $sProgram = $$phProperties{'Program'} = basename(__FILE__); #################################################################### # # Get Options. # #################################################################### my (%hOptions); if (!getopts('d:i:j:k:m:r:T:w:', \%hOptions)) { Usage($$phProperties{'Program'}); } #################################################################### # # DbFile, '-d', is optional. # #################################################################### $$phProperties{'DbFile'} = (exists($hOptions{'d'})) ? $hOptions{'d'} : undef; #################################################################### # # Job, '-j', is optional. # #################################################################### $$phProperties{'Job'} = (exists($hOptions{'j'})) ? $hOptions{'j'} : undef; #################################################################### # # Kvps, '-k', is optional. # #################################################################### my ($sKvps); $sKvps = (exists($hOptions{'k'})) ? $hOptions{'k'} : undef; if (defined($sKvps)) { @{$$phProperties{'Kvps'}} = split(/,/, $sKvps); } #################################################################### # # A web directory, '-w' is optional. # #################################################################### $$phProperties{'WebDir'} = (exists($hOptions{'w'})) ? $hOptions{'w'} : "/usr/local/www/data/snapshots"; #################################################################### # # An ignore file, '-i' is optional. # #################################################################### $$phProperties{'GlobalIgnoreFile'} = (exists($hOptions{'i'})) ? $hOptions{'i'} : undef; #################################################################### # # A compare mask, '-m', is optional. # #################################################################### $$phProperties{'CompareMask'} = (exists($hOptions{'m'})) ? $hOptions{'m'} : "none+md5"; #################################################################### # # A .rdy file, '-r', is required. # #################################################################### $$phProperties{'RdyFile'} = (exists($hOptions{'r'})) ? $hOptions{'r'} : undef; if (!defined($$phProperties{'RdyFile'})) { Usage($$phProperties{'Program'}); } #################################################################### # # A time period, '-T', is optional. # #################################################################### $$phProperties{'TimePeriod'} = (exists($hOptions{'T'})) ? $hOptions{'T'} : 3600; #################################################################### # # If there's any arguments left, it's an error. # #################################################################### if (scalar(@ARGV) > 0) { Usage($$phProperties{'Program'}); } #################################################################### # # Get the current timestamp, so we can reference it later. # #################################################################### my ( $sSecond, $sMinute, $sHour, $sMonthDay, $sMonth, $sYear, $sWeekDay, $sYearDay, $sDaylightSavings, ) = localtime(); $$phProperties{'RunDateTime'} = sprintf("%04s-%02s-%02s %02s:%02s:%02s", $sYear + 1900, $sMonth + 1, $sMonthDay, $sHour, $sMinute, $sSecond); #################################################################### # # Create the output directory if it does not exist. # #################################################################### if (!-d $$phProperties{'WebDir'}) { if (!mkpath($$phProperties{'WebDir'}, 0, 0755)) { print STDERR "$sProgram: Error='Could not create ", $$phProperties{'WebDir'}, " ($!)'\n"; } } #################################################################### # # Get the ClientId and Hostname. # #################################################################### my $sDateDir = dirname($$phProperties{'RdyFile'}); my $sProfileDir = dirname($sDateDir); ($$phProperties{'EnvFile'} = $$phProperties{'RdyFile'}) =~ s/rdy$/env/; if (!open(FH, "< $$phProperties{'EnvFile'}")) { print STDERR "$sProgram: Error='Could not open ", $$phProperties{'EnvFile'}, " ($!)'\n"; } else { while(my $sLine = ) { chomp($sLine); if ($sLine =~ /^Hostname=(.*)/) { $$phProperties{'HostName'} = $1; } elsif ($sLine =~ /^ClientId=(.*)/) { $$phProperties{'ClientId'} = $1; } elsif ($sLine =~ /^Command=(.*)/) { $$phProperties{'Command'} = $1; } elsif ($sLine =~ /^RunEpoch=(.+)\((.*)\)$/) { $$phProperties{'RunEpoch'} = $2; } elsif ($sLine =~ /^PutEpoch=(.+)\((.*)\)$/) { $$phProperties{'PutEpoch'} = $2; } } close(FH); } #################################################################### # # Make sure the input file exists. If it doesn't exist, we're done. # #################################################################### my $sCompareFile = $sProfileDir . "/" . "compared.cmp"; if (!-e $sCompareFile) { exit(0); } #################################################################### # # Create an array to hold the ignore rules. # #################################################################### my (@aIgnoreRules); $$phProperties{'LocalIgnoreFile'} = $sProfileDir . "/" . "ignore.rules"; foreach my $sIgnoreFile ($$phProperties{'LocalIgnoreFile'}, $$phProperties{'GlobalIgnoreFile'}) { if (defined($sIgnoreFile)) { if (!open(HFilters, "< $sIgnoreFile")) { print STDERR "$sProgram: Error='Could not open $sIgnoreFile ($!)'\n"; } else { while (my $sLine = ) { chomp($sLine); push(@aIgnoreRules, $sLine); } close(HFilters); } } } #################################################################### # # Create an array to hold the violations. # #################################################################### my (@aViolations); my $sTotalViolations = 0; if (!open(HViolations, "< $sCompareFile")) { print STDERR "$sProgram: Error='Could not open $sCompareFile ($!)'\n"; } else { while (my $sLine = ) { chomp($sLine); push(@aViolations, $sLine); $sTotalViolations++; } close(HViolations); #FIXME This is no longer required because ftimes_bimvw removes the # header from the compare output. # $sTotalViolations--; } #################################################################### # # Process the violations and create tables for each category. # #################################################################### my %hCategoryHtml; my $sFiltered; my %hCategories = ( 'C' => "Changed", 'M' => "Missing", 'N' => "New", 'X' => "Cross", 'U' => "Unknown", ); foreach my $sCategory (sort(keys(%hCategories))) { my $sTableHtml; my $sCategoryCount = 0; my $sFilterCount = 0; foreach my $sViolation (sort(@aViolations)) { next if ($sViolation =~ /^category[|]name[|]changed[|]unknown/); next unless ($sViolation =~ /^$sCategory[|]/); $sFiltered = 0; foreach my $sIgnoreRule (@aIgnoreRules) { if ($sViolation =~ /$sIgnoreRule/) { $sFiltered = 1; $sFilterCount++; last; # Terminate the loop on the first match. } } $sCategoryCount++; $hCategoryHtml{$sCategory}{'HTML'} .= BuildHtmlBody($$phProperties{'HostName'}, $sViolation, $sCategoryCount, $sFiltered); } $hCategoryHtml{$sCategory}{'count'} = ($sCategoryCount - $sFilterCount) . ":" . $sCategoryCount; } #################################################################### # # Generate the HTML report. # #################################################################### my $sHtmlReport; my $sHtmlOutFile = $$phProperties{'WebDir'} . "/" . $$phProperties{'ClientId'} . ".html"; my $sTableHtml = ""; $sHtmlReport = "\n"; $sHtmlReport .= "\n"; $sHtmlReport .= "

FTimes Change Report for " . $$phProperties{'HostName'} . "

\n"; $sHtmlReport .= "

CompareMask of " . $$phProperties{'CompareMask'} . "

\n"; $sHtmlReport .= "

"; foreach my $sCategory (sort(keys(%hCategories))) { my ($sViolationsCount, $sTotalCount) = split(/:/, $hCategoryHtml{$sCategory}{'count'}); if ($sTotalCount > 0) { $sHtmlReport .= "" . $hCategories{$sCategory} . "(" . $hCategoryHtml{$sCategory}{'count'} . ")  "; } else { $sHtmlReport .= $hCategories{$sCategory} . "(" . $hCategoryHtml{$sCategory}{'count'} . ")  "; } if ($sTotalCount >= 1) { $sTableHtml .= "

" .$hCategories{$sCategory} . "(" . $hCategoryHtml{$sCategory}{'count'} . ")

\n"; $sTableHtml .= BuildTable(); $sTableHtml .= $hCategoryHtml{$sCategory}{'HTML'}; $sTableHtml .= "\n"; } } $sHtmlReport .= "Total(" . $sTotalViolations . ")

\n"; $sHtmlReport .= $sTableHtml; $sHtmlReport .= "
\n"; $sHtmlReport .= "
Page last updated: " . $$phProperties{'RunDateTime'} . "
\n"; $sHtmlReport .= "\n"; $sHtmlReport .= "\n"; if (!open(HHtml, "> $sHtmlOutFile")) { print STDERR "$sProgram: Warning='Could not open $sHtmlOutFile for writing ($!)'\n"; } else { if ($sTotalViolations > 0) { print HHtml $sHtmlReport; } else { print HHtml "\n"; print HHtml "\n"; print HHtml "
\n"; print HHtml "

There are currently no violations for " . $$phProperties{'HostName'} . "

\n"; print HHtml "
\n"; print HHtml "Page last updated: " . $$phProperties{'RunDateTime'} . "\n"; print HHtml "
\n"; print HHtml "\n"; print HHtml "\n"; } close(HHtml); } #################################################################### # # Generate the HTML index. # #################################################################### my $sHtmlIndex; my %hClientTotals; my $sCounter = 1; my $sAlignCenter = " align=\"center\""; my $sAlignRight = " align=\"right\""; my $sBgcolor = " bgcolor=\"#cccccc\""; my $sLocalError; $sHtmlIndex = "\n"; $sHtmlIndex .= "\n"; $sHtmlIndex .= "\n"; $sHtmlIndex .= "
\n"; $sHtmlIndex .= "

FTimes Change Reports

\n"; $sHtmlIndex .= "

" . $$phProperties{'Command'} . "

\n"; $sHtmlIndex .= "\n"; $sHtmlIndex .= "\n"; $sHtmlIndex .= "  \n"; $sHtmlIndex .= " ClientID\n"; $sHtmlIndex .= " Total\n"; $sHtmlIndex .= " Changed\n"; $sHtmlIndex .= " Missing\n"; $sHtmlIndex .= " New\n"; $sHtmlIndex .= " Cross\n"; $sHtmlIndex .= " Unknown\n"; $sHtmlIndex .= " Run Time\n"; $sHtmlIndex .= " Last Update\n"; $sHtmlIndex .= "\n"; my $sIndexFile = $$phProperties{'WebDir'} . "/index.html"; my $sLckFile = $$phProperties{'WebDir'} . "/index.html.lck"; my $sTmpFile = $$phProperties{'WebDir'} . "/index.html.tmp"; if (!open(LH, "> $sLckFile")) { print STDERR "$sProgram: Error='File $sLckFile could not be opened ($!)'\n"; exit(2); } flock(LH, LOCK_EX); if (!defined(GetClientTotals($$phProperties{'WebDir'}, \%hClientTotals, \$sLocalError))) { print STDERR "$sProgram: Warning='$sLocalError'\n"; } foreach my $sCategory (sort(keys(%hCategories))) { $hClientTotals{$$phProperties{'ClientId'}}{$sCategory} = $hCategoryHtml{$sCategory}{'count'}; } $hClientTotals{$$phProperties{'ClientId'}}{'Total'} = $sTotalViolations; $hClientTotals{$$phProperties{'ClientId'}}{'LastUpdate'} = $$phProperties{'RunDateTime'}; $hClientTotals{$$phProperties{'ClientId'}}{'RunTime'} = sprintf("%.2f", ($$phProperties{'PutEpoch'} - $$phProperties{'RunEpoch'})); GetRunSeconds($$phProperties{'ClientId'}, \%hClientTotals); if (!defined(SetClientTotals($$phProperties{'WebDir'}, \%hClientTotals, \$sLocalError))) { print STDERR "$sProgram: Warning='$sLocalError'\n"; } if (defined($$phProperties{'DbFile'}) && !defined(SetClientDbTotals($phProperties, \%hClientTotals, \$sLocalError))) { print STDERR "$sProgram: Warning='$sLocalError'\n"; } foreach my $sClient (sort(keys(%hClientTotals))) { my $sFile = $sClient . ".html"; $sHtmlIndex .= "\n"; $sHtmlIndex .= " " . $sCounter . "\n"; $sHtmlIndex .= " \n"; $sHtmlIndex .= " " . $hClientTotals{$sClient}{'Total'} . "\n"; foreach my $sSubCategory (sort(keys(%hCategories))) { my ($sViolationsCount, $sTotalCount) = split(/:/, $hClientTotals{$sClient}{$sSubCategory}); $sViolationsCount = 0 if (!defined($sViolationsCount) || $sViolationsCount !~ /^\d+$/); $sTotalCount = 0 if (!defined($sTotalCount) || $sTotalCount !~ /^\d+$/); if ($sTotalCount > 0) { my $sColor = ($sViolationsCount > 0) ? "#ff6060" : "#00ff00"; $sHtmlIndex .= " "; $sHtmlIndex .= $hClientTotals{$sClient}{$sSubCategory} . "\n"; } else { $sHtmlIndex .= "  \n"; } } $sHtmlIndex .= " " . $hClientTotals{$sClient}{'RunTime'} . "(s)\n"; GetRunSeconds($sClient, \%hClientTotals); my $sMultiplier = 1.5; my $sLimit = 600; my $sDelta = ($hClientTotals{$$phProperties{'ClientId'}}{'RunSeconds'} - $hClientTotals{$sClient}{'RunSeconds'}); my $sDeltaColor = (($sDelta > $sMultiplier * $$phProperties{'TimePeriod'}) || ($sDelta > $$phProperties{'TimePeriod'} + $sLimit)) ? "#ffff00" : ""; $sHtmlIndex .= " " . $hClientTotals{$sClient}{'LastUpdate'} . "\n"; $sHtmlIndex .= "\n"; $sCounter++; } $sHtmlIndex .= "
" . $sClient . "
\n"; $sHtmlIndex .= "
\n"; BuildDescription(\$sHtmlIndex); $sHtmlIndex .= "Page last updated: " . $$phProperties{'RunDateTime'} . "\n"; $sHtmlIndex .= "
\n"; $sHtmlIndex .= "\n"; $sHtmlIndex .= "\n"; if (!open(TH, "> $sTmpFile")) { print STDERR "$sProgram: Error='File $sTmpFile could not be opened ($!)'\n"; exit(2); } binmode(TH); print TH $sHtmlIndex; close(TH); if (!move("$sTmpFile", "$sIndexFile")) { print STDERR "$sProgram: Error='Could not move $sTmpFile to $sIndexFile ($!)'\n"; } flock(LH, LOCK_UN); close(LH); unlink($sLckFile); 1; ###################################################################### # # BuildDescription # ###################################################################### sub BuildDescription { my ($psIndexHtml) = @_; $$psIndexHtml .= "\n"; $$psIndexHtml .= "\n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= "\n"; $$psIndexHtml .= "\n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= "\n"; $$psIndexHtml .= "\n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= "\n"; $$psIndexHtml .= "\n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= "\n"; $$psIndexHtml .= "\n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= "\n"; $$psIndexHtml .= "\n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= " \n"; $$psIndexHtml .= "\n"; $$psIndexHtml .= "
 CategoryDescription
CChangedFiles that have one or more modified attributes. Note: Only those attributes specified in the CompareMask are checked.
MMissingFiles that have been removed or excluded since the last snapshot was taken.
NNewFiles that have been added or included since the last snapshot was taken.
XCrossFiles that have one or more attributes in both the Changed and Unknown categories.
UUnknownFiles that have one or more attributes that could not be compared due to a lack of data.
\n"; $$psIndexHtml .= "
\n"; } ###################################################################### # # BuildHtmlBody # ###################################################################### sub BuildHtmlBody { my ($sHostname, $sCompareRecord, $sItemNumber, $sFiltered) = @_; my $sHtml; my ($sCategory, $sName, $sChanged, $sUnknown) = split(/\|/, $sCompareRecord); my $sColor = (!$sFiltered) ? "#ff6060" : "#00ff00"; $sHtml = "\n"; $sHtml .= " " . $sItemNumber . "\n"; $sHtml .= " " . $sCategory . "\n"; $sHtml .= " " . $sName . "\n"; if (length($sChanged) == 0) { $sHtml .= "  "; } else { $sChanged =~ s/,/, /g; $sHtml .= " " . $sChanged . "\n"; } if (length($sUnknown) == 0) { $sHtml .= "  \n"; } else { $sUnknown =~ s/,/, /g; $sHtml .= " " . $sUnknown . "\n"; } $sHtml .= "\n"; return $sHtml; } ###################################################################### # # BuildTable # ###################################################################### sub BuildTable { my ($sHtml); my $sAlignCenter = " align=\"center\""; my $sBgcolor = " bgcolor=\"#cccccc\""; $sHtml = "\n"; $sHtml .= "\n"; $sHtml .= "  \n"; $sHtml .= " Category\n"; $sHtml .= " File Name\n"; $sHtml .= " Changed\n"; $sHtml .= " Unknown\n"; $sHtml .= "\n"; return $sHtml; } ###################################################################### # # GetClientTotals # ###################################################################### sub GetClientTotals { my ($sWebDir, $phClientTotals, $psError) = @_; my $sClientFile = $sWebDir . "/clients.lst"; my %hRegexes = # Derived from nph-webjob.cgi. ( 'ClientId' => qq((?:[A-Za-z](?:(?:[0-9A-Za-z]|[_-](?=[^.]))){0,62})(?:[.][A-Za-z](?:(?:[0-9A-Za-z]|[_-](?=[^.]))){0,62}){0,127}), 'Decimal64Bit' => qq(\\d{1,20}), # 18446744073709551615 'PartOverWhole' => qq(\\d{1,20}:\\d{1,20}), 'Duration' => qq(\\d{1,10}(?:.\\d{1,2})?), 'DateTime' => qq(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}), ); if (!open(HH, "< $sClientFile")) { $$psError = "Could not open $sClientFile for reading ($!)"; return undef; } else { while (my $sLine = ) { chomp($sLine); ################################################################ # # Split the line and validate the input. Reject the whole line # if any input value does not pass muster. This will result in # a hole, but that's better than propagating potentially bogus # data. # ################################################################ my ($sClient, $sClientT, $sClientC, $sClientM, $sClientN, $sClientX, $sClientU, $sClientRunTime, $sClientUpdate) = split(/\|/, $sLine, -1); next if (!defined($sClient) || $sClient !~ /^$hRegexes{'ClientId'}$/); next if (!defined($sClientT) || $sClientT !~ /^$hRegexes{'Decimal64Bit'}$/); next if (!defined($sClientC) || $sClientC !~ /^$hRegexes{'PartOverWhole'}$/); next if (!defined($sClientM) || $sClientM !~ /^$hRegexes{'PartOverWhole'}$/); next if (!defined($sClientN) || $sClientN !~ /^$hRegexes{'PartOverWhole'}$/); next if (!defined($sClientX) || $sClientX !~ /^$hRegexes{'PartOverWhole'}$/); next if (!defined($sClientU) || $sClientU !~ /^$hRegexes{'PartOverWhole'}$/); next if (!defined($sClientRunTime) || $sClientRunTime !~ /^$hRegexes{'Duration'}$/); next if (!defined($sClientUpdate) || $sClientUpdate !~ /^$hRegexes{'DateTime'}$/); ################################################################ # # Save the values in a hash for later use. # ################################################################ $$phClientTotals{$sClient}{'Total'} = $sClientT; $$phClientTotals{$sClient}{'C'} = $sClientC; $$phClientTotals{$sClient}{'M'} = $sClientM; $$phClientTotals{$sClient}{'N'} = $sClientN; $$phClientTotals{$sClient}{'X'} = $sClientX; $$phClientTotals{$sClient}{'U'} = $sClientU; $$phClientTotals{$sClient}{'RunTime'} = $sClientRunTime; $$phClientTotals{$sClient}{'LastUpdate'} = $sClientUpdate; } close(HH); } return(1); } ###################################################################### # # GetRunSeconds # ###################################################################### sub GetRunSeconds { my ($sClient, $phClientTotals) = @_; my ($sDateYear, $sTime) = split(/ /, $$phClientTotals{$sClient}{'LastUpdate'}); my ($sYear, $sMonth, $sDay) = split(/-/, $sDateYear); my ($sHour, $sMinute, $sSecond) = split(/:/, $sTime); my $sUnixSeconds = timelocal($sSecond, $sMinute, $sHour, $sDay, $sMonth - 1, $sYear); $$phClientTotals{$sClient}{'RunSeconds'} = $sUnixSeconds; return(1); } ###################################################################### # # SetClientDbTotals # ###################################################################### sub SetClientDbTotals { my ($phProperties, $phClientTotals, $psError) = @_; if ( !defined($$phProperties{'DbFile'}) || !defined($$phProperties{'ClientId'}) || !defined($$phProperties{'Job'}) ) { $$psError = "Unable to proceed due to missing or undefined inputs."; return undef; } my $sClientT = $$phClientTotals{$$phProperties{'ClientId'}}{'Total'} || ""; my $sClientC = $$phClientTotals{$$phProperties{'ClientId'}}{'C'} || ""; my $sClientM = $$phClientTotals{$$phProperties{'ClientId'}}{'M'} || ""; my $sClientN = $$phClientTotals{$$phProperties{'ClientId'}}{'N'} || ""; my $sClientX = $$phClientTotals{$$phProperties{'ClientId'}}{'X'} || ""; my $sClientU = $$phClientTotals{$$phProperties{'ClientId'}}{'U'} || ""; my $sClientRunTime = $$phClientTotals{$$phProperties{'ClientId'}}{'RunTime'} || ""; my $sClientUpdate = $$phClientTotals{$$phProperties{'ClientId'}}{'LastUpdate'} || ""; push(@{$$phProperties{'Kvps'}}, "\"Total=$sClientT\""); push(@{$$phProperties{'Kvps'}}, "\"C=$sClientC\""); push(@{$$phProperties{'Kvps'}}, "\"M=$sClientM\""); push(@{$$phProperties{'Kvps'}}, "\"N=$sClientN\""); push(@{$$phProperties{'Kvps'}}, "\"X=$sClientX\""); push(@{$$phProperties{'Kvps'}}, "\"U=$sClientU\""); push(@{$$phProperties{'Kvps'}}, "\"RunTime=$sClientRunTime\""); push(@{$$phProperties{'Kvps'}}, "\"LastUpdate=$sClientUpdate\""); my @aArguments = ( "/usr/local/webjob/bin/webjob-mldbm-set-job-kvps", "-d", $$phProperties{'DbFile'}, "-c", $$phProperties{'ClientId'}, "-j", $$phProperties{'Job'}, @{$$phProperties{'Kvps'}} ); my $sCommand = join(" ", @aArguments); qx($sCommand); #print $sCommand, "\n"; return(1); } ###################################################################### # # SetClientTotals # ###################################################################### sub SetClientTotals { my ($sWebDir, $phClientTotals, $psError) = @_; my $sClientFile = $sWebDir . "/clients.lst"; if (!open(HH, "> $sClientFile")) { $$psError = "Could not open $sClientFile for writing ($!)"; return undef; } else { foreach my $sClientName (keys(%$phClientTotals)) { my $sClientLine = $sClientName . "|"; $sClientLine .= $$phClientTotals{$sClientName}{'Total'} . "|"; $sClientLine .= $$phClientTotals{$sClientName}{'C'} . "|"; $sClientLine .= $$phClientTotals{$sClientName}{'M'} . "|"; $sClientLine .= $$phClientTotals{$sClientName}{'N'} . "|"; $sClientLine .= $$phClientTotals{$sClientName}{'X'} . "|"; $sClientLine .= $$phClientTotals{$sClientName}{'U'} . "|"; $sClientLine .= $$phClientTotals{$sClientName}{'RunTime'} . "|"; $sClientLine .= $$phClientTotals{$sClientName}{'LastUpdate'}; print HH $sClientLine . "\n"; } close(HH); } return(1); } ###################################################################### # # Usage # ###################################################################### sub Usage { my ($sProgram) = @_; print STDERR "\n"; print STDERR "Usage: $sProgram [-d db] [-i global-ignore-file] [-j job] [-k key=value[,key=value[,...]]] [-m compare-mask] [-T time-period] [-w web-dir] -r rdy-file\n"; print STDERR "\n"; exit(1); } --- ftimes_bimvw_cmp2web ---