From 27ecac9fc88baafcaa183f0c74282202e56d7aac Mon Sep 17 00:00:00 2001 From: Jan Schaumann Date: Thu, 30 Sep 2010 17:05:00 -0400 Subject: [PATCH] Initial import of sources from Yahoo! --- BUGS | 25 + LICENSE | 34 ++ Makefile | 59 +++ README | 10 +- TODO | 9 + bin/fetch-vlist.sh | 205 ++++++++ bin/run-yvc.py | 18 + conf/yvc.conf | 19 + doc/ContributionLicenseAgreementYahoo.pdf | Bin 0 -> 33236 bytes doc/html/Makefile | 18 + doc/html/index.html | 77 +++ doc/man/fetch-vlist.1 | 80 +++ doc/man/yvc.1 | 204 ++++++++ doc/man/yvc.conf.5 | 67 +++ lib/yvc.py | 565 ++++++++++++++++++++++ misc/harvest_freebsd_yvc.pl | 171 +++++++ misc/redhat_oval_to_yvc.py | 93 ++++ test/Makefile | 12 + test/test.py | 295 +++++++++++ 19 files changed, 1955 insertions(+), 6 deletions(-) create mode 100644 BUGS create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 TODO create mode 100755 bin/fetch-vlist.sh create mode 100755 bin/run-yvc.py create mode 100644 conf/yvc.conf create mode 100644 doc/ContributionLicenseAgreementYahoo.pdf create mode 100644 doc/html/Makefile create mode 100644 doc/html/index.html create mode 100644 doc/man/fetch-vlist.1 create mode 100644 doc/man/yvc.1 create mode 100644 doc/man/yvc.conf.5 create mode 100644 lib/yvc.py create mode 100755 misc/harvest_freebsd_yvc.pl create mode 100755 misc/redhat_oval_to_yvc.py create mode 100644 test/Makefile create mode 100755 test/test.py diff --git a/BUGS b/BUGS new file mode 100644 index 0000000..7c29a9c --- /dev/null +++ b/BUGS @@ -0,0 +1,25 @@ +Known bugs: +----------- + +Patchlevels are not dealt with correctly. That is, if, for example, the +package listed in the vulnerabilities file is marked as "foo-1.2pl3" and a +package with a tiny version such as "foo-1.2.1" is installed, it may falsely +match. That is, comparison of "foo-1.2pl3" and "foo-1.2.1" claims that the +patchlevel version is higher. (The converse scenario also holds.) + +This is a restriction of the used distutils.versions.LooseVersion +implementation. Presumably, the assumption is that a piece of software +wouldn't mix patchlevels with tiny versions (?). Note that the expensive +shell-out to parse_version(1) wouldn't solve this problem either: that program +operates on the same assumption. + +---- + +Deeply nested brace expansions are not correctly dealt with. The +braceExpansion function is able to handle simply nested expansions such as +"foo-{,bar{-baz,-bla}}", but deeper levels of nesting may not yield the +expected results. + +For the purposes of the vulnerability list, this seems acceptable for the time +being, as deeply nested version strings are not found. An alternative (albeit +very expensive) would be to shell out to zsh to do brace expansion. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..eec3aca --- /dev/null +++ b/LICENSE @@ -0,0 +1,34 @@ +Software Copyright License Agreement (BSD License) + +Copyright (c) 2010, Yahoo! Inc. +All rights reserved. + +Redistribution and use of this software 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. + +* Neither the name of Yahoo! Inc. nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission of Yahoo! Inc. + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fb6c07a --- /dev/null +++ b/Makefile @@ -0,0 +1,59 @@ +# Copyright (c) 2008,2010 Yahoo! Inc. +# +# This example Makefile can be used to maintain vulnerability list. +# See 'make help' for more information. + +# Location to which to upload the vlists. +LOCATION=":~/public_html/yvc/" +FBVLIST=fbvlist +RH4VLIST=rh4vlist +RH5VLIST=rh5vlist +LISTS= ${RH5VLIST} ${RH4VLIST} ${FBVLIST} + +GONERS= ${RH5VLIST}.in ${RH4VLIST}.in ${FBVLIST}.in \ + com.redhat.rhsa-all.xml.bz2 + +date!=date + +all: sign upload + +help: + @echo "The following targets are available:" + @echo "all sign + upload" + @echo "clean remove any interim files" + @echo "help print this help" + @echo "sign sign the vulnerability list" + @echo "upload upload the vulnerability list" + +sign: ${LISTS} + +${FBVLIST}: ${FBVLIST}.in + gpg -o ${FBVLIST} --clearsign ${FBVLIST}.in + chmod a+r ${FBVLIST} + +${FBVLIST}.in: + @echo "# Generated on ${date}" > ${FBVLIST}.in + perl ./misc/harvest_freebsd_yvc.pl >> ${FBVLIST}.in + + +${RH4VLIST}: ${RH4VLIST}.in + gpg -o ${RH4VLIST} --clearsign ${RH4VLIST}.in + chmod a+r ${RH4VLIST} + +${RH4VLIST}.in: + python ./misc/redhat_oval_to_yvc.py 4 > ${RH4VLIST}.in + + +${RH5VLIST}: ${RH5VLIST}.in + gpg -o ${RH5VLIST} --clearsign ${RH5VLIST}.in + chmod a+r ${RH5VLIST} + +${RH5VLIST}.in: + python ./misc/redhat_oval_to_yvc.py 5 > ${RH5VLIST}.in + + +upload: sign + scp ${LISTS} ${LOCATION} + +clean: + rm -f ${LISTS} ${GONERS} diff --git a/README b/README index 24a38c6..391a96c 100644 --- a/README +++ b/README @@ -1,5 +1,3 @@ -This is a placeholder README for the 'yvc' utility. - 'yvc' is a software package vulnerability checker. 'yvc' compares the given package name against the list of known @@ -9,9 +7,9 @@ further information for each vulnerable package. 'yvc' was conceptually based on NetBSD's audit-packages program (see http://www.netbsd.org/support/security/#check-pkgsrc) and was written by -Jan Schaumann at Yahoo! Inc. +Jan Schaumann in 2008 while working at Yahoo! +Inc. Yahoo! open sourced this tool in the hopes that it will be useful to +other people -- unless otherwise noted, all files are released under the +terms of a 3-clause BSD license as noted in the file LICENSE. The 'y' in yvc can stand for a number of things. Make up your own. - -'yvc' will be made available to the world at large in the very exciting -and very near future. diff --git a/TODO b/TODO new file mode 100644 index 0000000..99ed10a --- /dev/null +++ b/TODO @@ -0,0 +1,9 @@ + +package for public: + - identify required packages + - write configure script to handle fetch-vlist: + - determine appropriate vlists to use + - provide option for place to upload/download + - write python magic to install correctly + +review helper scripts to ensure they work (efficiently) on all platforms diff --git a/bin/fetch-vlist.sh b/bin/fetch-vlist.sh new file mode 100755 index 0000000..7b1d088 --- /dev/null +++ b/bin/fetch-vlist.sh @@ -0,0 +1,205 @@ +#! /bin/sh +# +# Copyright (c) 2008,2009,2010 Yahoo! Inc. +# +# Originally written by Jan Schaumann in July 2008. +# +# The fetch-vlist tool is used to download the vulnerability lists to be +# used by the 'yvc' tool. After downloading them, it will verify the PGP +# signature and, if it checks out, install the files in the final +# destination. + +# Only used during development: +# set -eu + +### +### Globals +### + +DONT="" +EXIT_VALUE=0 +GPG="gpg" +GPG_FLAGS="--verify -q" +GPG_REDIR="2>/dev/null" +IGNORE_PGP_ERRS=0 +PROGNAME="${0##*/}" +TMPFILES="" + +## +## Modify this section to specify where to fetch your vlists from. +## +NLISTS=4 +VLIST1="http://ftp.netbsd.org/pub/NetBSD/packages/vulns/pkg-vulnerabilities" +VLIST1_LOCATION="/usr/local/var/var/yvc/nbvlist" +VLIST2="http:///yvc/fbvlist" +VLIST2_LOCATION="/usr/local/var/yvc/fbvlist" +VLIST3="http:///yvc/rh4vlist" +VLIST3_LOCATION="/usr/local/var/yvc/rh4vlist" +VLIST4="http:///yvc/rh5vlist" +VLIST4_LOCATION="/usr/local/var/yvc/rh5vlist" + +WGET="wget" +WGET_FLAGS="-t 1 -T 10 -q" + +### +### Functions +### + +# function : cleanup +# purose : exit handler to remove any temporarily created files + +cleanup() { + rm -f ${TMPFILES} +} + +# function : error +# purpose : print message to stderr and exit 1 +# input : any string +# output : input is echo'd to stderr, program aborted + +error() { + warn ${1} + exit 1 +} + +# function : warn +# purpose : print message to stderr +# input : any string +# output : input is echo'd to stderr +# sets EXIT_VALUE to 1 to indicate failure + +warn() { + echo "${PROGNAME}: ${1}" >&2 + EXIT_VALUE=1 +} + +# function : fetchVerifyInstall +# purpose : fetch, verify and install all vlists +# input : none +# result : all files are fetched, verified and installed into their +# final location; any errors encountered are caught and an +# appropriate error message printed + +fetchVerifyInstall() { + local n + + n=1 + while [ $n -le ${NLISTS} ]; do + local tmpfile=$(mktemp /tmp/${PROGNAME}.XXXXXX) + local list=$(eval echo \$VLIST${n}) + local target=$(eval echo \$VLIST${n}_LOCATION) + + TMPFILES="${TMPFILES} ${tmpfile}" + n=$(( $n + 1 )) + + fetchList ${tmpfile} ${list} || { + warn "Unable to fetch ${list}." + continue + } + + verifySignature ${tmpfile} || { + if [ ${IGNORE_PGP_ERRS} -ne 1 ]; then + warn "Unable to verify signature of ${list}." + continue + fi + } + + installFile ${tmpfile} ${target} || { + warn "Unable to install ${tmpfile} as ${target}." + continue + } + done +} + +# function : fetchList +# purpose : download the list from the given URL into a temporary +# location +# input : temporary file, list URL +# returns : exit value of wget command + +fetchList() { + local tmpfile=${1} + local url=${2} + + ${DONT} ${WGET} -O ${tmpfile} ${WGET_FLAGS} ${url} +} + +# function : installFile +# purpose : install the temporary file into the final destination if +# needed +# input : temporary file, final location + +installFile() { + local tmpfile=${1} + local final=${2} + + ${DONT} cmp -s ${tmpfile} ${final} || { + ${DONT} mv ${tmpfile} ${final} && \ + ${DONT} chmod 444 ${final} + } +} + +# function : usage +# purpose : print a usage summary +# returns : nothing, usage printed to stdout + +usage() { + echo "Usage: ${PROGNAME} [-dhiv]" + echo " -d don't do anything, just report what would be done" + echo " -h print this help and exit" + echo " -i ignore any pgp errors" + echo " -v be verbose" +} + +# function : verifySignature +# purpose : verify the pgp signature on the given file +# input : filename +# returns : retval of gpg command + +verifySignature() { + local file=${1} + ${DONT} eval ${GPG} ${GPG_FLAGS} ${file} ${GPG_REDIR} +} + +### +### Main +### + +trap cleanup 0 + +while getopts 'dhiv' opt; do + case ${opt} in + d) + DONT="echo" + ;; + h|\?) + usage + exit 0 + # NOTREACHED + ;; + i) + IGNORE_PGP_ERRS=1 + ;; + v) + WGET_FLAGS="-v" + GPG_FLAGS="${GPG_FLAGS} -v" + GPG_REDIR="" + ;; + *) + usage + exit 1 + # NOTREACHED + ;; + esac +done +shift $(( ${OPTIND} - 1 )) + +if [ $# -ne 0 ]; then + usage + exit 1 + # NOTREACHED +fi + +fetchVerifyInstall + +exit ${EXIT_VALUE} diff --git a/bin/run-yvc.py b/bin/run-yvc.py new file mode 100755 index 0000000..9ea91cb --- /dev/null +++ b/bin/run-yvc.py @@ -0,0 +1,18 @@ +#! /usr/local/bin/python2.5 +# +# Copyright (c) 2008,2010 Yahoo! Inc. +# +# Originally written by Jan Schaumann in July 2008. +# +# The entire functionality of the yvc(1) tool is found in the +# yahoo.yvc.Checker class. This script just invokes the 'main' function +# provided by yahoo.yvc. + +### +### Main +### + +if __name__ == "__main__": + import sys + from yahoo.yvc import main + main(sys.argv[1:]) diff --git a/conf/yvc.conf b/conf/yvc.conf new file mode 100644 index 0000000..5eb21e4 --- /dev/null +++ b/conf/yvc.conf @@ -0,0 +1,19 @@ +# This is the default configuration file for yvc(1). See yvc.conf(5) for +# details. + +# This section is required, don't remove it. +[YVC] + +# A list of vulnerability types that should be ignored. +# See yvc(1) for the exhaustive list of possible vulnerability types. +# For example: +# IGNORE_TYPES = denial-of-service, permissions-race + +# A list of URLs that should be ignored. For example: +# IGNORE_URLS = http://online.securityfocus.com/archive/1/272180 + +# The files in which the list of vulnerabilities are found. +VLISTS = /usr/local/var/yvc/fbvlist + +# Level of verbosity. +#VERBOSITY = 1 diff --git a/doc/ContributionLicenseAgreementYahoo.pdf b/doc/ContributionLicenseAgreementYahoo.pdf new file mode 100644 index 0000000000000000000000000000000000000000..66b445b77dfc58d47f170c9bdadee9f187fa86ea GIT binary patch literal 33236 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 NmYiHvUJL>D{{gKI+|K|2 literal 0 HcmV?d00001 diff --git a/doc/html/Makefile b/doc/html/Makefile new file mode 100644 index 0000000..232be08 --- /dev/null +++ b/doc/html/Makefile @@ -0,0 +1,18 @@ +# A Makefile to create html docs from the man pages. + +MANPAGES=../man/fetch-vlist.1 ../man/yvc.1 ../man/yvc.conf.5 + +all: html + +html: ${MANPAGES} +.for m in ${MANPAGES} + nroff -man ${m} | rman -f html > ${m:S/..\/man\///}.html +.endfor + +clean: +.for m in ${MANPAGES} + @rm -f ${m:S/..\/man\///}.html +.endfor + +upload: + scp *.html :~/public_html/yvc/ diff --git a/doc/html/index.html b/doc/html/index.html new file mode 100644 index 0000000..9b29974 --- /dev/null +++ b/doc/html/index.html @@ -0,0 +1,77 @@ + + + yvc -- a software package vulnerability check + + +

