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 0000000..66b445b Binary files /dev/null and b/doc/ContributionLicenseAgreementYahoo.pdf differ 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()