meitar / chkrelease

Utility script to cryptographically verify the contents of a filesystem against the content of a tarball.

This URL has Read+Write access

chkrelease / chkrelease.sh
100755 272 lines (234 sloc) 8.867 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
#!/bin/bash -
# File: chkrelease.sh
#
# Description: Utility script to cryptographically verify the contents
# of a filesystem against the content of a tarball. Useful
# in quickly auditing a filesystem of a production server
# against a release from a source code management system.
#
# Examples: A quick way to overwrite all files that this script has
# discovered were modified is to extract the output of the
# command and feed these paths to `tar`, like so:
#
# ./chkrelease.sh /tmp/release-tarball.tar / | \
# awk '{ print $1 }' | \
# tar -C / -xvf /tmp/release-tarball.tar --files-from=-
#
# The command expansion simply extracts the first column of output
# produced by the script if one exists, which is the filesystem
# paths present in the release tarball. It sends these paths to tar
# via standard input to extract them at the same directory that we're
# checking against which in this case is the root directory (/).
#
# Another way to use this utility is to generate a difference listing
# from the filesystem's contents and the release archive. To do this
# simply redirect STDOUT to a file.
#
# ./chkrelease.sh /tmp/release-tarball.tar / > /tmp/deltas &
#
# This will run as a background job. At any time, send the process ID
# of the job a SIGHUP to see a progress report. When it's done, you can
# then mend the filesystem deltas by feeding the deltas file to tar.
#
# awk '{print $1}' /tmp/deltas | \
# tar -C / -xvf /tmp/release-tarball.tar --files-from=-
 
# DEBUGGING
set -e
 
# TRAP SIGNALS
trap 'showTotals 1>&2' HUP # use HUP instead of INFO since INFO is not POSIX-compliant
trap 'showTotals; cleanTmpDirAndExit' INT # do showTotals when interupted by CTRL-C
trap 'cleanTmpDir' QUIT EXIT # do cleanTmpDir immediately before exiting
 
# FIND BINARIES
MD5UTIL=`which md5sum || which md5` # use GNU's md5sum if exists, md5 otherwise
TARUTIL=`which tar`
 
# INTERNAL VARIABLES
readonly PROGRAM=`basename "$0"`
readonly VERSION="0.1.3"
TMPDIR=${TMPDIR:-/tmp}
CMPDIR='.' # the directory on the filesystem to compare the tarball against
SHOWTOTALS=0 # whether or not to print the totals at the end
DONTCLEAN=0 # whether or not to run cleanTmpDir
SHOWPROGRESS=0 # whether or not to showTotals during operation
 
# GATHER PARAMETERS
# RETURN VALUES/EXIT STATUS CODES
# a return value between 1 and 125 inclusive indicates that a delta exists on the filesystem
# and doubles as a report of how many files have been modified
# (up to the limit defined by POSIX of 125, of course), which implies 0 means no delta
# while higher-numbered values indicate some abnormal process completion (up to the max of 255)
readonly E_BAD_OPTION=252
readonly E_MISSING_PARAM=253
readonly E_BAD_TARBALL=254
readonly E_UNKNOWN=255
 
# UTILITY FUNCTIONS
function cleanTmpDir () {
    if [ $DONTCLEAN -eq 1 -o ! -f "$TMPDIR/$PROGRAM.out.$$" ]; then
return # We won't clean up if told not to or don't know what to clean.
    fi
 
echo "Please wait while I clean the \$TMPDIR..." 1>&2
 
    # remove directories recursively
    grep '/' "$TMPDIR/$PROGRAM.out.$$" |
      sed -e 's/^\.\///' |
        cut -d '/' -f 1 |
          uniq |
    while read dir_to_rm; do
rm -rf "$TMPDIR/$dir_to_rm"
    done
 
    # remove any top-level files
    grep -v '/' "$TMPDIR/$PROGRAM.out.$$" |
    while read file_to_rm; do
rm -f "$TMPDIR/$file_to_rm"
    done
 
    # remove the temporary output file
    rm -f "$TMPDIR/$PROGRAM.out.$$"
}
 
function cleanTmpDirAndExit () {
    cleanTmpDir
    exit ${1:-$num_modified}
}
 
function getHashFromUtil () {
    case `basename $MD5UTIL` in
        'md5sum' )
            getHashFromUtil_md5sum $@
            ;;
        'md5' )
            getHashFromUtil_md5 $@
            ;;
    esac
}
 
function getHashFromUtil_md5 () {
    # we use the fourth position parameter ($4) because that's the default
    # space-separated parameter where `md5` returns a hash for us
    echo ${4:(-32)} # take the last 32 characters, since md5 hashes are always 32 bytes
}
 
function getHashFromUtil_md5sum () {
    echo `echo "$1" | cut -d ' ' -f 1`
}
 