yvc -- a software package vulnerability check

+

+ yvc compares the given package name against the list of + known vulnerabilities and reports any security issues. This output + contains the name and version of the package, the type of vulnerability, + and a URL for further information for each vulnerable package. +

+

+ yvc was conceptually based on NetBSD's + audit-packages program and was written by Jan Schaumann in 2008 while + working at Yahoo! Inc. Yahoo! open sourced the tool in the hopes that + it will be useful to other people -- unless otherwise noted, all files + are released under the terms of a 3-clause BSD license as noted in the + file LICENSE. +

+

+ The 'y' in yvc can stand for a number of things. Make up your own. +

+

The sources to yvc can be found at GitHub: + http://github.com/jschauma/yvc.

+
+

Vulnerability lists

+

+ The following lists of known vulnerabilities are available: +

+
+

Common usage

+

+ It is recommended for users of this package to run the fetch-vlist periodically from cron. + It is also recommended to run the yvc command regularly + from cron. + An example crontab would look like this:

+ +0 3 * * * /usr/local/bin/fetch-vlist
+0 4 * * * rpm -qa | /usr/local/bin/yvc +
+

+

+ Of course you can also invoke yvc manually. See the examples in the manual page for details. +

+

Manual pages

+ +
+ + diff --git a/doc/man/fetch-vlist.1 b/doc/man/fetch-vlist.1 new file mode 100644 index 0000000..9e1a999 --- /dev/null +++ b/doc/man/fetch-vlist.1 @@ -0,0 +1,80 @@ +.\" Copyright (c) 2008,2009,2010 Yahoo! Inc. +.\" +.Dd September 30, 2010 +.Dt FETCH-VLIST 1 +.Os +.Sh NAME +.Nm fetch-vlist +.Nd fetch and install vulnerbility lists +.Sh SYNOPSIS +.Nm +.Op Fl dhiv +.Sh DESCRIPTION +The +.Nm +tool downloads and installs the vulnerability lists used by +.Xr yvc 1 . +Each list is expected to be PGP signed; +.Nm +will verify the signature after donwloading the file. +.Pp +Originally, the lists are fetched into a temporary location. +If a fetched file is different from the currently used version, then it is +installed. +.Sh OPTIONS +The following options are supported by +.Nm : +.Bl -tag -width _h +.It Fl d +Don't do anything, just report what would be done. +.It Fl h +Print a short usage statement and exit. +.It Fl i +Ignore any errors due to the PGP signature. +Errors may include the inability to verify the signature because the +public key is not in the used keyring or an actual signature mismatch. +.It Fl v +Be verbose. +.El +.Sh LISTS +The following lists may be downloaded and installed by +.Nm : +.Bl -tag -width nbvlist_ +.It fbvlist +A list of vulnerabilities known in the FreeBSD ports collection, derived +from http://www.freebsd.org/ports/portaudit/ and fetched from +http:///yvc/fbvlist. +.It nbvlist +A list of vulnerabilities provided by the NetBSD Project. +See http://www.netbsd.org/support/security/#check-pkgsrc for details. +Retrieved from +http://ftp.netbsd.org/pub/NetBSD/packages/vulns/pkg-vulnerabilities. +.It rh4vlist +A list of vulnerabilities known in RHEL4, derived from +http://www.redhat.com/security/data/oval/com.redhat.rhsa-all.xml.bz2 and +fetched from +http:///yvc/rh4vlist. +.It rh5vlist +A list of vulnerabilities known in RHEL5, derived from +http://www.redhat.com/security/data/oval/com.redhat.rhsa-all.xml.bz2 and +fetched from +http:///yvc/rh5vlist. +.El +.Sh EXIT STATUS +.Ex -std +.Sh FILES +.Bl -tag -width _home_y_var_yvc_ +.It /home/y/var/yvc +The final directory into which the file is installed. +.El +.Sh SEE ALSO +.Xr yvc 1 +.Sh HISTORY +.Nm +was conceptually based on NetBSD's "download-vulnerability-list" command. +It was originally written by +.An Jan Schaumann +.Aq jschauma@yahoo-inc.com +in July 2008. +.Sh BUGS +Please report bugs and feature requests to the author. diff --git a/doc/man/yvc.1 b/doc/man/yvc.1 new file mode 100644 index 0000000..5345498 --- /dev/null +++ b/doc/man/yvc.1 @@ -0,0 +1,204 @@ +.\" Copyright (c) 2008,2009,2010 Yahoo! Inc. +.\" +.Dd September 30, 2010 +.Dt YVC 1 +.Os +.Sh NAME +.Nm yvc +.Nd a software package vulnerability checker +.Sh SYNOPSIS +.Nm +.Op Fl hv +.Op Fl c Ar file +.Op Fl l Ar file +.Op Ar pkg Oo Ar ... Oc +.Sh DESCRIPTION +The +.Nm +tool compares the given package name against the list of known +vulnerabilities and reports any security issues. +This output contains the name and version of the package, the type of +vulnerability, and a URL for further information for each vulnerable package. +.Sh OPTIONS +The following options are supported by +.Nm : +.Bl -tag -width l_file_ +.It Fl c Ar file +Read configuration from +.Ar file +(default: /usr/local/etc/yvc.conf). +.It Fl h +Print a short usage statement and exit. +.It Fl l Ar file +Check against the list of vulnerabilities provided in +.Ar file . +Can be used multiple times. +.It Fl v +Be verbose. +Can be used multiple times. +.El +.Sh INPUT +.Nm +takes as input a list of package names. +If no package names are given on the command-line, +.Nm +will read them from stdin. +.Pp +Input from stdin and from the command-line can be combined: if +.Nm +encounters "-" as an argument, it will read from stdin at that point. +.Sh DETAILS +.Nm +will then try to match each package against a list of known +vulnerabilities. +.Pp +The list of known vulnerabilities is taken from a text file. +In this file, each line lists the package and vulnerable versions, the type of +exploit, and an Internet address for further information: +.Bl -item +.It +.Aq package pattern +.Aq type +.Aq url +.El +.Pp +The type of exploit can be any text, although +some common types of exploits listed are: +.Bl -bullet -compact -offset indent +.It +cross-site-html +.It +cross-site-scripting +.It +denial-of-service +.It +file-permissions +.It +local-access +.It +local-code-execution +.It +local-file-read +.It +local-file-removal +.It +local-file-write +.It +local-root-file-view +.It +local-root-shell +.It +local-symlink-race +.It +local-user-file-view +.It +local-user-shell +.It +privacy-leak +.It +remote-code-execution +.It +remote-command-inject +.It +remote-file-creation +.It +remote-file-read +.It +remote-file-view +.It +remote-file-write +.It +remote-key-theft +.It +remote-root-access +.It +remote-root-shell +.It +remote-script-inject +.It +remote-server-admin +.It +remote-use-of-secret +.It +remote-user-access +.It +remote-user-file-view +.It +remote-user-shell +.It +unknown +.It +weak-authentication +.It +weak-encryption +.It +weak-ssl-authentication +.El +.Pp +The list of vulnerabilities is stored per default in two files under +/usr/local/var/yvc/. +.Sh CONFIGURATION +At startup, +.Nm +reads the system-wide configuration file /usr/local/etc/yvc.conf. +See +.Xr yvc.conf 5 +for details. +.Sh EXAMPLES +.Nm +can be run via +.Xr cron 8 , +to check the installed packages on a regular basis. +One might wish to invoke +.Nm +as: +.Bd -literal -offset indent +pkg_info | awk '{print $1}' | yvc +.Ed +.Pp +To check the packages 'zsh-4.2.6' and 'sudo-1.6.8pl1' against any known +vulnerabilities: +.Bd -literal -offset indent +yvc zsh-4.2.6 sudo-1.6.8pl1 +.Ed +.Pp +To check all rpms on the host +\'hostname.yahoo.com': +.Bd -literal -offset indent +ssh hostname.yahoo.com "rpm -qa" | yvc +.Ed +.Sh EXIT STATUS +.Ex -std +.Sh FILES +.Bl -tag -width _home_y_var_yvc_nbvlist_ +.It /usr/local/etc/yvc.conf +The +.Nm +configuration file. +.It /usr/local/var/yvc/fbvlist +A list of known vulnerabilities in the FreeBSD ports collection derived +from http://www.freebsd.org/ports/portaudit/. +.It /usr/local/var/yvc/nbvlist +A list of vulnerabilities provided by the NetBSD Project. +See http://www.netbsd.org/support/security/#check-pkgsrc for details. +.It /usr/local/var/yvc/rh4vlist +A list of vulnerabilities known in RHEL4, derived from +http://www.redhat.com/security/data/oval/com.redhat.rhsa-all.xml.bz2 . +.It /usr/local/var/yvc/rh5vlist +A list of vulnerabilities known in RHEL5, derived from +http://www.redhat.com/security/data/oval/com.redhat.rhsa-all.xml.bz2 . +.El +.Sh SEE ALSO +.Xr fetch-vlist 1 , +.Xr rpm 1 , +.Xr yinst 1 , +.Xr yvc.conf 5 +.Sh HISTORY +.Nm +was conceptually based on NetBSD's "audit-packages" command. +It was originally written by +.An Jan Schaumann +.Aq jschauma@yahoo-inc.com +in July 2008. +.Sh BUGS +Please report bugs and feature requests to the author. diff --git a/doc/man/yvc.conf.5 b/doc/man/yvc.conf.5 new file mode 100644 index 0000000..b726588 --- /dev/null +++ b/doc/man/yvc.conf.5 @@ -0,0 +1,67 @@ +.\" $Id: yvc.conf.5 4 2010-09-30 14:35:04Z jans $ +.\" $URL: svn+ssh://svn.corp.yahoo.com/yahoo/tools/yvc/branches/outgoing/doc/man/yvc.conf.5 $ +.\" +.\" Copyright (c) 2008 Yahoo! Inc. +.\" +.Dd October 31, 2009 +.Dt YVC.CONF 1 +.Os +.Sh NAME +.Nm yvc.conf +.Nd +.Xr yvc 1 +configuration file +.Sh DESCRIPTION +The +.Nm +file specifies the various configuration options for +.Xr yvc 1 . +.Pp +The +.Nm +file consists of sections and parameters. +A section begins with the name of the section in square brackets and continues +until the next section begins or the end the file is reached. +.Pp +Sections contain parameters of the form 'name: value'. +.\" .Pp +.\" The values can contain format strings which refer to other values in the same +.\" section. +.\" .Pp +.\" For example: +.\" .Bd -literal -offset indent +.\" something: %(dir)s/whatever +.\" .Ed +.\" .Pp +.\" would resolve the "%(dir)s" to the value of dir. +.Pp +A line starting with the '#' character is ignored. +.Sh SECTIONS +At the moment, the only section supported is "[YVC]". +.Sh PARAMETERS +The following parameters are understood: +.Bl -tag -width LIST_OF_VULNERABILITIES_ +.It IGNORE_TYPES +A list of vulnerability types that should be ignored. +.It IGNORE_URLS +A list of URLs that should be ignored. +That is, if a package is found to be vulnerable for the given URL, ignore it. +.It VLISTS +The file(s) in which the list(s) of vulnerabilities is/are found. +Defaults to /home/y/var/yvc/yvlist and a platform-specific vlist. +.It VERBOSITY +Level of verbosity. +Valid values are '0' (no output except for vulnerable packages), '1' (some +output), '2' (a lot of output). +Defaults to '0'. +.El +.Sh SEE ALSO +.Xr fetch-vlist 1 , +.Xr rpm 1 , +.Xr yinst 1 , +.Xr yvc 1 +.Sh HISTORY +Support for the +.Nm +file was built into the first release of +.Xr yvc 1 . diff --git a/lib/yvc.py b/lib/yvc.py new file mode 100644 index 0000000..2927277 --- /dev/null +++ b/lib/yvc.py @@ -0,0 +1,565 @@ +"""a software packages vulnerability checker + +An interface to compare given package names against the list of known +vulnerabilities and report any security issues. This interface is used by +the yvc(1) tool, though it's possible that it might prove useful for other +tools. + +yvc was based conceptually on NetBSD's audit-packages(1) command. + +""" + +# Copyright (c) 2008,2010 Yahoo! Inc. +# +# Originally written by Jan Schaumann in July 2008. + +import ConfigParser + +from distutils.version import LooseVersion +from fnmatch import fnmatch +import getopt +import logging +import os +import re +import stat +import string +import subprocess +import sys + +### +### Classes +### + +class Checker(object): + """A Software Package Vulnerability Checker + + The main interface of the 'yvc' program. Its member functions are used to + run the program, it's private members are (mostly) configuration options + that can be set via the command-line. + """ + + EXIT_ERROR = 1 + EXIT_SUCCESS = 0 + + def __init__(self): + """Construct a Checker object with default values.""" + + self.__opts = { + "cfg_file" : "/usr/local/etc/yvc.conf", + "ignore_types" : None, + "ignore_urls" : None, + "vlists" : [], + "verbosity" : logging.WARNING + } + + self.__frobbed = {} + + self.__list_opts = [ "ignore_types", "ignore_urls" ] + self.__int_opts = [ "verbosity" ] + + self.__cfg_section = "YVC" + + self.__vulns = [] + + + def _setVerbosity(self, f): + """set the verbosity based on the given factor""" + + n = int(f) + v = self.getOpt("verbosity") + + if (v > logging.INFO): + v = logging.INFO + if (n > 1): + # XXX: magic number; logging uses specific numbers, but + # has no specified increment + v -= (5 * n) + + # The logging module treats 0 as 'unset'. + if v < 1: + v = 1 + self.setOpt("verbosity", v) + logging.basicConfig(level=self.getOpt("verbosity"), + format='%(message)s') + + + class Usage(Exception): + """A simple exception that provides a usage statement and a return code.""" + + def __init__(self, rval): + self.err = rval + self.msg = 'Usage: %s [-hv] [-c file] [-l file] [pkg [...]]\n' \ + % os.path.basename(sys.argv[0]) + self.msg += '\t-c file read configuration from file\n' + self.msg += '\t-h print this message and exit\n' + self.msg += '\t-l file check against the list of vulnerabilities provided in file\n' + self.msg += '\t-v be verbose\n' + + + def checkPackage(self, package): + """check a given package against all vulnerabilities and report results + + Arguments: + package -- package name to check + """ + + logging.info("Checking package '%s'..." % package) + pkg = os.path.basename(package) + for v in self.__vulns: + if self.ignore(v): + continue + logging.log(15, "Checking package '%s' against %s..." % (package, v.url)) + if v.match(pkg): + print "Package %s has a %s vulnerability, see: %s" % \ + (package, v.type, v.url) + + + def getOpt(self, opt): + """Retrieve the given configuration option. + + Returns: + The value for the given option if it exists, None otherwise. + """ + + try: + r = self.__opts[opt] + except ValueError: + r = None + + return r + + + def ignore(self, v): + """determine whether or not to ignore a given Vulnerability + + Arguments: + v -- a Vulnerability + + Returns: + True or False + """ + + if self.__opts["ignore_types"]: + try: + i = self.__opts["ignore_types"].index(v.type) + logging.log(15, "Ignoring vulnerability %s based on type %s." + % (v.url, v.type)) + return True + except ValueError: + pass + + if self.__opts["ignore_urls"]: + try: + i = self.__opts["ignore_urls"].index(v.url) + logging.log(15, "Ignoring vulnerability %s based on URL." % v.url) + return True + except ValueError: + pass + + return False + + + def makeV(self, line): + """create a Vulnerability object from the given line + + Arguments: + line -- a line from a vulnerability list, expected to be in the + format "nametypeurl" + + Returns: + None -- input line was not in expected format; or + an object of type Vulnerability + """ + + v = None + + pattern = re.compile('(?P^[^#\s]+)\s+(?P[^\s]+)\s+(?P.*)$') + rem = pattern.match(line) + + if rem: + v = Vulnerability(rem.group('pattern'), + rem.group('type'), + rem.group('url')) + + return v + + + def parseConfig(self, cfile): + """parse the configuration file and set appropriate variables + + This function may throw an exception if it can't read or parse the + configuration file (for any reason). + + Arguments: + cfile -- the configuration file to parse + """ + + cfg = ConfigParser.ConfigParser() + try: + f = file(cfile) + except IOError, e: + logging.error("Unable to open config file '%s': %s" % \ + (self.__opts["cfg_file"], e.strerror)) + raise + + try: + cfg.readfp(f) + f.close() + except ConfigParser.ParsingError, e: + logging.error("Unable to parse config file: %s" % e.__repr__()) + raise + # NOTREACHED + + if not cfg.has_section(self.__cfg_section): + raise ConfigParser.NoSectionError("Default section \"%s\" not found in %s." + % (self.__cfg_section, self.__opts["cfg_file"])) + + for key in self.__opts: + v = None + + if key in self.__frobbed: + v = self.__frobbed[key] + else: + if cfg.has_option(self.__cfg_section, key): + v = cfg.get(self.__cfg_section, key) + + if v: + if (key == "verbosity"): + self._setVerbosity(v) + elif (key == "vlists"): + if type(v) is str: + lists = v.split() + elif type(v) is list: + lists = v + else: + logging.error("'%s' is of type %s??" % (key, type(v))) + continue + self.__opts[key] = lists + else: + self.__opts[key] = v + + + def parseList(self, list): + """parse the vulnerability list and build a list of vulnerabilities + + This function may throw an exception if it can't read or parse the + configuration file (for any reason). + + Arguments: + list -- the file containing the vulnerabilities + """ + + logging.info("Parsing vulnerability list (%s)." % list) + try: + f = file(list) + except IOError, e: + logging.error("Unable to open list of vulnerabilities '%s': %s" % (list, e)) + raise + + line = f.readline() + while len(line) != 0: + v = self.makeV(line) + if v and not self.ignore(v): + self.__vulns.append(v) + line = f.readline() + + f.close() + + + def parseOptions(self, inargs): + """Parse given command-line options and set appropriate attributes. + + Arguments: + inargs -- arguments to parse + + Returns: + the list of arguments remaining after all flags have been + processed + + Raises: + Usage -- if '-h' or invalid command-line args are given + """ + + try: + opts, args = getopt.getopt(inargs, "c:hl:v") + except getopt.GetoptError: + raise self.Usage(self.EXIT_ERROR) + + for o, a in opts: + if o in ("-c"): + self.setOpt("cfg_file", a) + if o in ("-h"): + raise self.Usage(self.EXIT_SUCCESS) + if o in ("-l"): + vlists = [] + if ("vlists" in self.__frobbed): + vlists = self.getOpt("vlists") + vlists.append(a) + self.setOpt("vlists", vlists) + if o in ("-v"): + self._setVerbosity(1) + + return args + + + def setOpt(self, opt, val): + """Set the given option to the provided value""" + + self.__opts[opt] = val + self.__frobbed[opt] = val + + + def verifyOptions(self): + """make sure that all given options (from command-line or config file) are + valid""" + + for opt in self.__list_opts: + if self.__opts[opt]: + self.__opts[opt] = self.__opts[opt].split() + + for opt in self.__int_opts: + if type(self.__opts[opt]) is not int: + try: + self.__opts[opt] = string.atoi(self.__opts[opt]) + except ValueError: + logging.error("Invalid value for configuration option '%s': %s" + % (opt, self.__opts[opt])) + raise + + +class Vulnerability(object): + """An object representing a vulnerability. + + A Vulnerability consists of a package name-version pattern, a + vulnerability type and a URL providing more information about the given + vulnerability. Each such attribute is represented as a string and can + trivially be accessed via the members 'pattern', 'type' and 'url'. + """ + + def __init__(self, pattern, type, url): + """Construct a Vulnerability with the given attributes.""" + + self.pattern = pattern + self.type = type + self.url = url + + + def match(self, pkg): + """Compare a given name-version pair to the object's pattern. + + This function determines if a given name-version pair matches this + object's pattern. Since a Vulnerability's pattern may contain brace + expansion or fnmatch-like expressions as well as an operation, a + simple string comparison is not sufficient. + + Arguments: + pkg -- a package name and version string, eg "package-1.2.3" + + Returns: True or False + """ + + # version comparison + # - if pkg-name and pattern are identical + # - return true + # - if pattern matches {, then + # - perform brace expansion + # for each pattern name + # - if pattern matches <=> + # - if name portion matches pkg-name portion + # - construct a LooseVersion from pkg-name + # - if version matches <=> (ie we had ">n -1): + patterns = braceExpand(self.pattern) + + for pat in patterns: + m = re.search('(?P[^<=>]+)(?P[<=>]+)(?P.*)', pat) + if m: + name = m.group('name') + cmp = m.group('cmp') + version = m.group('version') + + pname = re.sub(r'(.*)-.*', r'\1', pkg) + if pname == name: + pkgversion = LooseVersion(pkg) + m = re.search('(?P[^<=>]*)(?P[<=>]+)(?P.*)', version) + if m: + min = m.group('min') + cmp2 = m.group('cmp2') + max = m.group('max') + minversion = LooseVersion(name + "-" + min) + maxversion = LooseVersion(name + "-" + max) + return (versionCompare(pkgversion, cmp, minversion) and + versionCompare(pkgversion, cmp2, maxversion)) + else: + patternversion = LooseVersion(name + "-" + version) + return versionCompare(pkgversion, cmp, patternversion) + else: + if fnmatch(pkg, pat): + return True + + return False + +### +### Utility functions +### + +def braceExpand(input): + """expand a string possibly containing a brace expansion + + This functions expands an input string to a list of strings based on brace + expansion somewhat similar to zsh(1). It does not perform numeric range + expansion, however. + + As an example, given the input string "foo-1{,b,-bar}", this function will + return [ "foo-1", "foo-1b", "foo-1-bar" ]. + + Simply nested expansions may also be resolved recursively. Note, however, + that this is not supposed to be fully compatible with zsh(1) style + expansions -- if more complex expansions are needed, consider shelling out + to "echo echo string | zsh". + + Arguments: + input -- any string + + Returns + A list of strings, possibly containing the input string as a single + element. + """ + + m = re.search(r'(?P.*?){(?P[^{}]+)}(?P.*)', input) + if not m: + return [ input ] + + expanded = [] + term = m.group('term') + suffixes = m.group('suffixes') + rest = m.group('rest') + + # Nested zero-matches such as "foo-{,bar{this,something}}" shouldn't get the + # base term appended multiple times. Since we expand from the inside out, + # add the base term once, then remove empty item. + m = re.search(r'(?P.*){,', term) + if m: + expanded.append(m.group('base')) + term = re.sub(r'{,', r'{', term, 1) + + # suffixes can be expansion, too -- recurse + expansions = braceExpand(suffixes) + for exp in expansions: + for x in exp.split(","): + # Fully expanded suffixes can also be expansions themselves! + # Recurse! + s = braceExpand(term + x + rest) + expanded += s + + return expanded + + +def versionCompare(v1, op, v2): + """compare two versions based on the given operator + + Arguments: + v1 -- a string or Version object + op -- operator, indicating type of comparison (">", ">=", "<", "<=") + v2 -- a string or Version object + + Returns: + True -- if ( v1 op v2 ) satisfies the comparison + False -- otherwise + + ie, effectively returns "v1 op b2" + """ + + if (op == ">="): + return (v1 >= v2) + elif (op == ">"): + return (v1 > v2) + elif (op == "<"): + return (v1 < v2) + elif (op == "<="): + return (v1 <= v2) + elif (op == "=="): + return (v1 == v2) + else: + # shouldn't happen + logging.error("Unexpected operand: %s (%s %s %s)" % + (op, v1, op, v2)) + return False + +### +### A 'main' for the yvc(1) program. +### + +def doStdin(checker): + """check packages from stdin + + Arguments: + checker -- an instance of a yvc Checker + """ + + while 1: + line = sys.stdin.readline() + if not line: + break + pkgs = line.split() + for p in pkgs: + checker.checkPackage(p) + + +def main(args): + """Run the yvc(1) program. + + Arguments: + args -- command-line arguments + """ + + try: + checker = Checker() + try: + args = checker.parseOptions(args) + except checker.Usage, u: + if (u.err == checker.EXIT_ERROR): + out = sys.stderr + else: + out = sys.stdout + out.write(u.msg) + sys.exit(u.err) + # NOTREACHED + + try: + checker.parseConfig(checker.getOpt("cfg_file")) + checker.verifyOptions() + for vlist in checker.getOpt("vlists"): + checker.parseList(vlist) + except Exception, e: + logging.error(e) + sys.exit(checker.EXIT_ERROR) + # NOTREACHED + + if args: + for p in args: + if (p == "-"): + doStdin(checker) + else: + checker.checkPackage(p) + else: + doStdin(checker) + + except KeyboardInterrupt: + # catch ^C, so we don't get a "confusing" python trace + sys.exit(checker.EXIT_ERROR) diff --git a/misc/harvest_freebsd_yvc.pl b/misc/harvest_freebsd_yvc.pl new file mode 100755 index 0000000..8f4f935 --- /dev/null +++ b/misc/harvest_freebsd_yvc.pl @@ -0,0 +1,171 @@ +#! /usr/local/bin/perl -w +# +# Copyright (c) 2009,2010 Yahoo! Inc. +# +# Originally written by Joshua Moss in March 2009. +# +# This program fetches the list of known vulnerabilities in the FreeBSD +# ports collection from http://www.freebsd.org/ports/portaudit/ and +# generates a yvc(1) compatible vlist. + +use strict; +use IO::Socket; + +use constant FBSD_PORTAUDIT => "http://www.freebsd.org/ports/portaudit/"; + +# each element stores a hash which contains unique vuln id, description, +# packages (vulns) +my @VULN_INFO; + +### +### Subroutines +### + +# function : get_freebsd_vulns +# purpose : loop over the list of vulnerabilities and populate the global +# array with hashes containing an id and a description +# inputs : none +# returns : void, global @VULN_INFO has been populated + +sub get_freebsd_vulns { + + my $vuln_regex = qr/([a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})\.html\"\>\s*([^<]+)/; + + foreach my $line (get_page(FBSD_PORTAUDIT)) { + next unless ($line =~ m/$vuln_regex/); + # unique vuln id, description + push(@VULN_INFO, { id => $1, desc => $2 }); + } + + return; +} + +# function : get_freebsd_vuln_detail +# purpose : loop over all vulnerabilities and fetch their respective page, +# then parse that page and populate the 'vulns' field of this +# vulnerabilitie's hash +# inputs : none +# returns : void, populates @VULN_INFO further + +sub get_freebsd_vuln_detail { + my $affects; + + for (my $i=0; $i< scalar (@VULN_INFO); $i++) { + + my $vuln_page = FBSD_PORTAUDIT . $VULN_INFO[$i]->{'id'} . '.html'; + $affects = 0; + + foreach my $line (get_page($vuln_page)) { + if ($line =~/Affects\:/) { + $affects = 1; + next; + } + + if ($affects && $line =~ m/li>\s*([^<]+){'vulns'} }, $1); + } + + if ($affects && $line =~ m/\/ul/) { + $affects = 0; + next; + } + } + } + + return; +} + +# function : print_freebsd_vuln_yvc +# purpose : iterate over all vulnerabilities and print them in the desired +# format +# inputs : none +# returns : void, output is printed to stdout + +sub print_freebsd_vuln_yvc { + + my $desc_regex = qr/(multiple-vulnerabilities|denial-of-service|cross-site-request-forgery|remote-dos|xss|cross-site-scripting|arbitrary-code-execution|script-insertion|input-validation|directory-traversal|heap-overflow|buffer-overflow|stack-overflow|session-hijacking|command-execution|local-privilege-escalation|command-injection|information-disclosure|arbitrary-file-disclosure|arbitrary-script-execution)/; + + # reverse just to be consistent with pkgsrc vlist ordering + for (my $i=scalar(@VULN_INFO); $i>=0; $i--) { + + next unless ($VULN_INFO[$i]->{'vulns'}); + + foreach my $pkg (sort @{ $VULN_INFO[$i]->{'vulns'} } ) { + next unless ($pkg =~/[a-z]/); + + $pkg =~ s/\<\;/\/g; + #$pkg =~ s/=/-/g; + $pkg =~ s/\s+//g; + + $VULN_INFO[$i]->{'desc'} = lc( $VULN_INFO[$i]->{'desc'} ); + $VULN_INFO[$i]->{'desc'} =~ s/^\s*[a-z0-9]+\s+\-{2,}\s*//; + $VULN_INFO[$i]->{'desc'} =~ s/[^a-z0-9]/-/g; + $VULN_INFO[$i]->{'desc'} =~ s/-{2,}/-/g; + $VULN_INFO[$i]->{'desc'} =~ s/^-+//g; + $VULN_INFO[$i]->{'desc'} =~ s/-+$//g; + $VULN_INFO[$i]->{'desc'} =~ s/-vulnerability$//; + + # shorten some of these long descriptions to their core issue + if ($VULN_INFO[$i]->{'desc'} =~/$desc_regex/) { + $VULN_INFO[$i]->{'desc'} = $1; + $VULN_INFO[$i]->{'desc'} =~s/^dos$/denial-of-service/; + $VULN_INFO[$i]->{'desc'} =~s/^xss$/cross-site-scripting/; + } + + printf("%s\t%s\t%s\n", $pkg, $VULN_INFO[$i]->{'desc'}, + FBSD_PORTAUDIT . $VULN_INFO[$i]->{'id'} . '.html'); + } + } + + return; +} + +# function : get_page +# purpose : retrieve the given document and return the contents as an array +# inputs : URI +# returns : an array of lines + +sub get_page { + my ($url) = @_; + + my ($port, $host, $uri); + + if ($url =~s/^(https?):\/+([^\/]+)(\/.*)$//) { + $port = ($1 eq 'https') ? 443 : 80; + $host = $2; + $uri = $3; + } else { + return undef; + } + + my $PAGE = IO::Socket::INET->new(PeerAddr=>$host, + PeerPort=>$port, + Proto=>"tcp", + Timeout=>7) or return; + + my $timeout = 30; + print $PAGE "GET $uri HTTP/1.0\r\n", + "Host: $host\r\n\r\n"; + + alarm($timeout); + + my @page = <$PAGE>; + + return @page; +} + +### +### Main +### + +# scrape the main listing +get_freebsd_vulns(); + +# dive into each page +get_freebsd_vuln_detail(); + +# clean up and print out +print_freebsd_vuln_yvc(); + +exit(0); diff --git a/misc/redhat_oval_to_yvc.py b/misc/redhat_oval_to_yvc.py new file mode 100755 index 0000000..540b0b5 --- /dev/null +++ b/misc/redhat_oval_to_yvc.py @@ -0,0 +1,93 @@ +#!/usr/local/bin/python + +# +# Copyright (c) 2009,2010 Yahoo! Inc. +# +# Originally written by Joshua Moss in October 2009. +# +# This program reads the Open Vulnerability and Assessment Language (OVAL) +# file, available from http://www.redhat.com/security/data/oval/com.redhat.rhsa-all.xml.bz2 +# and generates a yvc(1) compatible vlist. +# + +import re +import sys +import socket +import os.path +import xml.dom.minidom +import bz2 +import urllib +import time + +# Source of the oval xml.bz2 file +oval_url = 'http://www.redhat.com/security/data/oval/com.redhat.rhsa-all.xml.bz2' + +# Destination of the oval xml.bz2 file +oval_bz2 = './com.redhat.rhsa-all.xml.bz2' + +# Usage +if len(sys.argv) != 2: + print "Usage: %s %s" % (sys.argv[0], '<4|5>') + sys.exit(1) + + +### +### Subroutines +### + + +# function : download_redhat_oval_bz2 +# purpose : fetches the xml.bz2 file from redhat and write to local dir on disk +# inputs : none +# returns : void + +def download_redhat_oval_bz2(): + if not os.path.isfile(oval_bz2): # XXX remove this if you want to download every time, rather than handle via make clean + socket.setdefaulttimeout(45) + urllib.urlretrieve(oval_url, oval_bz2) # in, out + urllib.urlcleanup() + + + +# function : print_redhat_yvc +# purpose : traverses the xml dom for vulnerability information and prints out in the desired +# format +# inputs : numeric string, appends to regex search for 'el' +# returns : void, output is printed to stdout + +def print_redhat_yvc(version = '[45]'): + + print '# Generated on ' + time.strftime("%a %b %e %H:%M:%S %Z %Y") # Wed Oct 28 12:05:05 PDT 2009 + + arch = bz2.BZ2File(oval_bz2, 'r') + document = xml.dom.minidom.parse(arch) + + apps = {} # they seem to keep their tests in chronological order, we only want the latest + definitions = document.getElementsByTagName('definition') + for d in definitions: + #description = d.getElementsByTagName('metadata')[0].getElementsByTagName('description')[0].lastChild.nodeValue # TODO parse descriptions for keywords + title = d.getElementsByTagName('metadata')[0].getElementsByTagName('title')[0].lastChild.nodeValue.split(': ')[1].replace('\n', '') + title = re.sub('[^a-zA-Z0-9]+', '-', title).rstrip('-').lower() + ref_url = d.getElementsByTagName('reference')[0].getAttribute('ref_url') + + criterions = d.getElementsByTagName('criterion') + for c in criterions: + criteria = c.getAttribute('comment').replace('\t', ' ') + + if re.search('el'+version, criteria, re.I) == None: + continue # skipping lines that don't match version + + criteria = re.sub('\d+:', '', criteria) + criteria = criteria.replace(' is earlier than ','<') + + app = criteria.split('<')[0] + + apps[app] = "%s\t%s\t%s" % (criteria, title, ref_url) + + for app in apps: + print apps[app] + + +download_redhat_oval_bz2() +print_redhat_yvc(sys.argv[1]) +sys.exit(0) diff --git a/test/Makefile b/test/Makefile new file mode 100644 index 0000000..780f04d --- /dev/null +++ b/test/Makefile @@ -0,0 +1,12 @@ +# Copyright (c) 2008 Yahoo! Inc. +# +# Originally written by Jan Schaumann in July 2008. +# +# A Makefile to run all tests in this directory. + +all: py + +py: + @for p in *.py; do \ + python $$p; \ + done diff --git a/test/test.py b/test/test.py new file mode 100755 index 0000000..d3d727c --- /dev/null +++ b/test/test.py @@ -0,0 +1,295 @@ +#! /usr/local/bin/python +# +# Copyright (c) 2008, Yahoo! Inc. +# +# Originally written by Jan Schaumann in July +# 2008. +# +# A unittest for functionality in yvc.py. + +import sys +sys.path.append("../lib/") + +from distutils.version import LooseVersion +import ConfigParser +import logging +import unittest + +import yvc + +class TestYvc(unittest.TestCase): + + def setUp(self): + self.yvc = yvc.Checker() + + + def testDefaults(self): + cfg_opts = { + "cfg_file" : "/usr/local/etc/yvc.conf", + "ignore_types" : None, + "ignore_urls" : None, + "vlists" : [], + "verbosity" : logging.WARNING + } + + for key, val in cfg_opts.iteritems(): + self.assertEqual(self.yvc.getOpt(key), val) + + + def testUsageHelp(self): + opts = [ "-h" ] + # -h triggers usage + self.assertRaises(yvc.Checker.Usage, self.yvc.parseOptions, opts) + try: + self.yvc.parseOptions(opts) + except yvc.Checker.Usage, u: + self.assertEqual(yvc.Checker.EXIT_SUCCESS, u.err) + + + def testUsageAddList(self): + opts = [ "-l", "/dev/null", "-l", "/whatever" ] + vlist = [ "/dev/null", "/whatever" ] + self.yvc.parseOptions(opts) + self.assertEqual(self.yvc.getOpt("vlists"), vlist) + + + def testUsageMissingArg(self): + # -c requires an argument + opts = [ "-c" ] + self.assertRaises(yvc.Checker.Usage, self.yvc.parseOptions, opts) + try: + self.yvc.parseOptions(opts) + except yvc.Checker.Usage, u: + self.assertEqual(yvc.Checker.EXIT_ERROR, u.err) + + + def testUsageAllValid(self): + opts = [ "-c", "/dev/null", "-l", "/dev/null", "-v" ] + # need to assert that this does NOT raise Usage + try: + self.assertRaises(yvc.Checker.Usage, self.yvc.parseOptions, opts) + except self.failureException: + pass + + def testParseConfig(self): + cfg = "../conf/yvc.conf" + opts = [ "-c", cfg ] + # parsing a correct file works + self.yvc.parseConfig(cfg) + + # parsing an empty file raises an error + self.assertRaises(ConfigParser.NoSectionError, self.yvc.parseConfig, "/dev/null") + + # passing a single vlist overrides defaults from config file + opts = [ "-c", cfg, "-l", "../yvlist" ] + self.yvc.parseOptions(opts) + self.yvc.parseConfig(cfg) + self.assertEqual([ "../yvlist" ], self.yvc.getOpt("vlists")) + + + def testSetOpts(self): + f = self.yvc.getOpt("cfg_file") + self.yvc.setOpt("cfg_file", "invalid") + self.assertEqual("invalid", self.yvc.getOpt("cfg_file")) + self.yvc.setOpt("cfg_file", f) + + + def testMakeValidV(self): + v = self.yvc.makeV("cfengine<1.5.3nb3 remote-root-shell ftp://ftp.NetBSD.org/pub/NetBSD/security/advisories/NetBSD-SA2000-013.txt.asc") + self.assertEqual("cfengine<1.5.3nb3", v.pattern) + self.assertEqual("remote-root-shell", v.type) + self.assertEqual("ftp://ftp.NetBSD.org/pub/NetBSD/security/advisories/NetBSD-SA2000-013.txt.asc", v.url) + + def testMakeCommentV(self): + v = self.yvc.makeV("# a comment of some sort") + self.assertEqual(None, v) + + + def testMakeInvalidV(self): + v = self.yvc.makeV("alinewithout anything") + self.assertEqual(None, v) + + + def testVulnerabilities(self): + v1 = yvc.Vulnerability("foo-1.2", "local-root-shell", + "http://www.nowhere.com") + self.assertEqual(True, v1.match("foo-1.2")) + self.assertEqual(False, v1.match("foo-2.1")) + + + def testBraceExpand(self): + input = "foo-1.2" + self.assertEqual([ input ], yvc.braceExpand(input)) + + + def testBraceExpandSuffixes(self): + input = "foo-1.2{,-bar,12}" + output = [ "foo-1.2", "foo-1.2-bar", "foo-1.212" ] + self.assertEqual(output, yvc.braceExpand(input)) + + + def testBraceExpandPrefixSuffixes(self): + input = "{this-,that-}foo-1.2{,-bar,12}" + output = [ "this-foo-1.2", "this-foo-1.2-bar", "this-foo-1.212", + "that-foo-1.2", "that-foo-1.2-bar", "that-foo-1.212" ] + self.assertEqual(output, yvc.braceExpand(input)) + + + def testBraceExpandNested(self): + input = "foo-1.2{,-bar{-baz,-blog}}" + output = [ "foo-1.2", "foo-1.2-bar-baz", "foo-1.2-bar-blog" ] + self.assertEqual(output, yvc.braceExpand(input)) + + + def testBraceExpandSuffixesTrailing(self): + input = "foo-1.2{,-bar,12}-bar" + output = [ "foo-1.2-bar", "foo-1.2-bar-bar", "foo-1.212-bar" ] + self.assertEqual(output, yvc.braceExpand(input)) + + + def testVersionCompareLarger(self): + s1 = LooseVersion("foo-1.2") + s2 = LooseVersion("foo-1.3") + self.assertTrue(yvc.versionCompare(s1, "<", s2)) + self.assertTrue(yvc.versionCompare(s1, "<=", s2)) + self.assertFalse(yvc.versionCompare(s1, ">", s2)) + self.assertFalse(yvc.versionCompare(s1, ">=", s2)) + + + def testVersionCompareSmaller(self): + s1 = LooseVersion("RealPlayerGold-10.0.9.809.20070726") + s2 = LooseVersion("RealPlayerGold-10.0.0.809.20070726") + self.assertTrue(yvc.versionCompare(s1, ">", s2)) + self.assertTrue(yvc.versionCompare(s1, ">=", s2)) + self.assertFalse(yvc.versionCompare(s1, "<", s2)) + self.assertFalse(yvc.versionCompare(s1, "<=", s2)) + + + def testVersionCompareEqual(self): + s1 = LooseVersion("ports/ldconfig_compat-1.0_8.1yahoo") + s2 = s1 + self.assertFalse(yvc.versionCompare(s1, ">", s2)) + self.assertTrue(yvc.versionCompare(s1, ">=", s2)) + self.assertFalse(yvc.versionCompare(s1, "<", s2)) + self.assertTrue(yvc.versionCompare(s1, "<=", s2)) + + + def testMatchSimpleSmaller(self): + v = self.yvc.makeV("cfengine<1.5.3nb3 remote-root-shell ftp://ftp.NetBSD.org/pub/NetBSD/security/advisories/NetBSD-SA2000-013.txt.asc") + self.assertTrue(v.match("cfengine-1.5.2nb2")) + self.assertTrue(v.match("cfengine-1.5")) + self.assertFalse(v.match("cfengine-1.5.3nb3")) + self.assertFalse(v.match("cfengine-1.5.3nb4")) + self.assertFalse(v.match("cfengine-1.6")) + self.assertFalse(v.match("something-1.5.3nb3")) + + + def testMatchSimpleSmallerEqual(self): + v = self.yvc.makeV("pine<=4.21 remote-root-shell ftp://ftp.FreeBSD.org/pub/FreeBSD/CERT/advisories/FreeBSD-SA-00:59.pine.asc") + self.assertTrue(v.match("pine-4.20")) + self.assertTrue(v.match("pine-4.21")) + self.assertFalse(v.match("pine-4.22")) + self.assertFalse(v.match("anything-4.21")) + + + def testMatchExact(self): + v = self.yvc.makeV("ap-php-4.0.4 remote-code-execution http://security.e-matters.de/advisories/012002.html") + self.assertTrue(v.match("ap-php-4.0.4")) + self.assertFalse(v.match("ap-php-4.0.3")) + self.assertFalse(v.match("ap-php-4.0.5")) + self.assertFalse(v.match("whatever")) + + + def testMatchSimpleRange(self): + v = self.yvc.makeV("apache-2.0.3[0-3]* remote-root-shell http://httpd.apache.org/info/security_bulletin_20020617.txt") + self.assertFalse(v.match("apache-2.0.3")) + self.assertTrue(v.match("apache-2.0.30")) + self.assertTrue(v.match("apache-2.0.31")) + self.assertTrue(v.match("apache-2.0.32")) + self.assertTrue(v.match("apache-2.0.33")) + self.assertFalse(v.match("apache-2.0.2")) + self.assertFalse(v.match("apache-2.0.299")) + self.assertFalse(v.match("apache-2.0.4")) + + + def testMatchExpansion(self): + v = self.yvc.makeV("kdenetwork-3.0.4{,nb1} remote-root-shell http://www.kde.org/info/security/advisory-20021111-2.txt") + self.assertTrue(v.match("kdenetwork-3.0.4")) + self.assertTrue(v.match("kdenetwork-3.0.4nb1")) + self.assertFalse(v.match("kdenetwork-3.0.4nb2")) + self.assertFalse(v.match("kdenetwork-3.0.3nb2")) + + + def testMatchExpansionSmaller(self): + v = self.yvc.makeV("mozilla{,-bin,-gtk2,-gtk2-bin}<1.7.10 http-frame-spoof http://secunia.com/advisories/15601/") + self.assertTrue(v.match("mozilla-1.6")) + self.assertTrue(v.match("mozilla-bin-1.7.9")) + self.assertTrue(v.match("mozilla-gtk2-1.7")) + self.assertTrue(v.match("mozilla-gtk2-bin-1.7")) + + + def testMatchSimpleGreater(self): + v = self.yvc.makeV("gnupg-devel>=1.9.23 buffer-overflow http://lists.gnupg.org/pipermail/gnupg-announce/2006q4/000241.html") + self.assertTrue(v.match("gnupg-devel-1.9.23")) + self.assertTrue(v.match("gnupg-devel-1.9.23.1")) + self.assertTrue(v.match("gnupg-devel-1.9.24")) + self.assertFalse(v.match("gnupg-devel-1.9.22")) + self.assertFalse(v.match("gnupg-devel-1.9.22.100")) + + + def testMatchSubstring(self): + v = self.yvc.makeV("dia>=0.87 arbitrary-code-execution http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2006-1550") + self.assertFalse(v.match("dialog-1.0.20050911")) + self.assertTrue(v.match("dia-1.0.20050911")) + + + def testMatchExpandRange(self): + v = self.yvc.makeV("acroread{,5,7}-[0-9]* multiple-unspecified http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2008-0655") + self.assertTrue(v.match("acroread5-5.10nb1")) + + + def testMatchGreaterEqualAndSmaller(self): + v = self.yvc.makeV("php>=5<5.1.0 inject-smtp-headers http://cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2005-3883") + self.assertTrue(v.match("php-5.0.8")) + self.assertTrue(v.match("php-5")) + self.assertTrue(v.match("php-5.0.999999")) + self.assertFalse(v.match("php-4.99")) + self.assertFalse(v.match("php-5.1.0")) + + + def testMatchGreaterEqualAndSmallerEqual(self): + v = self.yvc.makeV("php>=5.1<=5.3.2 inject-smtp-headers http://cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2005-3883") + self.assertTrue(v.match("php-5.1")) + self.assertTrue(v.match("php-5.3.2")) + self.assertTrue(v.match("php-5.1.1")) + self.assertTrue(v.match("php-5.3.0")) + self.assertFalse(v.match("php-4.99")) + self.assertFalse(v.match("php-5.0.1")) + self.assertFalse(v.match("php-5.3.3")) + self.assertFalse(v.match("php-6")) + + + def testMatchPatchLevel(self): + v = self.yvc.makeV("python24<2.4nb4 remote-code-execution http://www.python.org/security/PSF-2005-001/") + # XXX: this currently fails since LooseVersion doesn't handle + # patchlevel as we'd expect + #self.assertFalse(v.match("python24-2.4.3nb3")) + #self.assertFalse(v.match("python24-2.4.1")) + self.assertTrue(v.match("python24-2.4")) + self.assertTrue(v.match("python24-2.4nb3")) + self.assertTrue(v.match("python24-2.4nb1")) + + + def testMatchPatchLevelReverse(self): + v = self.yvc.makeV("ruby18-base<1.8.6.114 access-validation-bypass http://preview.ruby-lang.org/en/news/2008/03/03/webrick-file-access-vulnerability/") + # XXX: this currently fails since LooseVersion doesn't handle + # patchlevel as we'd expect + #self.assertTrue(v.match("ruby18-base-1.8.6nb1")) + self.assertTrue(v.match("ruby18-base-1.8.6.113")) + self.assertFalse(v.match("ruby18-base-1.8.6.114")) + self.assertFalse(v.match("ruby18-base-1.8.6.115")) + self.assertFalse(v.match("ruby18-base-1.8.7")) + + +if __name__ == '__main__': + unittest.main()