openpkg/lint-rpm.pl

changeset 428
f880f219c566
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/openpkg/lint-rpm.pl	Tue Jul 31 12:23:42 2012 +0200
     1.3 @@ -0,0 +1,447 @@
     1.4 +##
     1.5 +##  lint-rpm.pl -- OpenPKG *.rpm File Checker
     1.6 +##  Copyright (c) 2000-2012 OpenPKG GmbH <http://openpkg.com/>
     1.7 +##
     1.8 +##  This software is property of the OpenPKG GmbH, DE MUC HRB 160208.
     1.9 +##  All rights reserved. Licenses which grant limited permission to use,
    1.10 +##  copy, modify and distribute this software are available from the
    1.11 +##  OpenPKG GmbH.
    1.12 +##
    1.13 +##  THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
    1.14 +##  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
    1.15 +##  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
    1.16 +##  IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR
    1.17 +##  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    1.18 +##  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
    1.19 +##  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
    1.20 +##  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
    1.21 +##  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    1.22 +##  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
    1.23 +##  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
    1.24 +##  SUCH DAMAGE.
    1.25 +##
    1.26 +
    1.27 +#   Perl run-time requirement
    1.28 +require 5;
    1.29 +BEGIN {
    1.30 +    eval "use Getopt::Long; use IO;";
    1.31 +    if ($@) {
    1.32 +        print STDERR
    1.33 +            "lint-rpm: ERROR: This command requires a full-size Perl installation!\n" .
    1.34 +            "lint-rpm: HINT:  Install OpenPKG \"perl\" package to use this command.\n";
    1.35 +        exit(1);
    1.36 +    }
    1.37 +}
    1.38 +
    1.39 +#   OpenPKG instance prefix
    1.40 +my $my_prefix = $ENV{'OPENPKG_PREFIX'};
    1.41 +delete $ENV{'OPENPKG_PREFIX'};
    1.42 +
    1.43 +#   program information
    1.44 +my $progname = "lint-rpm";
    1.45 +my $progvers = "1.0.0";
    1.46 +
    1.47 +#   parameters (defaults)
    1.48 +my $version  = 0;
    1.49 +my $verbose  = 0;
    1.50 +my $help     = 0;
    1.51 +my $check    = 'all';
    1.52 +my $tmpdir   = ($ENV{TMPDIR} || $ENV{TEMPDIR} || "/tmp") . "/$progname";
    1.53 +my $rpm      = "$my_prefix/bin/openpkg rpm";
    1.54 +my $rpm2cpio = "$my_prefix/bin/openpkg rpm2cpio";
    1.55 +
    1.56 +#   exception handling support
    1.57 +$SIG{__DIE__} = sub {
    1.58 +    my ($err) = @_;
    1.59 +    $err =~ s|\s+at\s+.*||s if (not $verbose);
    1.60 +    print STDERR "$progname:ERROR: $err ". ($! ? "($!)" : "") . "\n";
    1.61 +    exit(1);
    1.62 +};
    1.63 +
    1.64 +#   command line parsing
    1.65 +Getopt::Long::Configure("bundling");
    1.66 +my $result = GetOptions(
    1.67 +    'V|version'     => \$version,
    1.68 +    'v|verbose'     => \$verbose,
    1.69 +    'h|help'        => \$help,
    1.70 +    'c|check=s'     => \$check,
    1.71 +    't|tmpdir=s'    => \$tmpdir,
    1.72 +    'r|rpm=s'       => \$rpm,
    1.73 +) || die "option parsing failed";
    1.74 +if ($help) {
    1.75 +    print "Usage: $progname [options] [RPMFILE ...]\n" .
    1.76 +          "Available options:\n" .
    1.77 +          " -v,--verbose       enable verbose run-time mode\n" .
    1.78 +          " -h,--help          print out this usage page\n" .
    1.79 +          " -c,--check=CHECKS  select checks to perform (default='all')\n" .
    1.80 +          " -r,--rpm=FILE      filesystem path to RPM program\n" .
    1.81 +          " -t,--tmpdir=PATH   filesystem path to temporary directory\n" .
    1.82 +          " -V,--version       print program version\n";
    1.83 +    exit(0);
    1.84 +}
    1.85 +if ($version) {
    1.86 +    print "OpenPKG $progname $progvers\n";
    1.87 +    exit(0);
    1.88 +}
    1.89 +
    1.90 +#   verbose message printing
    1.91 +sub msg_verbose {
    1.92 +    my ($msg) = @_;
    1.93 +    print STDERR "$msg\n" if ($verbose);
    1.94 +}
    1.95 +
    1.96 +#   warning message printing
    1.97 +sub msg_warning {
    1.98 +    my ($msg) = @_;
    1.99 +    print STDERR "$progname:WARNING: $msg\n";
   1.100 +}
   1.101 +
   1.102 +#   error message printing
   1.103 +sub msg_error {
   1.104 +    my ($msg) = @_;
   1.105 +    print STDERR "$progname:ERROR: $msg\n";
   1.106 +}
   1.107 +
   1.108 +#   determine check list
   1.109 +my @check_list = (qw(
   1.110 +    layout
   1.111 +    attrib
   1.112 +    content
   1.113 +));
   1.114 +my @checks = ();
   1.115 +if ($check eq 'all') {
   1.116 +    @checks = @check_list;
   1.117 +}
   1.118 +else {
   1.119 +    foreach my $c (split(/,/, $check)) {
   1.120 +        if (not grep(/^$c$/, @check_list)) {
   1.121 +            die "invalid check \"$c\"";
   1.122 +        }
   1.123 +        push(@checks, $c);
   1.124 +    }
   1.125 +}
   1.126 +
   1.127 +#   global return code
   1.128 +$main::GRC = 0;
   1.129 +
   1.130 +#   environment preparation
   1.131 +system("rm -rf $tmpdir");
   1.132 +system("mkdir -p $tmpdir");
   1.133 +
   1.134 +#   iterate over all .rpm files
   1.135 +foreach my $filename (@ARGV) {
   1.136 +    die "unable to open file \"$filename\" for reading"
   1.137 +        if (not -f $filename);
   1.138 +    my $info = &rpm_info($filename, \@checks);
   1.139 +    foreach my $check (@checks) {
   1.140 +        eval "\&check_$check(\$filename, \$info);";
   1.141 +    }
   1.142 +    &rpm_info_cleanup($filename, \@checks, $info);
   1.143 +}
   1.144 +
   1.145 +#   environment cleanup
   1.146 +system("rm -rf $tmpdir");
   1.147 +
   1.148 +#   die gracefully
   1.149 +exit($main::GRC);
   1.150 +
   1.151 +##  _________________________________________________________________
   1.152 +##
   1.153 +##  COMMON SUBROUTINES
   1.154 +##  _________________________________________________________________
   1.155 +##
   1.156 +
   1.157 +sub lint_message {
   1.158 +    my ($type, $file, $msg) = @_;
   1.159 +    $file =~ s|^.+?/([^/]+)$|$1|s;
   1.160 +    printf(STDERR "%s:%s: %s: %s\n", $progname, $type, $file, $msg);
   1.161 +}
   1.162 +
   1.163 +sub lint_warning {
   1.164 +    my ($file, $msg) = @_;
   1.165 +    &lint_message("WARNING", $file, $msg);
   1.166 +    $main::GRC = 1 if ($main::GRC < 1);
   1.167 +}
   1.168 +
   1.169 +sub lint_error {
   1.170 +    my ($file, $msg) = @_;
   1.171 +    &lint_message("ERROR", $file, $msg);
   1.172 +    $main::GRC = 2 if ($main::GRC < 2);
   1.173 +}
   1.174 +
   1.175 +##  _________________________________________________________________
   1.176 +##
   1.177 +##  RPM INFORMATION GATHERING
   1.178 +##  _________________________________________________________________
   1.179 +##
   1.180 +
   1.181 +sub rpm_info {
   1.182 +    my ($filename, $checks) = @_;
   1.183 +    my $info = {};
   1.184 +
   1.185 +    #   query package name
   1.186 +    &msg_verbose("++ querying RPM package name");
   1.187 +    $info->{name} = `$rpm -qp --qf '%{NAME}' $filename`;
   1.188 +
   1.189 +    #   query prefix
   1.190 +    &msg_verbose("++ querying RPM package installation prefix");
   1.191 +    $info->{prefix} = `$rpm -qp --qf '%{PREFIXES}' $filename`;
   1.192 +
   1.193 +    #   query file listing
   1.194 +    &msg_verbose("++ querying RPM package file listing");
   1.195 +    my @list = `$rpm -qplv $filename`;
   1.196 +    my @config = `$rpm -qplc $filename`;
   1.197 +
   1.198 +    #   process file listing
   1.199 +    $info->{ls} = {};
   1.200 +    foreach my $entry (@list) {
   1.201 +        if ($entry =~ m|^\(contains no files\)\s*$|s) {
   1.202 +            next;
   1.203 +        }
   1.204 +        elsif ($entry =~ m|^(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(.{12})\s+(.+)\s*$|s) {
   1.205 +            my ($perm, $links, $owner, $group, $size, $mtime, $path) = ($1, $2, $3, $4, $5, $6, $7);
   1.206 +            my $symlink = "";
   1.207 +            if ($path =~ m|^(\S+)\s+->\s+(\S+)$|) {
   1.208 +                ($path, $symlink) = ($1, $2);
   1.209 +            }
   1.210 +            $path =~ s|\s+$||s;
   1.211 +            my $config = 0;
   1.212 +            if (grep(m|^$path$|, @config)) {
   1.213 +                $config = 1;
   1.214 +            }
   1.215 +            $info->{ls}->{$path} = {
   1.216 +                'perm'    => $perm,
   1.217 +                'links'   => $links,
   1.218 +                'owner'   => $owner,
   1.219 +                'group'   => $group,
   1.220 +                'size'    => $size,
   1.221 +                'time'    => $mtime,
   1.222 +                'path'    => $path,
   1.223 +                'symlink' => $symlink,
   1.224 +                'config'  => $config,
   1.225 +            };
   1.226 +        }
   1.227 +        else {
   1.228 +            &lint_error($filename, "invalid file listing entry: \"$entry\"");
   1.229 +        }
   1.230 +    }
   1.231 +
   1.232 +    #   unpacking files
   1.233 +    if (grep(/^content$/, @{$checks})) {
   1.234 +        &msg_verbose("++ unpacking RPM package files");
   1.235 +        $info->{root} = "$tmpdir/root";
   1.236 +        system("mkdir -p ".$info->{root});
   1.237 +        system("$rpm2cpio $filename | (cd ".$info->{root}." && cpio -idmu 2>/dev/null)");
   1.238 +    }
   1.239 +
   1.240 +    return $info;
   1.241 +}
   1.242 +
   1.243 +sub rpm_info_cleanup {
   1.244 +    my ($filename, $checks, $info) = @_;
   1.245 +
   1.246 +    if (grep(/^content$/, @{$checks})) {
   1.247 +        system("rm -rf ".$info->{root}) if ($info->{root} =~ m/^\/.+/ and -d $info->{root});
   1.248 +    }
   1.249 +}
   1.250 +
   1.251 +##  _________________________________________________________________
   1.252 +##
   1.253 +##  CHECK "layout": file path layout
   1.254 +##  _________________________________________________________________
   1.255 +##
   1.256 +
   1.257 +sub check_layout {
   1.258 +    my ($rpm, $info) = @_;
   1.259 +
   1.260 +    #   no need to check 'openpkg' package because it
   1.261 +    #   has a hard-coded file list!
   1.262 +    return if ($rpm =~ m|^(.+?/)?openpkg-\d[^/]+$|);
   1.263 +
   1.264 +    #   check prefix
   1.265 +    if ($info->{prefix} !~ m|^/.+$|) {
   1.266 +        &lint_error($rpm, "invalid installation prefix ".$info->{prefix}.
   1.267 +                    " (expected to match \"^/.+\$\")");
   1.268 +        return;
   1.269 +    }
   1.270 +
   1.271 +    #   check top-level path (all-in-one)
   1.272 +    my @topdirs = (qw(
   1.273 +        bin cgi etc include info lib libexec
   1.274 +        local man pub sbin share usr var
   1.275 +    ));
   1.276 +    my $topdirs = "{".join(",", @topdirs)."}";
   1.277 +    if (not keys(%{$info->{ls}})) {
   1.278 +        &lint_error($rpm, "invalid empty package (expected at least one file)");
   1.279 +        return;
   1.280 +    }
   1.281 +    foreach my $path (keys(%{$info->{ls}})) {
   1.282 +        my $ok = 0;
   1.283 +        foreach my $topdir (@topdirs) {
   1.284 +            my $prefix = quotemeta($info->{prefix} . "/" . $topdir);
   1.285 +            if ($path =~ m/^$prefix$/ && $rpm !~ m|^openpkg-\d+|) {
   1.286 +                &lint_error($rpm, "top-level directory \"$topdir\" provided" .
   1.287 +                            " (expected none except for 'openpkg' package)");
   1.288 +            }
   1.289 +            if ($path =~ m/^$prefix/) {
   1.290 +                $ok = 1;
   1.291 +                last;
   1.292 +            }
   1.293 +        }
   1.294 +        if (not $ok) {
   1.295 +            &lint_error($rpm, "invalid top-level directory in path \"$path\"".
   1.296 +                       " (expected one of $topdirs)");
   1.297 +        }
   1.298 +    }
   1.299 +
   1.300 +    #   check for second-level path (all-in-one)
   1.301 +    my @topdirs_subdir_no  = (qw(bin cgi info sbin));
   1.302 +    my @topdirs_subdir_yes = (qw(etc libexec share var));
   1.303 +    foreach my $path (keys(%{$info->{ls}})) {
   1.304 +        foreach my $topdir (@topdirs_subdir_yes) {
   1.305 +            my $prefix = quotemeta($info->{prefix} . "/" . $topdir);
   1.306 +            if ($path =~ m/^$prefix\/[^\/]+$/) {
   1.307 +                if ($info->{ls}->{$path}->{perm} !~ m|^d|) {
   1.308 +                    &lint_error($rpm, "invalid positioned file \"$path\" under topdir \"$topdir\" (expected directory)");
   1.309 +                }
   1.310 +            }
   1.311 +        }
   1.312 +        foreach my $topdir (@topdirs_subdir_no) {
   1.313 +            my $prefix = quotemeta($info->{prefix} . "/" . $topdir);
   1.314 +            if ($path =~ m/^$prefix\/[^\/]+$/) {
   1.315 +                if ($info->{ls}->{$path}->{perm} =~ m|^d|) {
   1.316 +                    &lint_error($rpm, "invalid positioned directory \"$path\" under topdir \"$topdir\" (expected file)");
   1.317 +                }
   1.318 +            }
   1.319 +        }
   1.320 +    }
   1.321 +
   1.322 +    #   check "bin" and "sbin" directories
   1.323 +    foreach my $path (keys(%{$info->{ls}})) {
   1.324 +        foreach my $topdir (qw(bin sbin)) {
   1.325 +            my $prefix = quotemeta($info->{prefix} . "/" . $topdir);
   1.326 +            if ($path =~ m/^$prefix\/(.+)$/) {
   1.327 +                my $file = $1;
   1.328 +                if ($file =~ m|^[^/]+\.[^/.]+$|) {
   1.329 +                    &lint_warning($rpm, "strange executable filename \"$path\" containing an extension (expected no extension)");
   1.330 +                }
   1.331 +                my $perm = $info->{ls}->{$path}->{'perm'};
   1.332 +                if ($perm =~ m|^-| && $perm !~ m|^-[-r][-w][sx][-r][-w][-sx][-r][-w][-tx]$|) {
   1.333 +                    &lint_error($rpm, "non-executable file \"$path\" (with permissions \"$perm\" under topdir \"$topdir\" (expected to be executable)");
   1.334 +                }
   1.335 +            }
   1.336 +        }
   1.337 +    }
   1.338 +
   1.339 +    #   check for symbolic link targets (outside absolute, dangling)
   1.340 +    foreach my $path (keys(%{$info->{ls}})) {
   1.341 +        my $symlink = $info->{ls}->{$path}->{'symlink'};
   1.342 +        if ($symlink ne '') {
   1.343 +            #   check for outside absolute target
   1.344 +            my $prefix = quotemeta($info->{prefix});
   1.345 +            if ($symlink =~ m|^/.*| and $symlink !~ m|^$prefix|s) {
   1.346 +                &lint_warning($rpm, "symbolic link \"$path\" points to absolute path \"$symlink\" outside prefix \"$info->{prefix}\" (expected it to be under prefix only)");
   1.347 +            }
   1.348 +            #   check for dangling target
   1.349 +            my $resolved = &resolve($info, $path);
   1.350 +            sub resolve {
   1.351 +                my ($info, $path) = @_;
   1.352 +                if (not defined($info->{ls}->{$path})) {
   1.353 +                    return $path;
   1.354 +                }
   1.355 +                my $symlink = $info->{ls}->{$path}->{'symlink'};
   1.356 +                if ($symlink eq '') {
   1.357 +                    return $path;
   1.358 +                }
   1.359 +                else {
   1.360 +                    my $resolved;
   1.361 +                    if ($symlink =~ m|^/|) {
   1.362 +                        $resolved = $symlink;
   1.363 +                    }
   1.364 +                    else {
   1.365 +                        $resolved = $path;
   1.366 +                        $resolved =~ s|/[^/]+$||s;
   1.367 +                        $resolved .= "/" . $symlink;
   1.368 +                        $resolved =~ s|/{2,}|/|sg;
   1.369 +                        $resolved =~ s|/\.(?=/)||sg;
   1.370 +                        $resolved =~ s|/\.$||sg;
   1.371 +                        1 while ($resolved =~ s|/[^/]+/\.\./|/|s);
   1.372 +                        $resolved =~ s|/[^/]+/\.\.$||s;
   1.373 +                        $resolved =~ s|(.)/$|$1|s;
   1.374 +                    }
   1.375 +                    return &resolve($info, $resolved);
   1.376 +                }
   1.377 +            }
   1.378 +            if (not defined($info->{ls}->{$resolved})) {
   1.379 +                &lint_error($rpm, "symbolic link \"$path\" points to not existing target path \"$resolved\" (expected existing target path)");
   1.380 +            }
   1.381 +        }
   1.382 +    }
   1.383 +}
   1.384 +
   1.385 +##  _________________________________________________________________
   1.386 +##
   1.387 +##  CHECK "attrib": file attributes
   1.388 +##  _________________________________________________________________
   1.389 +##
   1.390 +
   1.391 +sub check_attrib {
   1.392 +    my ($rpm, $info) = @_;
   1.393 +
   1.394 +    #   check for empty files
   1.395 +    foreach my $path (keys(%{$info->{ls}})) {
   1.396 +        if (    $info->{ls}->{$path}->{'size'} == 0
   1.397 +            and $info->{ls}->{$path}->{'perm'} !~ m|^d|
   1.398 +            and $path !~ m/^.*\/lib\/perl\/.+\.bs$/
   1.399 +            and $rpm !~ m/\bopenpkg-[0-9]+/) {
   1.400 +            &lint_warning($rpm, "empty file \"$path\" found (expected no empty files)");
   1.401 +        }
   1.402 +    }
   1.403 +
   1.404 +    #   check for %config flagged files
   1.405 +    my $prefix = quotemeta($info->{'prefix'});
   1.406 +    my $name   = quotemeta($info->{'name'});
   1.407 +    foreach my $path (keys(%{$info->{ls}})) {
   1.408 +        my $config = $info->{ls}->{$path}->{'config'};
   1.409 +        my $path   = $info->{ls}->{$path}->{'path'};
   1.410 +        if ($rpm !~ m/\bopenpkg-[0-9]+/ and $config and $path !~ m/^$prefix\/etc\/($name\d*|fsl)\/.+/) {
   1.411 +            &lint_warning($rpm, "non-config file \"$path\" flagged as \%config (expected only \"$prefix/etc/$name/*\" files to be \%config files)");
   1.412 +        }
   1.413 +        if ($config and $path =~ m/^$prefix\/s?bin\/[^\/]+$/) {
   1.414 +            &lint_error($rpm, "executable file \"$path\" flagged as \%config");
   1.415 +        }
   1.416 +        if ($config and $path =~ m/^$prefix\/etc\/rc\.d\/rc\.$name$/) {
   1.417 +            &lint_error($rpm, "run-command file \"$path\" flagged as \%config");
   1.418 +        }
   1.419 +    }
   1.420 +
   1.421 +    #   check for permissions
   1.422 +    foreach my $path (keys(%{$info->{ls}})) {
   1.423 +        my $perm = $info->{ls}->{$path}->{'perm'};
   1.424 +        if ($path =~ m/^$prefix\/(bin|sbin)\/[^\/]+$/) {
   1.425 +            if ($perm !~ m|^[^d]..[xs]..[xs]..[xt]$|) {
   1.426 +                &lint_warning($rpm, "executable file \"$path\" has permissions \"$perm\" only (expected it to be executable by everyone)");
   1.427 +            }
   1.428 +        }
   1.429 +    }
   1.430 +
   1.431 +    #   TODO: user/group?
   1.432 +}
   1.433 +
   1.434 +##  _________________________________________________________________
   1.435 +##
   1.436 +##  CHECK "content": file content
   1.437 +##  _________________________________________________________________
   1.438 +##
   1.439 +
   1.440 +sub check_content {
   1.441 +    my ($rpm, $info) = @_;
   1.442 +
   1.443 +    #   TODO: stripped (file)
   1.444 +    #   TODO: syslibs (ldd)
   1.445 +    #   TODO: hard-coded paths pointing outside instance
   1.446 +    #   TODO: sanity check for shebang-lines
   1.447 +    #   TODO: contained temporary path
   1.448 +    #   TODO: bad-files: .po
   1.449 +}
   1.450 +

mercurial