function showTotals () {
    echo
echo "Total number of files to audit: $num_total_files"
    echo "Total number of files audited: $num_audited"
    echo "Total number of files modified: $num_modified"
    echo "Total number of files skipped: $num_skipped"
}
 
function usage () {
    echo "Usage is as follows:"
    echo "$PROGRAM <--version|-v>"
    echo
echo " Prints the program version number on a line by itself and exits."
    echo
echo "$PROGRAM <--help|--usage|-?>"
    echo
echo " Prints this usage output and exits."
    echo
echo "$PROGRAM [--count|-c] [--messy|-m] [--progress|-p] <release_tarball> [root_of_directory_to_audit]"
    echo
echo " <release_tarball> is the tar file to compare [root_of_directory_to_audit] against."
    echo
echo " [root_of_directory_to_audit] defaults to '.' (current directory) if not specified."
    echo
echo " If '--count' or '-c' is specified, a summary will be printed when it is done."
    echo
echo " If '--messy' or '-m' is specified, $PROGRAM will not remove files from the"
    echo " temporary directory (\$TMPDIR) that it creates when it is done running."
    echo " Useful for examining files after $PROGRAM has run."
    echo
echo " If '--progress' or '-p' is specified, $PROGRAM will display a progress report"
    echo " on STDERR during operation. Useful if you are bored and want something to watch."
    echo " Alternatively, while $PROGRAM is running in the background, send it a SIGHUP to"
    echo " produce the same effect. If set, '--count' is automatically assumed, as well."
}
 
function usageAndExit () {
    usage
    exit ${1:-255}
}
 
function version () {
    echo "$PROGRAM version $VERSION"
}
 
function versionAndExit () {
    version
    exit ${1:-255}
}
 
# Process command-line arguments.
while test $# -gt 0; do
case $1 in
        --count | -c )
            shift
SHOWTOTALS=1
            ;;
 
        --messy | -m )
            shift
DONTCLEAN=1
            ;;
 
        --progress | -p )
            shift
SHOWPROGRESS=1
            SHOWTOTALS=1 # if asked for progress, report at end, too
            ;;
 
        --version | -v )
            versionAndExit 0
            ;;
 
        -? | --help | --usage )
            usageAndExit 0
            ;;
 
        -* )
            echo "Unrecognized option: $1" 1>&2
            usageAndExit $E_BAD_OPTION
            ;;
 
        * )
            break;
            ;;
    esac
done
 
# Ensure we have a tarball to work with
if [[ $1 == '' ]]; then
echo "$PROGRAM: missing parameter" 1>&2
    usageAndExit $E_MISSING_PARAM
elif [ ! -f $1 -o ! -r $1 ]; then
echo "$PROGRAM: $1 is not a readable file" 1>&2
    usageAndExit $E_BAD_TARBALL
else
TARBALL="$1"
fi
 
# Get our comparison directory
if [[ $2 != '' ]]; then
CMPDIR="$2"
    if [ ! -d $CMPDIR -o ! -r $CMPDIR ]; then
echo "$PROGRAM: $CMPDIR is not a readable directory" 1>&2
        usageAndExit $E_BAD_OPTION
    fi
fi
 
"$TARUTIL" -tf "$TARBALL" | grep -v '/$' > "$TMPDIR/$PROGRAM.out.$$"
 
num_total_files=`grep -v '/$' "$TMPDIR/$PROGRAM.out.$$" | wc -l | awk '{print $1}'`
num_skipped=0 # count of non-normal files skipped
num_audited=0
num_modified=0 # this also becomes the exit value
while read file; do
num_audited=`expr $num_audited + 1`
    rel_hash= # the released file's hash
    old_hash= # the filesystem file's hash
 
    "$TARUTIL" -C "$TMPDIR" -xf "$TARBALL" "$file"
 
    if [ -L "$file" ]; then # always echo symbolic links so their are untarred
        echo "$file is a symbolic link, skipping"
        num_skipped=`expr $num_skipped + 1`
        continue
fi
 
rel_hash=$(getHashFromUtil "$("$MD5UTIL" "$TMPDIR/$file")")
    if [ -f "$CMPDIR/$file" ]; then
old_hash=$(getHashFromUtil "$("$MD5UTIL" "$CMPDIR/$file")")
    fi
 
if [ "$old_hash" != "$rel_hash" ]; then
echo "$file does not match $CMPDIR/$file"
        num_modified=`expr $num_modified + 1`
    fi
 
if [ $SHOWPROGRESS -eq 1 -a `expr $num_audited % 10` -eq 0 ]; then
clear 1>&2 # clear standard error only
        showTotals 1>&2 # because we're only outputting there
    fi
done < "$TMPDIR/$PROGRAM.out.$$"
 
test $SHOWTOTALS -eq 1 && showTotals 1>&2
 
# limit exit status to common Unix practice
if [ $num_modified -lt 126 ]; then
exit $num_modified
else
exit 125
fi