From e860c9a5235572599a0ada35988ce029e8012644 Mon Sep 17 00:00:00 2001 From: Ikey Doherty Date: Tue, 24 Mar 2015 17:25:24 +0000 Subject: [PATCH] Initial import Signed-off-by: Ikey Doherty --- .gitignore | 26 + AUTHORS | 2 + HACKING | 90 +++ LICENSE | 339 ++++++++++ Makefile.am | 14 + README.md | 62 ++ TODO | 6 + autogen.sh | 14 + common.mk | 13 + configure.ac | 49 ++ data/Makefile.am | 20 + data/bottom.template | 5 + data/cve-check-tool.conf | 49 ++ data/package.template | 7 + data/top.template | 132 ++++ docs/Makefile.am | 5 + docs/cve-check-tool.1 | 86 +++ src/Makefile.am | 46 ++ src/cli.c | 66 ++ src/cli.h | 34 + src/csv.c | 93 +++ src/csv.h | 34 + src/cve-check-tool.h | 89 +++ src/cve-string.c | 80 +++ src/cve-string.h | 71 +++ src/eopkg.c | 140 +++++ src/eopkg.h | 28 + src/fetch.c | 139 +++++ src/fetch.h | 39 ++ src/html.c | 181 ++++++ src/html.h | 21 + src/main.c | 742 ++++++++++++++++++++++ src/plugins/jira/Makefile.am | 22 + src/plugins/jira/jira.c | 926 ++++++++++++++++++++++++++++ src/plugins/jira/jira.cfg | 49 ++ src/plugins/jira/jira.h | 158 +++++ src/plugins/jira/jira_test_client.c | 156 +++++ src/plugins/jira/run_tests.sh | 15 + src/rpm.c | 216 +++++++ src/rpm.h | 42 ++ src/template.c | 94 +++ src/template.h | 27 + src/util.c | 252 ++++++++ src/util.h | 208 +++++++ tests/Makefile.am | 25 + tests/check-core.c | 108 ++++ 46 files changed, 5020 insertions(+) create mode 100644 .gitignore create mode 100644 AUTHORS create mode 100644 HACKING create mode 100644 LICENSE create mode 100644 Makefile.am create mode 100644 README.md create mode 100644 TODO create mode 100755 autogen.sh create mode 100644 common.mk create mode 100644 configure.ac create mode 100644 data/Makefile.am create mode 100644 data/bottom.template create mode 100644 data/cve-check-tool.conf create mode 100644 data/package.template create mode 100644 data/top.template create mode 100644 docs/Makefile.am create mode 100644 docs/cve-check-tool.1 create mode 100644 src/Makefile.am create mode 100644 src/cli.c create mode 100644 src/cli.h create mode 100644 src/csv.c create mode 100644 src/csv.h create mode 100644 src/cve-check-tool.h create mode 100644 src/cve-string.c create mode 100644 src/cve-string.h create mode 100644 src/eopkg.c create mode 100644 src/eopkg.h create mode 100644 src/fetch.c create mode 100644 src/fetch.h create mode 100644 src/html.c create mode 100644 src/html.h create mode 100644 src/main.c create mode 100644 src/plugins/jira/Makefile.am create mode 100644 src/plugins/jira/jira.c create mode 100644 src/plugins/jira/jira.cfg create mode 100644 src/plugins/jira/jira.h create mode 100644 src/plugins/jira/jira_test_client.c create mode 100755 src/plugins/jira/run_tests.sh create mode 100644 src/rpm.c create mode 100644 src/rpm.h create mode 100644 src/template.c create mode 100644 src/template.h create mode 100644 src/util.c create mode 100644 src/util.h create mode 100644 tests/Makefile.am create mode 100644 tests/check-core.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b08092d --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +Makefile +Makefile.in +m4/ +config.* +ltmain.sh +libtool +install-sh +stamp-* +depcomp +missing +aclocal.m4 +.deps +compile +configure +*.o +autom4te.cache +cve-check-tool +report.html +test-driver + +# Checks +check_core +check_*.log +check_*.trs +test-*.log +.dirstamp diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..01d9a2a --- /dev/null +++ b/AUTHORS @@ -0,0 +1,2 @@ +Ikey Doherty +John L. Whiteman diff --git a/HACKING b/HACKING new file mode 100644 index 0000000..d3aa28b --- /dev/null +++ b/HACKING @@ -0,0 +1,90 @@ +Hacking cve-check-tool +---------------------- + +Indentation is strictly 8 spaces (set tab stops to 8 spaces) - No tabs +No spaces between function name and parentheses. Only use space after +"if (", etc. + +Curly braces must be on the same line (i.e. expressions) unless it +is the function declaration: + +Acceptable: + int main(int argc, char **argv) + { + if (someThingOrOther) { + // Do something + } + return 0; + } + +Unacceptable: + int main(int argc, char **argv) { + + if(someThingOrOther) + { + // Do something + } + return 0; + } + +When appropriate remember to declare your function first! It helps when +looking back through the file. + +Use consistent pointers! "*" should prefix your variable name. Also ensure +your pointers (where appropriate) are not left uninitialized, resulting +in broken g_free() calls. + +Acceptable: + char *something = NULL; + doFunc(&something); + +Unacceptable: + char* something; + doFunc(&someThing); + +Minimise your use of "goto"'s, and test every code path is reached. Also +ensure *every* if/else, etc, even if single line, is wrapped in curly braces. + +Memory management: +------------------ +cve-check-tool prefers a scope-based approach to memory management, +employing a RAII-like system for allocations. Where possible, avoid +explicit free's and reuse of unrelated variables. + +util.h defines a number of autofree()-ready types. These are implemented +using __attribute__ ((cleanup(x))), available in GCC and Clang. Thus, +MSVC (and potentially other compilers) are not supported. + +An autofree variable is declared using the autofree() macro, which is +primarily provided for syntatical sugar. Here is an example of a +variable that is automatically reaped/freed when it goes out of scope: + + autofree(GError) *error = NULL; + +Remember that these *are* scope sensitive, so the following would result +in undefined behaviour: + + gchar *somestr = NULL; + { + autofree(gchar) *str = g_strdup_printf("Scoped string\n"); + somestr = str; + } + printf("%s: %d\n", somestr, strlen(somestr)); + +At this point, 'str' has been freed, and somestr still points to the +memory that has now been freed. + +As a rule of thumb, if you find yourself in an instance where you have +used an explicit free/unref in a fashion that could be automated, you +should define the cleanup function in util.h (see DEF_AUTOFREE) + +Pull Requests/commiting: +------------------------ +Commits should clearly define the purpose in less than 80 columns in +the first line. Futher expansion, if needed, should be provided in a +following paragraph, separated by one blank line. + +N.B This hacking document is a port of Evolve OS C Coding Style. +For futher details (including *non-glib*) functions, see: + + * https://evolve-os.com/wiki/Development/CodingStyleC diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d7f1051 --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ +GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..2dbe60a --- /dev/null +++ b/Makefile.am @@ -0,0 +1,14 @@ +ACLOCAL_AMFLAGS = -I m4 + +EXTRA_DIST = ${top_srcdir}/README.md \ + ${top_srcdir}/LICENSE \ + ${top_srcdir}/common.mk + +NULL = +CLEANFILES = + +SUBDIRS = \ + data \ + src \ + tests \ + docs diff --git a/README.md b/README.md new file mode 100644 index 0000000..2647a1f --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +cve-check-tool +============== + +cve-check-tool, as its name suggests, is a tool for checking known +(public) CVEs. The tool will identify potentially vunlnerable software +packages within Linux distributions through version matching. Where +possible it will also seek to determine (through a distribution +implemention) if a vulnerability has been addressed by way of a patch. + +CVEs are only ever *potential* - due to the various policies of various +distributions, and indeed semantics in versioning within various projects, +it is expected that the tool may generate false positives. + +The tool is designed to integrate with a locally cached copy of the +National Vulnerability Database, which should be updated every 3-4 +hours. Correctly integrated within the workflow of a distribution, +and indeed with the correct bug report tool, this yields a minimum +4 hour turnaround on all disclosed CVEs (non-embargoed) + +Data Usage +---------- +cve-check-tool downloads the NVD in its entirety, from 2002 until the +current moment. The decompressed XML database is in excess of 550MB, +so this should be taken into account before running the tool. From then +on, only the *changed* database segments are fetched. Therefore it is +advisable to use cve-check-tool on a machine that has sufficient space +and internet connection. + +On a fairly modern machine, it should only take around 10 seconds to +consume the databases. Note however that when the tool runs, it will +use a lot of resources to ensure it is fast (it needs to go through over +7 million lines of XML, for one.) + +CLI usage: +---------- + +Most common usage, automatically determine package type and scan for the +packages in the given package list file: + + cve-check-tool ../packages + +Recurse a directory structure, with the predetermined type of eopkg: + + cve-check-tool -t eopkg . + +Check a single RPM source package, ignoring patched issues: + + cve-check-tool -n readline.spec + +Flags can be combined, check `-h` for details. An example to recurse all +directories, finding .spec RPM files, and ignoring patched issues: + + cve-check-tool -n -t rpm . + + +License +-------- + +cve-check-tool is available under the terms of the GNU General Public License, +Version 2. Please check the LICENSE file for further details. + +Copyright (C) 2015 Intel Corporation diff --git a/TODO b/TODO new file mode 100644 index 0000000..fded8f1 --- /dev/null +++ b/TODO @@ -0,0 +1,6 @@ +TODO List +--------- + + * Abstract distro specifics further + * Fully integrate automation for various BTS, i.e. Jira + * Add coverage and expand unit tests diff --git a/autogen.sh b/autogen.sh new file mode 100755 index 0000000..ebc0ee1 --- /dev/null +++ b/autogen.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +set -e + +autoreconf --force --install --symlink --warnings=all + +args="\ +--sysconfdir=/etc \ +--localstatedir=/var \ +--prefix=/usr \ +--enable-silent-rules" + +./configure CFLAGS='-g -O0' $args "$@" +make clean diff --git a/common.mk b/common.mk new file mode 100644 index 0000000..b68b0a4 --- /dev/null +++ b/common.mk @@ -0,0 +1,13 @@ +AM_CFLAGS = -fstack-protector -Wall -pedantic \ + -Wstrict-prototypes -Wundef -fno-common \ + -Werror-implicit-function-declaration \ + -Wformat -Wformat-security -Werror=format-security \ + -Wno-conversion -Wunused-variable -Wunreachable-code \ + -std=c99 -Werror + +AM_CPPFLAGS = \ + -I $(top_srcdir)/src \ + -DDATA_DIRECTORY=\"$(datadir)/cve-check-tool\" \ + -DSITE_PATH=\"$(sysconfdir)\" -DDEFAULT_PATH=\"$(datadir)/defaults/cve-check-tool\" + +AUTOMAKE_OPTIONS = color-tests parallel-tests diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..5105f01 --- /dev/null +++ b/configure.ac @@ -0,0 +1,49 @@ +AC_INIT([cve-check-tool], 1, [michael.i.doherty@intel.com], [cve-check-tool], [https://github.com/ikeydoherty/cve-check-tool]) +AM_INIT_AUTOMAKE([-Wno-portability no-dist-gzip dist-xz foreign subdir-objects]) +AC_PROG_CC +AC_PROG_CC_STDC +LT_PREREQ(2.2) +AC_CONFIG_HEADERS([config.h]) +AC_PREFIX_DEFAULT(/usr/local) +AM_SILENT_RULES([yes]) +LT_INIT([disable-static]) + +# Package requirements +PKG_CHECK_MODULES([GLIB], [glib-2.0 >= 2.36.0]) +PKG_CHECK_MODULES([GIO], [gio-2.0 >= 2.36.0]) +PKG_CHECK_MODULES([LIBXML2], [libxml-2.0 >= 2.9.1]) +PKG_CHECK_MODULES([LIBCURL], [libcurl >= 7.29.0]) + +# Jira plugin +PKG_CHECK_MODULES([LIBJSON_GLIB], [json-glib-1.0 >= 0.16.0]) +PKG_CHECK_MODULES([LIBGOBJECT], [gobject-2.0 >= 2.0.0]) + +# Unit tests +PKG_CHECK_MODULES([CHECK], [check >= 0.9]) + +AC_CONFIG_MACRO_DIR([m4]) + + +AC_CONFIG_FILES([Makefile + data/Makefile + docs/Makefile + tests/Makefile + src/Makefile]) + +AC_OUTPUT + +AC_MSG_RESULT([ + cve-check-tool $VERSION + ======== + + prefix: ${prefix} + libdir: ${libdir} + sysconfdir: ${sysconfdir} + exec_prefix: ${exec_prefix} + bindir: ${bindir} + datarootdir: ${datarootdir} + + compiler: ${CC} + cflags: ${CFLAGS} + ldflags: ${LDFLAGS} +]) diff --git a/data/Makefile.am b/data/Makefile.am new file mode 100644 index 0000000..87d3731 --- /dev/null +++ b/data/Makefile.am @@ -0,0 +1,20 @@ +TEMPLATE_FILES = \ + bottom.template \ + package.template \ + top.template + +DEFAULT_FILES = \ + cve-check-tool.conf + +EXTRA_DIST = \ + $(TEMPLATE_FILES) \ + $(DEFAULT_FILES) + +templatedir=$(datadir)/cve-check-tool/ +template_DATA = $(TEMPLATE_FILES) + +defaultdir=$(datadir)/defaults/cve-check-tool +default_DATA = $(DEFAULT_FILES) + +install-data-hook: + chmod 700 $(DESTDIR)$(defaultdir)/cve-check-tool.conf diff --git a/data/bottom.template b/data/bottom.template new file mode 100644 index 0000000..3b58a2d --- /dev/null +++ b/data/bottom.template @@ -0,0 +1,5 @@ + + + + + diff --git a/data/cve-check-tool.conf b/data/cve-check-tool.conf new file mode 100644 index 0000000..04f95cb --- /dev/null +++ b/data/cve-check-tool.conf @@ -0,0 +1,49 @@ +###################################################################### +# Use this section to define your global JIRA configuration parameters +###################################################################### +[JIRA] +# Sets the JIRA server's REST API URL +# e.g., https:///rest/api/2 +url= + +# Sets the JIRA login user name +user= + +# Sets the JIRA login user name's password if any +password= + +# Sets the timeout in seconds in case server hangs. 0 means wait forver +timeout_secs=60 + +# Sets to true to show more output, good for debugging +verbose=false + +###################################################################### +# Use this section to define the default fields and values for adding +# a new JIRA issue. Make sure that they exist with proper permissions. +# You can add if required. If your JIRA schema is very complicated +# with many custom fields and options then consider using template instead. +# +# =value # For simple fields like summary and description +# .=value # For fields that have attributes, issuetype +# .[] # For fields that arrays of attributes, components +###################################################################### +[JIRA-New-Issue] +components.[name]=Security +description=This is CVE bug +issuetype.name=Bug +project.key=CLEAR +assignee.name= +#priority.name=P2 +#customfield_#####.value=Normal + +###################################################################### +# Use this section to define your JIRA search criteria +###################################################################### +[JIRA-Search-Issues] + +# Literal JIRA jql search criteria +jql=project=CLEAR AND component=Security + +# Set search token for summary fields. Globbing with wildcards ok. +search_filter=CVE-* diff --git a/data/package.template b/data/package.template new file mode 100644 index 0000000..34f857d --- /dev/null +++ b/data/package.template @@ -0,0 +1,7 @@ + + {{CVEID}} + {{PACKAGE_NAME}} + {{DESC}} + {{SCORE}} + {{STATUS_STRING}} + diff --git a/data/top.template b/data/top.template new file mode 100644 index 0000000..a886a45 --- /dev/null +++ b/data/top.template @@ -0,0 +1,132 @@ + + +{{AFFECTED_STRING}} + + + +

{{AFFECTED_STRING}}

+
+ +
+ + + + + + + + + + + diff --git a/docs/Makefile.am b/docs/Makefile.am new file mode 100644 index 0000000..43256de --- /dev/null +++ b/docs/Makefile.am @@ -0,0 +1,5 @@ +-include $(top_srcdir)/common.mk + + +dist_man_MANS = \ + cve-check-tool.1 diff --git a/docs/cve-check-tool.1 b/docs/cve-check-tool.1 new file mode 100644 index 0000000..5d3c3d7 --- /dev/null +++ b/docs/cve-check-tool.1 @@ -0,0 +1,86 @@ +.TH "CVE\-CHECK\-TOOL" "1" "" "cve\-check\-tool 1" "cve\-check\-tool" + +.SH "NAME" +cve\-check\-tool \- Check distribution source packages for CVEs + +.SH "SYNOPSIS" +.HP \w'\fBcve\-check\-tool\fR\ 'u +\fBcve\-check\-tool\fR [OPTIONS...] FILE + +.SH "DESCRIPTION" +.PP +\fBcve\-check\-tool\fR +is used to check for Common Vulnerabilities and Exposures (CVE) in +Linux distribution source packages. +You may pass the path of a source package directly, or a path to file +which lists all package paths. In both instances, autodetection is used +on the package type. Recursion matching is only supported when you have +explicitly set \fB\-\-type\fR. + +.SH "OPTIONS" +.PP +The following options are understood: +.PP +\fB\-h\fR, \fB\-\-help\fR +.RS 4 +Prints a help message\&. +.RE +.PP +\fB\-n\fR, \fB\-\-not\-patched\fR +.RS 4 +Suppress reports for patched packages\&. +.RE +.PP +\fB\-t\fR, \fB\-\-type\fR TYPE +.RS 4 +Force the type of package, ignoring detection\&. +Available types can be determine by using this option without +any parameters. +Note that forcing a package type will enable recursive searching for +source packages, by passing a directory as the argument to \fBcve\-check\-tool\fR +.RE +.PP +\fB\-s\fR, \fB\-\-srpm\-dir\fR +.RS 4 +Set a source rpm directory to use with packages files. \fBcve\-check\-tool\fR will +expect a tab delimited input file, with the name, version and release on each line. +Source rpm files are required to exist when using this option. +.RE +.PP +\fB\-m\fR, \fB\-\-modified\fR DATE +.RS 4 +Set the modification date for which any report should be ignored, if it +falls on or after this date. Full date formats including the time are +supported. +.RE +.PP +\fB\-N\fR, \fB\-\-no\-html\fR +.RS 4 +Suppress generation of HTML report\&. +.RE +.PP +\fB\-c\fR, \fB\-\-csv\fR +.RS 4 +Enter silent mode and only produce CSV output\&. +.RE +.PP +\fB\-v\fR, \fB\-\-version\fR +.RS 4 +Print the version of \fBcve\-check\-tool\fR and exit\&. +.RE + +.SH "EXIT STATUS" +.PP +On success, 0 is returned, a non\-zero failure code otherwise\& + +.SH "COPYRIGHT" +.PP +Copyright 2015 Intel Corporation\&. License: Creative Commons +Attribution\-ShareAlike 3.0 Unported\s-2\u[1]\d\s+2\&. + +.SH "NOTES" +.IP " 1." 4 +Creative Commons Attribution\-ShareAlike 3.0 Unported +.RS 4 +\%http://creativecommons.org/licenses/by-sa/3.0/ +.RE diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..e3ac88b --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,46 @@ +-include $(top_srcdir)/common.mk + +bin_PROGRAMS = cve-check-tool + +cve_check_tool_SOURCES = \ + main.c \ + cve-check-tool.h \ + util.c \ + util.h \ + eopkg.c \ + eopkg.h \ + template.c \ + template.h \ + rpm.c \ + rpm.h \ + html.c \ + html.h \ + cli.c \ + cli.h \ + csv.c \ + csv.h \ + fetch.c \ + fetch.h \ + cve-string.h \ + cve-string.c \ + plugins/jira/jira.c \ + plugins/jira/jira.h + +cve_check_tool_CFLAGS = \ + $(GLIB_CFLAGS) \ + $(GIO_CFLAGS) \ + $(LIBXML2_CFLAGS) \ + $(LIBCURL_CFLAGS) \ + $(AM_CFLAGS) \ + $(LIBJSON_GLIB_CFLAGS) \ + $(LIBGOBJECT_CFLAGS) + + +cve_check_tool_LDADD = \ + $(GLIB_LIBS) \ + $(GIO_LIBS) \ + $(LIBXML2_LIBS) \ + $(LIBCURL_LIBS) \ + $(LIBJSON_GLIB_LIBS) \ + $(LIBGOBJECT_LIBS) + diff --git a/src/cli.c b/src/cli.c new file mode 100644 index 0000000..9410009 --- /dev/null +++ b/src/cli.c @@ -0,0 +1,66 @@ +/* + * cli.c - cve-check-tool helpers + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#define _GNU_SOURCE + +#include + +#include "config.h" +#include "util.h" +#include "cve-check-tool.h" +#include "cli.h" + +void cli_write_report(CveCheckTool *self) +{ + GHashTableIter iter; + gchar *key = NULL; + struct source_package_t *v = NULL; + GList *c = NULL, *t = NULL; + struct cve_entry_t *entry = NULL; + + g_hash_table_iter_init(&iter, self->db); + while (g_hash_table_iter_next(&iter, (void**)&key, (void**)&v)) { + if (!v->issues && !v->patched && !self->show_unaffected) { + continue; + } + if (!v->issues && self->hide_patched) { + continue; + } + printf("%s %s (%u patched, %u issues)\n"C_WHITE"------------"C_RESET"\n", key, (char*)v->version, g_list_length(v->patched), g_list_length(v->issues)); + for (c = v->issues; c; c = c->next) { + entry = g_hash_table_lookup(self->cdb, (gchar*)c->data); + if (self->modified > 0 && entry->modified > self->modified) { + continue; + } + printf(" * "C_RED"%s"C_RESET" : %s\n\n", (char*)c->data, entry->summary); + /* Print links.. */ + bool p = false; + for (t = entry->uris; t; t = t->next) { + printf(" - %s\n", (char*)t->data); + p = true; + } + if (p) { + printf("\n"); + } + } + if (!self->hide_patched) { + for (c = v->patched; c; c = c->next) { + entry = g_hash_table_lookup(self->cdb, (gchar*)c->data); + if (self->modified > 0 && entry->modified > self->modified) { + continue; + } + printf(" * "C_BLUE"%s [PATCHED]"C_RESET" : %s\n\n", (char*)c->data, entry->summary); + } + } + printf("\n"); + } +} + diff --git a/src/cli.h b/src/cli.h new file mode 100644 index 0000000..acea352 --- /dev/null +++ b/src/cli.h @@ -0,0 +1,34 @@ +/* + * cli.h - cve-check-tool helpers + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#define _GNU_SOURCE + +#include "config.h" +#include "util.h" +#include "cve-check-tool.h" + +#define COL "\x1B[" +#define COLOR(x) COL"3"#x"m" + +#define C_RESET "\033[0m" +#define C_RED COLOR(1) +#define C_GREEN COLOR(2) +#define C_YELLOW COLOR(3) +#define C_BLUE COLOR(4) +#define C_MAGENTA COLOR(5) +#define C_CYAN COLOR(6) +#define C_WHITE COLOR(7) + + +/** + * Write the report to stdout + */ +void cli_write_report(CveCheckTool *self); diff --git a/src/csv.c b/src/csv.c new file mode 100644 index 0000000..c088fed --- /dev/null +++ b/src/csv.c @@ -0,0 +1,93 @@ +/* + * csv.c - CSV output + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#define _GNU_SOURCE + +#include + +#include "config.h" +#include "util.h" +#include "cve-check-tool.h" +#include "csv.h" + +static inline bool filter_item(CveCheckTool *self, GList *item) +{ + struct cve_entry_t *c_entry = g_hash_table_lookup(self->cdb, (gchar*)item->data); + if (self->modified > 0 && c_entry->modified > self->modified) { + return true; + } + return false; +} + +gchar *list_as_string(CveCheckTool *self, GList *list) +{ + GList *it = NULL; + gchar *ret = NULL; + if (!list) { + return NULL; + } + + if (!filter_item(self, list)) { + ret = g_strdup_printf("%s", (gchar*)list->data); + } + + if (!list->next) { + return ret; + } + + for (it = list->next; it->next; it = it->next) { + if (filter_item(self, it)) { + continue; + } + gchar *next = NULL; + next = g_strdup_printf("%s %s", ret, (gchar*)it->data); + if (ret) { + g_free(ret); + } + ret = next; + } + return ret; +} + +void csv_write_report(CveCheckTool *self) +{ + GHashTableIter iter; + gchar *key = NULL; + struct source_package_t *v = NULL; + + /* package,version,unpatched CVE numbers space delimited,patched CVE numbers space delimited */ + g_hash_table_iter_init(&iter, self->db); + while (g_hash_table_iter_next(&iter, (void**)&key, (void**)&v)) { + autofree(gchar) *issues = NULL; + autofree(gchar) *patched = NULL; + gchar *is = NULL, *pa = NULL; + + if (!v->issues && !v->patched && !self->show_unaffected) { + continue; + } + if (!v->issues && self->hide_patched) { + continue; + } + is = issues = list_as_string(self, v->issues); + if (!is) { + is = ""; + } + pa = patched = list_as_string(self, v->patched); + if (!pa) { + pa = ""; + } + if (!self->show_unaffected && !issues && !patched) { + continue; + } + printf("%s,%s,%s,%s\n", key, (char*)v->version, is, pa); + } +} + diff --git a/src/csv.h b/src/csv.h new file mode 100644 index 0000000..f68a726 --- /dev/null +++ b/src/csv.h @@ -0,0 +1,34 @@ +/* + * csv.h - CSV output + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#define _GNU_SOURCE + +#include "config.h" +#include "util.h" +#include "cve-check-tool.h" + +#define COL "\x1B[" +#define COLOR(x) COL"3"#x"m" + +#define C_RESET "\033[0m" +#define C_RED COLOR(1) +#define C_GREEN COLOR(2) +#define C_YELLOW COLOR(3) +#define C_BLUE COLOR(4) +#define C_MAGENTA COLOR(5) +#define C_CYAN COLOR(6) +#define C_WHITE COLOR(7) + + +/** + * Write the report to stdout + */ +void csv_write_report(CveCheckTool *self); diff --git a/src/cve-check-tool.h b/src/cve-check-tool.h new file mode 100644 index 0000000..f608125 --- /dev/null +++ b/src/cve-check-tool.h @@ -0,0 +1,89 @@ +/* + * cve-check-tool.h + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#pragma once + +#include +#include + +/** + * Maps vulnerabilities into a consumable format + */ +struct vulnerability_t { + gchar *product; /**examine function, and add the parsed package into the + * current list. + * + * @param path Full legal path to the source package + */ +void cve_add_package(const char *path); diff --git a/src/cve-string.c b/src/cve-string.c new file mode 100644 index 0000000..61d9959 --- /dev/null +++ b/src/cve-string.c @@ -0,0 +1,80 @@ +/* + * cve-string.c - string management + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#define _GNU_SOURCE + +#include +#include +#include +#include + +#include "cve-string.h" + +cve_string *cve_string_dup(const char *str) +{ + struct cve_string_t *st = calloc(1, sizeof(struct cve_string_t)); + if (!st) { + return NULL; + } + st->len = asprintf(&st->str, "%s", str); + if (st->len < 0 || !st->str) { + free(st); + return NULL; + } + return st; +} + +cve_string *cve_string_dup_printf(const char *ptn, ...) +{ + if (!ptn) { + return NULL; + } + + struct cve_string_t *st = calloc(1, sizeof(struct cve_string_t)); + if (!st) { + return NULL; + } + + va_list va; + va_start(va, ptn); + + st->len = vasprintf(&st->str, ptn, va); + if (st->len < 0 || !st->str) { + free(st); + st = NULL; + goto end; + } +end: + va_end(va); + + return st; +} + +bool cve_string_cat(cve_string *s, const char *append) +{ + char *p = NULL; + int len = 0; + + if (!s || !append) { + return false; + } + if (!s->str) { + return false; + } + len = asprintf(&p, "%s%s", s->str, append); + if (!p || len < s->len) { + return false; + } + free(s->str); + s->str = p; + s->len = len; + return true; +} diff --git a/src/cve-string.h b/src/cve-string.h new file mode 100644 index 0000000..59f9fd1 --- /dev/null +++ b/src/cve-string.h @@ -0,0 +1,71 @@ +/* + * cve-string.h - string management + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#pragma once + +#define _GNU_SOURCE + +#include +#include + +/** + * Safely represent and store a buffer as a string + */ +typedef struct cve_string_t { + char *str; /**str) { + free(str->str); + } + free(str); +} + +/** + * Append the contents of 'append' into the given cve_string + * + * @param str Pointer to a valid cve_string + * @param append Text to append into the cve_string + * + * @return a boolean value indicating success + */ +bool cve_string_cat(cve_string *str, const char *append); diff --git a/src/eopkg.c b/src/eopkg.c new file mode 100644 index 0000000..f08b143 --- /dev/null +++ b/src/eopkg.c @@ -0,0 +1,140 @@ +/* + * eopkg.c - Evolve OS specific + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#define _GNU_SOURCE +#include +#include + +#include "eopkg.h" +#include "util.h" + +#define PATCH_PREFIX "files/security/" + +struct source_package_t *eopkg_inspect_pspec(const char *filename) +{ + xmlDocPtr doc = NULL; + xmlNodePtr root = NULL; + xmlNodePtr nxt = NULL; + xmlNodePtr child = NULL; + xmlChar *tmp = NULL; + struct source_package_t *t = NULL; + + xmlChar *source_name = NULL; + int release = -1; + xmlChar *version = NULL; + + doc = xmlReadFile(filename, NULL, 0); + if (!doc) { + return NULL; + } + + root = xmlDocGetRootElement(doc); + if (!root) { + goto clean; + } + + if (!xmlStrEqual(root->name, BAD_CAST "PISI")) { + printf("Invalid root node\n"); + goto clean; + } + + for (nxt = root->children; nxt; nxt = nxt->next) { + if (nxt->type != XML_ELEMENT_NODE) { + continue; + } + if (xmlStrEqual(nxt->name, BAD_CAST "Source")) { + /* Grab child with "Source" name */ + for (child = nxt->children; child; child = child->next) { + if (!(child->type == XML_ELEMENT_NODE && xmlStrEqual(child->name, BAD_CAST "Name"))) { + continue; + } + if (!child->children) { + /* bail */ + break; + } + if (child->children->type != XML_TEXT_NODE) { + /* bail */ + break; + } + source_name = child->children->content; + break; + } + } else if (xmlStrEqual(nxt->name, BAD_CAST "History")) { + for (child = nxt->children; child; child = child->next) { + if (!(child->type == XML_ELEMENT_NODE && xmlStrEqual(child->name, BAD_CAST "Update"))) { + continue; + } + tmp = xmlGetProp(child, BAD_CAST "release"); + if (!tmp) { + g_warning("Missing release property"); + continue; + } + int t_release = atoi((const char*)tmp); + if (t_release > release) { + release = t_release; + if (version) { + version = NULL; + } + for (xmlNodePtr sub = child->children; sub; sub = sub->next) { + if (!(sub->type == XML_ELEMENT_NODE && xmlStrEqual(sub->name, BAD_CAST "Version"))) { + continue; + } + if (sub->children && sub->children->type == XML_TEXT_NODE) { + version = sub->children->content; + } + break; + } + } + xmlFree(tmp); + } + } + } + + if (!version || !source_name) { + goto clean; + } + + t = calloc(1, sizeof(struct source_package_t)); + if (!t) { + goto clean; + } + t->name = xmlStrdup(source_name); + t->version = xmlStrdup(version); + t->release = release; + t->path = g_path_get_dirname(filename); + t->xml = true; /* Ensure xmlFree is used */ + +clean: + xmlFreeDoc(doc); + return t; +} + +bool eopkg_is_patched(struct source_package_t *pkg, char *id) +{ + bool ret = false; + /* Determine if its patched. */ + autofree(gchar) *pnamet = g_ascii_strdown((gchar*)id, -1); + autofree(gchar) *pname = g_strdup_printf("%s.patch", pnamet); + autofree(gchar) *tpath = g_build_filename(G_DIR_SEPARATOR_S, pkg->path, PATCH_PREFIX, pname, NULL); + + ret = g_file_test(tpath, G_FILE_TEST_EXISTS); + return ret; +} + +bool eopkg_is_package(const char *filename) +{ + return g_str_has_suffix((const gchar*)filename, "pspec.xml") || g_str_has_prefix((const gchar*)filename, "pspec_x86_64.xml"); +} + +void eopkg_locate_sources(const char *directory) +{ + find_sources(directory, &eopkg_is_package); +} diff --git a/src/eopkg.h b/src/eopkg.h new file mode 100644 index 0000000..9a4e11d --- /dev/null +++ b/src/eopkg.h @@ -0,0 +1,28 @@ +/* + * eopkg.h + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#pragma once + +#include "cve-check-tool.h" + +/** + * Inspect an Evolve OS pspec.xml file (eopkg packaging) + * + * @param filename Path to the pspec.xml file + * @return a struct source_package_t if successful, otherwise NULL + */ +struct source_package_t *eopkg_inspect_pspec(const char *filename); + +bool eopkg_is_patched(struct source_package_t *pkg, char *id); + +void eopkg_locate_sources(const char *directory); + +bool eopkg_is_package(const char *filename); diff --git a/src/fetch.c b/src/fetch.c new file mode 100644 index 0000000..92a4f35 --- /dev/null +++ b/src/fetch.c @@ -0,0 +1,139 @@ +/* + * fetch.c - cve-check-tool helpers + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include +#include +#include +#include +#include + +#include "fetch.h" +#include "util.h" + +struct fetch_t { + FILE *f; + const char *target; +}; + +size_t write_func(void *ptr, size_t size, size_t nmemb, struct fetch_t *f) +{ + if (!f->f) { + f->f = fopen(f->target, "wb"); + } + if (!f->f) { + return -1; + } + return fwrite(ptr, size, nmemb, f->f); +} + +FetchStatus fetch_uri(const char *uri, const char *target, bool verbose) +{ + FetchStatus ret = FETCH_STATUS_FAIL; + CURLcode res; + struct stat st; + CURL *curl = NULL; + struct fetch_t *f = NULL; + + curl = curl_easy_init(); + if (!curl) { + return ret; + } + + if (stat(target, &st) == 0) { + res = curl_easy_setopt(curl, CURLOPT_TIMECONDITION, CURL_TIMECOND_IFMODSINCE); + if (res != CURLE_OK) { + goto bail; + } + res = curl_easy_setopt(curl, CURLOPT_TIMEVALUE, st.st_mtime); + if (res != CURLE_OK) { + goto bail; + } + } + + res = curl_easy_setopt(curl, CURLOPT_URL, uri); + if (res != CURLE_OK) { + goto bail; + } + if (verbose) { + res = curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); + } + res = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_func); + if (res != CURLE_OK) { + goto bail; + } + f = calloc(1, sizeof(struct fetch_t)); + if (!f) { + goto bail; + } + f->target = target; + res = curl_easy_setopt(curl, CURLOPT_WRITEDATA, f); + if (res != CURLE_OK) { + goto bail; + } + + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + goto bail; + } + ret = f->f ? FETCH_STATUS_UPDATE : FETCH_STATUS_OK; + +bail: + + if (f) { + if (f->f) { + fclose(f->f); + } + free(f); + } + + if (curl) { + curl_easy_cleanup(curl); + } + + return ret; +} + +bool gunzip_file(const char *path) +{ + GFile *in = NULL, *out = NULL; + autofree(gchar) *newpath = NULL; + autofree(GFileInputStream) *fis = NULL; + autofree(GFileOutputStream) *fos = NULL; + autofree(GOutputStream) *cos = NULL; + autofree(GZlibDecompressor) *conv = NULL; + gsize ret; + + newpath = g_strdup(path); + + if (g_str_has_suffix(newpath, ".gz")) { + newpath = str_replace(newpath, ".gz", ""); + } + + in = g_file_new_for_path(path); + out = g_file_new_for_path(newpath); + + fis = g_file_read(in, NULL, NULL); + if (!fis) { + return NULL; + } + fos = g_file_replace(out, NULL, FALSE, 0, NULL, NULL); + if (!fos) { + return NULL; + } + + conv = g_zlib_decompressor_new(G_ZLIB_COMPRESSOR_FORMAT_GZIP); + cos = g_converter_output_stream_new(G_OUTPUT_STREAM(fos), G_CONVERTER(conv)); + if (!cos) { + return NULL; + } + ret = g_output_stream_splice(cos, G_INPUT_STREAM(fis), G_OUTPUT_STREAM_SPLICE_NONE, NULL, NULL); + return (ret > 0 ? true : false ); +} diff --git a/src/fetch.h b/src/fetch.h new file mode 100644 index 0000000..54fa6d4 --- /dev/null +++ b/src/fetch.h @@ -0,0 +1,39 @@ +/* + * fetch.h - cve-check-tool helpers + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#pragma once + +#include + +typedef enum { + FETCH_STATUS_FAIL, + FETCH_STATUS_UPDATE, + FETCH_STATUS_OK +} FetchStatus; + +/** + * Fetch the given URI to the target directory, only if it is newer than + * its counterpart on disk, or the counterpart does not yet exist. + * + * @param uri URI to fetch + * @param target Target filename + * @param verbose Whether to be verbose + * @return A FetchStatus, indicating the operation taken + */ +FetchStatus fetch_uri(const char *uri, const char *target, bool verbose); + +/** + * Attempt to extract the given gzipped file + * + * @param filename Path to file + * @return a boolean value, indicating success of the operation + */ +bool gunzip_file(const char *path); diff --git a/src/html.c b/src/html.c new file mode 100644 index 0000000..211209b --- /dev/null +++ b/src/html.c @@ -0,0 +1,181 @@ +/* + * html.c - cve-check-tool helpers + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#define _GNU_SOURCE + +#include "config.h" +#include "util.h" +#include "template.h" +#include "cve-check-tool.h" + +#define TMPL(X) DATA_DIRECTORY G_DIR_SEPARATOR_S X ".template" + +#define TMPL_BOTTOM TMPL("bottom") +#define TMPL_PACKAGE TMPL("package") +#define TMPL_TOP TMPL("top") + +#define NVD_CVE_URI "http://web.nvd.nist.gov/view/vuln/detail?vulnId=" + +static inline gchar *_concat(gchar *source, gchar *moar) +{ + gchar *ret = g_strconcat(source, moar, NULL); + if (ret) { + g_free(source); + return ret; + } + return source; +} + +static inline gchar *dotemplate(GHashTable *k, char *p) +{ + gchar *ret = template_string(p, k); + g_free(p); + return ret; +} + +static inline bool load_template(const char *tmpl_name, gchar **ret) +{ + autofree(GError) *error = NULL; + + if (!g_file_get_contents(tmpl_name, ret, NULL, &error)) { + g_printerr("Unable to access mandatory template: %s\n", tmpl_name); + *ret = NULL; + return false; + } + return true; +} + +#define LOAD_TEMPLATE(name,ret) if (!load_template(name,ret)) { return false; } + +bool write_report(CveCheckTool *self, char *filename) +{ + autofree(gchar) *bottom, *package, *top; + autofree(GError) *error = NULL; + bottom = package = top = NULL; + autofree(GHashTable) *macros = NULL; + autofree(gchar) *aff = NULL; + autofree(gchar) *output = NULL; + autofree(gchar) *body = NULL; + GHashTableIter iter; + gchar *key = NULL; + struct source_package_t *v = NULL; + GList *c = NULL; + struct cve_entry_t *c_entry = NULL; + gint affected = 0; + guint row = 0; + + /* Mandatory template files */ + LOAD_TEMPLATE(TMPL_BOTTOM, &bottom); + LOAD_TEMPLATE(TMPL_PACKAGE, &package); + LOAD_TEMPLATE(TMPL_TOP, &top); + + macros = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, NULL); + g_hash_table_insert(macros, "LNVD_CVE_URI", NVD_CVE_URI); + + g_hash_table_iter_init(&iter, self->db); + while (g_hash_table_iter_next(&iter, (void**)&key, (void**)&v)) { + bool hit = false; + if (!v->issues && !v->patched && !self->show_unaffected) { + continue; + } + if (!v->issues && self->hide_patched) { + continue; + } + + guint cve_len = 0; + + if (v->issues) { + cve_len += g_list_length(v->issues); + } + if (v->patched && !self->hide_patched) { + cve_len += g_list_length(v->patched); + } + + g_hash_table_insert(macros, "PACKAGE_NAME", key); + + /* TODO: Collapse redundancy into helper. */ + g_hash_table_insert(macros, "STATUS_CLASS", "not-patched"); + g_hash_table_insert(macros, "STATUS_STRING", "Check"); + for (c = v->issues; c; c = c->next) { + autofree(gchar) *ent = NULL; + c_entry = g_hash_table_lookup(self->cdb, (gchar*)c->data); + + if (self->modified > 0 && c_entry->modified > self->modified) { + continue; + } + hit = true; + + g_hash_table_insert(macros, "LCVEID", c_entry->id); + g_hash_table_insert(macros, "CVEID", c_entry->id); + g_hash_table_insert(macros, "DESC", c_entry->summary); + g_hash_table_insert(macros, "ROW_CLASS", row % 2 ? "even" : "odd"); + g_hash_table_insert(macros, "SCORE", c_entry->score); + + /* TODO: Add links .. */ + ent = g_strdup(package); + ent = dotemplate(macros, ent); + if (body) { + body = _concat(body, ent); + } else { + body = g_strdup(ent); + } + ++row; + } + g_hash_table_insert(macros, "STATUS_CLASS", "patched"); + g_hash_table_insert(macros, "STATUS_STRING", "Patched"); + if (!self->hide_patched && v->patched) { + for (c = v->patched; c; c = c->next) { + autofree(gchar) *ent = NULL; + c_entry = g_hash_table_lookup(self->cdb, (gchar*)c->data); + + if (self->modified > 0 && c_entry->modified > self->modified) { + continue; + } + hit = true; + + g_hash_table_insert(macros, "LCVEID", c_entry->id); + g_hash_table_insert(macros, "CVEID", c_entry->id); + g_hash_table_insert(macros, "DESC", c_entry->summary); + g_hash_table_insert(macros, "ROW_CLASS", row % 2 ? "even" : "odd"); + g_hash_table_insert(macros, "SCORE", c_entry->score); + + /* TODO: Add links .. */ + ent = g_strdup(package); + ent = dotemplate(macros, ent); + if (body) { + body = _concat(body, ent); + } else { + body = g_strdup(ent); + } + ++row; + } + } + if (hit) { + ++affected; + } + } + + aff = g_strdup_printf("CVE Report for %d package%s", affected, + affected > 1 ? "s" : ""); + + g_hash_table_insert(macros, "AFFECTED_STRING", aff); + top = dotemplate(macros, top); + output = g_strdup(top); + output = _concat(output, body); + output = _concat(output, bottom); + /* Write file */ + if (!g_file_set_contents(filename, output, -1, &error)) { + g_printerr("Unable to write report: %s\n", error->message); + return false; + } + + return true; +} diff --git a/src/html.h b/src/html.h new file mode 100644 index 0000000..34f317c --- /dev/null +++ b/src/html.h @@ -0,0 +1,21 @@ +/* + * html.h - cve-check-tool helpers + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#define _GNU_SOURCE + +#include "config.h" +#include "util.h" +#include "cve-check-tool.h" + +/** + * Attempt to write the report to disk + */ +bool write_report(CveCheckTool *self, char *filename); diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..1023d07 --- /dev/null +++ b/src/main.c @@ -0,0 +1,742 @@ +/* + * main.c - cve-check-tool + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cve-check-tool.h" + +#include "eopkg.h" +#include "rpm.h" +#include "util.h" +#include "html.h" +#include "cli.h" +#include "csv.h" +#include "plugins/jira/jira.h" +#include "config.h" +#include "cve-string.h" + +#define YEAR_START 2002 +#define URI_PREFIX "http://static.nvd.nist.gov/feeds/xml/cve" +#include "fetch.h" + +static CveCheckTool *self; +static char *srpm_dir = NULL; + + +/** XML traversal state */ +static bool in_list = false; +static bool in_entry = false; +static bool in_product = false; +static bool in_summary = false; + +#define DEFAULT_CONFIG_FILE DEFAULT_PATH "/cve-check-tool.conf" +#define SITE_CONFIG_FILE SITE_PATH "/cve-check-tool.conf" + +/** + * Helper utility to free a struct source_package_t + */ +static inline void package_free(void *p) +{ + if (!p) { + return; + } + struct source_package_t *t = p; + if (t->issues) { + g_list_free_full(t->issues, xmlFree); + } + if (t->patched) { + g_list_free_full(t->patched, xmlFree); + } + if (t->path) { + g_free(t->path); + } + if (t->xml) { + xmlFree((xmlChar*)t->name); + xmlFree((xmlChar*)t->version); + } else { + g_free((gchar*)t->name); + g_free((gchar*)t->version); + } + if (t->extra && srpm_dir) { + g_strfreev(t->extra); + } + + free(t); +} + +/** + * Purse a CPE line into a consumable form + * + * @param inp cpe:/ identifier string + * @param vuln Where to store the resulting vulnerability data + * @return a boolean value, true if the operation succeeded + */ +static bool parse_vuln(const xmlChar* inp, struct vulnerability_t *vuln) +{ + gchar *product = NULL; + gchar *vendor = NULL; + gchar *version = NULL; + int len = 0; + /* Example: cpe:/a:oracle:siebel_crm:8.1.1 */ + gchar **splits = g_strsplit((const gchar*)inp, ":", 10); + if ((len = g_strv_length(splits)) < 4) { + g_strfreev(splits); + return false; + } + + vendor = g_strdup(splits[2]); + product = g_strdup(splits[3]); + if (len > 4) { + version = g_strdup(splits[4]); + } + g_strfreev(splits); + + vuln->vendor = vendor; + vuln->product = product; + vuln->version = version; + + return true; +} + +/** + * More state tracking + */ +static xmlChar *cur_id = NULL; +static xmlChar *summary = NULL; +static xmlChar *score = NULL; +static xmlChar *modified = NULL; +static bool had_interest = false; + +static bool in_link = false; +static GList *uris = NULL; +static bool in_vuln_cvss = false; +static bool in_base_metrics = false; +static bool in_score = false; +static bool in_date = false; + +/** + * Main iterator for XML parsing + * + * @param r A valid xmlTextReaderPtr (open) + */ +static void process_node(xmlTextReaderPtr r) +{ + const xmlChar *name = NULL; + const xmlChar *value = NULL; + struct vulnerability_t vuln = { 0 }; + struct source_package_t *t = NULL; + xmlChar *uri = NULL; + int64_t last_mod = -1; + + name = xmlTextReaderConstName(r); + if (!name) { + return; + } + /* New entry */ + if (xmlStrEqual(name, BAD_CAST "entry")) { + in_entry = !in_entry; + if (!in_entry) { + if (had_interest) { + struct cve_entry_t *ent = NULL; + + ent = calloc(1, sizeof(struct cve_entry_t)); + gchar *tmpk = NULL, *tmpv = NULL; + tmpk = g_strdup((const gchar*)cur_id); + tmpv = g_strdup((const gchar*)summary); + /* Force overwrite/free correctly */ + if (g_hash_table_contains(self->cdb, tmpk)) { + g_hash_table_remove(self->cdb, tmpk); + } + ent->id = tmpk; + ent->summary = tmpv; + ent->uris = uris; + if (self->modified > 0) { + last_mod = parse_xml_date((char*)modified); + ent->modified = last_mod; + } + if (score) { + ent->score = g_strdup((gchar*)score); + } + g_hash_table_insert(self->cdb, tmpk, ent); + uris = NULL; + } else { + if (uris) { + g_list_free_full(uris, xmlFree); + uris = NULL; + } + } + if (score) { + xmlFree(score); + score = NULL; + } + if (cur_id) { + xmlFree(cur_id); + cur_id = NULL; + } + if (summary) { + xmlFree(summary); + summary = NULL; + } + if (modified) { + xmlFree(modified); + modified = NULL; + } + had_interest = false; + return; + } + if (cur_id) { + xmlFree(cur_id); + } + cur_id = xmlTextReaderGetAttribute(r, BAD_CAST "id"); + if (!cur_id) { + return; + } + return; + } + if (xmlStrEqual(name, BAD_CAST "vuln:references")) { + in_link = !in_link; + return; + } + if (in_link && xmlStrEqual(name, BAD_CAST "vuln:reference")) { + uri = xmlTextReaderGetAttribute(r, BAD_CAST "href"); + if (!uri) { + return; + } + uris = g_list_append(uris, uri); + uri = NULL; + } + if (xmlStrEqual(name, BAD_CAST "vuln:vulnerable-software-list")) { + in_list = !in_list; + return; + } + if (in_list && xmlStrEqual(name, BAD_CAST "vuln:product")) { + in_product = !in_product; + return; + } + /* Score checking */ + if (xmlStrEqual(name, BAD_CAST "vuln:cvss")) { + in_vuln_cvss = !in_vuln_cvss; + return; + } + if (in_vuln_cvss && xmlStrEqual(name, BAD_CAST "cvss:base_metrics")) { + in_base_metrics = !in_base_metrics; + return; + } + if (in_base_metrics && xmlStrEqual(name, BAD_CAST "cvss:score")) { + in_score = !in_score; + } + if (in_base_metrics && in_score) { + value = xmlTextReaderConstValue(r); + if (!value) { + return; + } + score = xmlStrdup(value); + } + /* Get last modified */ + if (xmlStrEqual(name, BAD_CAST "vuln:last-modified-datetime")) { + in_date = !in_date; + } + if (in_date && self->modified > 0) { + value = xmlTextReaderConstValue(r); + if (!value) { + return; + } + modified = xmlStrdup(value); + } + /* Product checking */ + if (in_list && in_product) { + value = xmlTextReaderConstValue(r); + if (!value) { + return; + } + if (!parse_vuln(value, &vuln)) { + return; + } + t = g_hash_table_lookup(self->db, vuln.product); + if (!t) { + goto clean; + } + if (!(vuln.version && xmlStrEqual(t->version, BAD_CAST vuln.version))) { + goto clean; + } + + if (self->is_patched && self->is_patched(t, (gchar*)cur_id)) { + if (!g_list_find_custom(t->patched, cur_id, (GCompareFunc)strcmp)) { + gchar *tmp = g_strdup((const gchar*)cur_id); + if (!tmp) { + abort(); + } + t->patched = g_list_append(t->patched, tmp); + had_interest = true; + } + } else { + if (!g_list_find_custom(t->issues, cur_id, (GCompareFunc)strcmp)) { + gchar *tmp = g_strdup((const gchar*)cur_id); + if (!tmp) { + abort(); + } + t->issues = g_list_append(t->issues, tmp); + had_interest = true; + } + } +clean: + g_free(vuln.vendor); + g_free(vuln.product); + if (vuln.version) { + g_free(vuln.version); + } + return; + } + if (in_entry && xmlStrEqual(name, BAD_CAST "vuln:summary")) { + in_summary = !in_summary; + if (in_summary && summary) { + xmlFree(summary); + summary = NULL; + } + return; + } + if (in_summary) { + summary = xmlTextReaderValue(r); + return; + } +} + +/** + * Parse an NVD xml database + * + * @param fname Path to the nvd db + * @return a boolean value, true if the operation succeeded + */ +static bool parse_file(const char *fname) +{ + xmlTextReaderPtr r = xmlReaderForFile(fname, NULL, 0); + if (!r) { + return false; + } + int ret; + + while ((ret = xmlTextReaderRead(r)) > 0) { + process_node(r); + } + xmlFreeTextReader(r); + + return true; +} + +/** + * Util to free a struct cve_entry_t + */ +static inline void cve_free(void *v) +{ + if (!v) { + return; + } + struct cve_entry_t *t = v; + if (t->uris) { + g_list_free_full(t->uris, xmlFree); + } + if (t->score) { + xmlFree((xmlChar*)t->score); + } + g_free(t->id); + g_free(t->summary); + g_free(t); +} + +void cve_add_package(const char *path) +{ + struct source_package_t *pkg = self->examine(path); + if (!pkg) { + return; + } + g_hash_table_insert(self->db, pkg->name, pkg); +} + +static void show_version(void) +{ + const gchar *msg = "\ +" PACKAGE " " PACKAGE_VERSION "\n\ +Copyright (C) 2015 Intel Corporation\n\ +" PACKAGE_NAME " is free software; you can redistribute it and/or modify\n\ +it under the terms of the GNU General Public License as published by\n\ +the Free Software Foundation; either version 2 of the License, or\n\ +(at your option) any later version."; + printf("%s\n", msg); +} + +static bool hide_patched = false; +static bool show_unaffected = false; +static bool _show_version = false; +static bool skip_update = false; +static gchar *forced_type = NULL; +static bool no_html = false; +static bool csv_mode = false; +static char *modified_stamp = NULL; + +static GOptionEntry _entries[] = { + { "not-patched", 'n', 0, G_OPTION_ARG_NONE, &hide_patched, "Hide patched/addressed CVEs", NULL }, + { "not-affected", 'a', 0, G_OPTION_ARG_NONE, &show_unaffected, "Show unaffected items", NULL }, + { "skip-update", 'u', 0, G_OPTION_ARG_NONE, &skip_update, "Skip update of databases", NULL }, + { "version", 'v', 0, G_OPTION_ARG_NONE, &_show_version, "Show version", NULL }, + { "type", 't', 0, G_OPTION_ARG_STRING, &forced_type, "Set package type to T", "T" }, + { "no-html", 'N', 0, G_OPTION_ARG_NONE, &no_html, "Disable HTML report", NULL }, + { "modified", 'm', 0, G_OPTION_ARG_STRING, &modified_stamp, "Ignore reports after modification date", "D" }, + { "srpm-dir", 's', 0, G_OPTION_ARG_STRING, &srpm_dir, "Source RPM directory", "S" }, + { "csv", 'c', 0, G_OPTION_ARG_NONE, &csv_mode, "Output CSV formatted data only", NULL }, + { NULL } +}; + +static bool set_package_type(PackageType type) +{ + switch (type) { + case PACKAGE_TYPE_EOPKG: + self->examine = &eopkg_inspect_pspec; + self->is_patched = &eopkg_is_patched; + self->locate = &eopkg_locate_sources; + return true; + case PACKAGE_TYPE_RPM: + self->examine = &rpm_inspect_spec; + self->is_patched = &rpm_is_patched; + self->locate = &rpm_locate_sources; + return true; + default: + return false; + } +} + +static void run_jira(GKeyFile *config) +{ + if (!init_jira_plugin(config, NULL)) return; + /* if (!init_jira_plugin(NULL, DEFAULT_CONFIG_FILE)) return; */ + /* if (!init_jira_plugin(NULL, "plugins/jira/jira.cfg")) return; */ +} + +/** + * Main entry. + */ +int main(int argc, char **argv) +{ + autofree(GError) *error = NULL; + autofree(GOptionContext) *context = NULL; + autofree(GDateTime) *date = NULL; + autofree(gchar) *target = NULL; + autofree(GKeyFile) *config = NULL; + autofree(cve_string) *workdir = NULL; + __attribute__ ((unused)) struct stat st; + gint year; + int ret = EXIT_FAILURE; + PackageType type; + CveCheckTool instance = {0}; + instance.modified = -1; + time_t ti; + LIBXML_TEST_VERSION + + self = &instance; + context = g_option_context_new(" - cve check tool"); + g_option_context_add_main_entries(context, _entries, NULL); + if (!g_option_context_parse(context, &argc, &argv, &error)) { + g_printerr("Invalid options: %s\n", error->message); + goto cleanup; + } + + if (_show_version) { + show_version(); + ret = EXIT_SUCCESS; + goto cleanup; + } + + if (argc != 2) { + printf("Usage: %s [path-to-source-spec|path-to-source-list-file]\n", argv[0]); + goto cleanup; + } + + if (!g_file_test(argv[1], G_FILE_TEST_EXISTS)) { + printf("%s does not exist\n", argv[1]); + goto cleanup; + } + + target = get_absolute_path(argv[1]); + if (!target) { + goto cleanup; + } + + if (srpm_dir) { + if (!g_file_test(srpm_dir, G_FILE_TEST_IS_DIR)) { + printf("srpm directory does not exist or is not a directory\n"); + goto cleanup; + } + self->is_patched = srpm_is_patched; + } + + if (forced_type) { + if (g_str_equal(forced_type, "rpm")) { + type = PACKAGE_TYPE_RPM; + } else if (g_str_equal(forced_type, "eopkg")) { + type = PACKAGE_TYPE_EOPKG; + } else { + printf("Unknown type: \"%s\".\nCurrently supported types: eopkg, rpm", forced_type); + goto cleanup; + } + } else { + type = guess_package_type(target, false); + } + + if (modified_stamp) { + ti = curl_getdate(modified_stamp, NULL); + if (ti <= 0) { + fprintf(stderr, "Invalid date\n"); + goto cleanup; + } + instance.modified = (int64_t)ti; + } + + config = g_key_file_new(); + if (g_file_test(SITE_CONFIG_FILE, G_FILE_TEST_EXISTS)) { + if (!g_key_file_load_from_file(config, SITE_CONFIG_FILE, G_KEY_FILE_KEEP_TRANSLATIONS, &error)) { + fprintf(stderr, "Unable to read configuration: %s\n", error->message); + goto cleanup; + } + } else if (g_file_test(DEFAULT_CONFIG_FILE, G_FILE_TEST_EXISTS)) { + if (!g_key_file_load_from_file(config, DEFAULT_CONFIG_FILE, G_KEY_FILE_KEEP_TRANSLATIONS, &error)) { + fprintf(stderr, "Unable to read default configuration: %s\n", error->message); + goto cleanup; + } + } else { + fprintf(stderr, "No valid configuration found, aborting\n"); + goto cleanup; + } + + self->config = config; + run_jira(self->config); /* this will go somewhere else */ + + self->hide_patched = hide_patched; + self->show_unaffected = show_unaffected; + instance.db = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, package_free); + instance.cdb = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, cve_free); + + /* Automatically disable HTML output in CSV mode */ + if (csv_mode) { + no_html = true; + } + + workdir = cve_string_dup_printf("%s/NVDS/", g_get_home_dir()); + if (stat(workdir->str, &st) != 0) { + if (mkdir(workdir->str, 0777) != 0) { + fprintf(stderr, "Unable to create required directory: %s\n", workdir->str); + goto cleanup; + } + } + + if (type > PACKAGE_TYPE_MIN && type < PACKAGE_TYPE_UNKNOWN) { + if (!set_package_type(type)) { + printf("Unsupported package type\n"); + goto cleanup; + } + /* Attempt to add a single package.. */ + if (g_file_test(target, G_FILE_TEST_IS_DIR)) { + /* Recurse.. */ + self->locate(target); + } else { + cve_add_package(target); + } + } else if (type == PACKAGE_TYPE_UNKNOWN && is_package_list(target)) { + /* We're a packages thingy. */ + autofree(GFile) *fi = NULL; + autofree(GFile) *basedir = NULL; + autofree(gchar) *basedirsz = NULL; + autofree(GFileInputStream) *fis = NULL; + autofree(GDataInputStream) *dis = NULL; + char *read = NULL; + + fi = g_file_new_for_path(target); + basedir = g_file_get_parent(fi); + basedirsz = g_file_get_path(basedir);; + fis = g_file_read(fi, NULL, &error); + + if (!fis) { + g_printerr("Unable to open file for reading: %s\n", error->message); + goto cleanup; + } + dis = g_data_input_stream_new(G_INPUT_STREAM(fis)); + + while ((read = g_data_input_stream_read_line(dis, NULL, NULL, NULL)) != NULL) { + /* Attempt to get */ + autofree(gchar) *path = NULL; + + read = g_strchomp(read); + if (g_str_equal(read, "")) { + continue; + } + + /* Tab delimited files mean we don't look through git trees */ + if (str_contains(read, "\t")) { + autofree(gstrv) *splits = g_strsplit(read, "\t", 3); + struct source_package_t *t = NULL; + + if (srpm_dir) { + t = rpm_inspect_srpm(srpm_dir, splits[0], splits[1], splits[2]); + if (!t) { + goto clean; + } + } else { + t = calloc(1, sizeof(struct source_package_t)); + if (!t) { + goto clean; + } + t->path = NULL; + t->name = g_strdup(splits[0]); + t->version = g_strdup(splits[1]); + } + g_hash_table_insert(self->db, t->name, t); + goto clean; + } + + /* try directory above *first* as this is the norm */ + path = g_build_path(G_DIR_SEPARATOR_S, basedirsz, "..", read, NULL); + if (!g_file_test(path, G_FILE_TEST_EXISTS)) { + g_free(path); + /* Fall back to building from current directory */ + path = g_build_path(G_DIR_SEPARATOR_S, basedirsz, read, NULL); + } + + if (!g_file_test(path, G_FILE_TEST_EXISTS)) { + printf("Warning: Not found: %s\n", path); + goto clean; + } + + /* Attempt to determine type.. */ + if (type == PACKAGE_TYPE_UNKNOWN) { + type = guess_package_type(path, true); + if (type == PACKAGE_TYPE_UNKNOWN) { + printf("Unable to determine package type, bailing\n"); + g_free(read); + goto cleanup; + } + if (!set_package_type(type)) { + printf("Unsupported package type\n"); + g_free(read); + goto cleanup; + } + } + self->locate(path); +clean: + g_free(read); + } + } else { + printf("Unsupported file: %s\n", target); + goto cleanup; + } + + gint size = g_hash_table_size(self->db); + if (size == 0) { + printf("No source files were encountered, aborting\n"); + goto cleanup; + } + /* Consider a verbosity flag... */ + if (!csv_mode) { + printf("Scanned %d source file%s\n", size, size > 1 ? "s" : ""); + } + date = g_date_time_new_now_local(); + year = g_date_time_get_year(date); + + /* Round 1: Obtain and extract updated DBs */ + if (!skip_update) { + for (int i = YEAR_START; i <= year+1; i++) { + autofree(gchar) *uri = NULL; + autofree(gchar) *target = NULL; + FetchStatus st; + + if (i > year) { + uri = g_strdup_printf("%s/nvdcve-2.0-Modified.xml.gz", URI_PREFIX); + target = g_strdup_printf("%s/NVDS/nvdcve-2.0-Modified.xml.gz", g_get_home_dir()); + } else { + uri = g_strdup_printf("%s/nvdcve-2.0-%d.xml.gz", URI_PREFIX, i); + target = g_strdup_printf("%s/NVDS/nvdcve-2.0-%d.xml.gz", g_get_home_dir(), i); + } + + st = fetch_uri(uri, target, !csv_mode); + switch (st) { + case FETCH_STATUS_FAIL: + fprintf(stderr, "Failed to fetch %s\n", uri); + goto cleanup; + case FETCH_STATUS_UPDATE: + if (!gunzip_file(target)) { + fprintf(stderr, "Unable to extract %s\n", target); + goto cleanup; + } + break; + default: + break; + } + } + } + + for (int i = YEAR_START; i <= year+1; i++) { + autofree(gchar) *s = NULL; + if (i > year) { + s = g_strdup_printf("%s/NVDS/nvdcve-2.0-Modified.xml", g_get_home_dir()); + } else { + s = g_strdup_printf("%s/NVDS/nvdcve-2.0-%d.xml", g_get_home_dir(), i); + } + if (!parse_file(s)) { + fprintf(stderr, "\nUnable to find: %s\n", s); + goto cleanup; + } + autofree(gchar) *txt = g_strdup_printf("Loaded: %s", basename(s)); + if (csv_mode) { + continue; + } + printf("\r%-40s", txt); + fflush(stdout); + + if (i > year) { + printf("\n"); + } + } + + if (csv_mode) { + csv_write_report(self); + } else { + cli_write_report(self); + } + + if (!no_html) { + if (!write_report(self, "report.html")) { + fprintf(stderr, "Report generation failed\n"); + } else { + ret = EXIT_SUCCESS; + } + } else { + ret = EXIT_SUCCESS; + } + +cleanup: + if (instance.db) { + g_hash_table_unref(instance.db); + } + if (self->cdb) { + g_hash_table_unref(instance.cdb); + } + run_jira(self->config); + + xmlCleanupParser(); + return ret; +} diff --git a/src/plugins/jira/Makefile.am b/src/plugins/jira/Makefile.am new file mode 100644 index 0000000..111fb73 --- /dev/null +++ b/src/plugins/jira/Makefile.am @@ -0,0 +1,22 @@ +-include $(top_srcdir)/common.mk + +bin_PROGRAMS = jira_test_client + +jira_plugin_SOURCES = \ + jira.c \ + jira.h + +jira_plugin_CFLAGS = \ + $(GLIB_CFLAGS) \ + $(GIO_CFLAGS) \ + $(LIBCURL_CFLAGS) \ + $(AM_CFLAGS) \ + $(LIBJSON_GLIB_CFLAGS) \ + $(LIBGOBJECT_CFLAGS) + +jira_plugin_LDADD = \ + $(GLIB_LIBS) \ + $(GIO_LIBS) \ + $(LIBCURL_LIBS) + $(LIBJSON_GLIB_LIBS) \ + $(LIBGOBJECT_LIBS) diff --git a/src/plugins/jira/jira.c b/src/plugins/jira/jira.c new file mode 100644 index 0000000..658a285 --- /dev/null +++ b/src/plugins/jira/jira.c @@ -0,0 +1,926 @@ +/* + * jira.c - cve-check-tool + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "jira.h" + +#define _GNU_SOURCE + +struct jira_cfg_t { + gchar *url; + gchar *user; + gchar *password; + int timeout_secs; + bool verbose; + GSList *fields; + gchar *jql; + gchar *search_filter; + GHashTable *template; +}; + +struct jira_cfg_field_t { + gchar *name; + gchar *attribute; + gchar *value; + bool is_array; +}; + +struct jira_data_t { + gchar *data; + size_t size; +}; + +static bool get_jira_secret(gchar **secret); + +static size_t get_str_length(const gchar *str); + +static bool is_empty(const gchar *str); + +static bool parse_jira_cfg_field(const gchar *key, const gchar *value); + +static bool parse_jira_cfg_bool(const gchar*path, GKeyFile *key_file, + const gchar *group, const gchar *key, bool *assignee, + bool ignore_error, bool default_value); + +static bool parse_jira_cfg_int(const gchar*path, GKeyFile *key_file, + const gchar *group, const gchar *key,int *assignee, + bool ignore_error, int default_value); + +static bool parse_jira_cfg_file(GKeyFile *config, const gchar *path); + +static bool parse_jira_cfg_str(const gchar *path, GKeyFile *keyfile, + const gchar *group, const gchar *key, gchar **assignee, + bool ignore_error, gchar *default_value); + +static size_t write_jira_cb(void *ptr, size_t size, size_t memb, void *data); + +static struct jira_cfg_t *g_jira_cfg = NULL; + +bool add_new_jira_issue(const gchar *jira_json) +{ + bool ret = false; + gchar *url = NULL; + CURL *curl = NULL; + CURLcode curl_status = CURLE_OK; + autofree(gchar) *secret = NULL; + struct curl_slist *headers = NULL; + struct jira_data_t post; + struct jira_data_t response; + + if (is_empty(jira_json)) { + printf("Error: Missing json to add new JIRA issue\n"); + return false; + } + if (is_empty(g_jira_cfg->url)) { + printf("Error: Missing require JIRA server URL\n"); + return false; + } + if (!get_jira_secret(&secret)) { + return false; + } + url = g_strdup_printf("%s/issue", g_jira_cfg->url); + if (url == NULL) { + curl_global_cleanup(); + printf("Error: Out of memory for searching issues\n"); + return false; + } + response.data = g_malloc(1); + if (response.data == NULL) { + curl_global_cleanup(); + printf("Error: Out of memory\n"); + return false; + } + response.size = 0; + post.data = g_strdup(jira_json); + post.size = (long)get_str_length(jira_json); + curl = curl_easy_init(); + if (!curl) { + curl_global_cleanup(); + printf("Error: Curl failed to initalize for adding a new issue\n"); + return false; + } + headers = curl_slist_append(NULL,"Accept: application/json"); + headers = curl_slist_append(headers,"Content-Type: application/json"); + curl_easy_setopt(curl,CURLOPT_HTTPHEADER,headers); + curl_easy_setopt(curl,CURLOPT_USERAGENT,"libcurl-agent/1.0"); + curl_easy_setopt(curl,CURLOPT_URL,url); + curl_easy_setopt(curl,CURLOPT_NOPROGRESS,1L); + curl_easy_setopt(curl,CURLOPT_USERPWD,secret); + curl_easy_setopt(curl,CURLOPT_POSTFIELDS,post.data); + curl_easy_setopt(curl,CURLOPT_POSTFIELDSIZE,post.size); + curl_easy_setopt(curl,CURLOPT_SSL_VERIFYPEER,0L); + curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,write_jira_cb); + curl_easy_setopt(curl,CURLOPT_WRITEDATA,(void *)&response); + curl_easy_setopt(curl,CURLOPT_READDATA,&post); + if (g_jira_cfg->timeout_secs) { + curl_easy_setopt(curl, CURLOPT_TIMEOUT, g_jira_cfg->timeout_secs); + } + if (g_jira_cfg->verbose) { + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + printf("%s\n", post.data); + } + curl_status = curl_easy_perform(curl); + if (curl_status != CURLE_OK) { + printf("Error: %s\n", curl_easy_strerror(curl_status)); + ret = false; + } else { + if (g_jira_cfg->verbose) { + printf("%s\n", post.data); + } + ret = check_jira_response(response.data); + if (ret) { + /* Perhaps a double check with new jql query here */ + } + } + g_free(response.data); + g_free(post.data); + curl_easy_cleanup(curl); + curl_global_cleanup(); + return ret; +} + +bool build_new_jira_issue(const gchar *summary, + bool as_template, gchar **jira_json) +{ + const GSList *iter = g_jira_cfg->fields; + struct jira_cfg_field_t *jira_cfg_field = NULL; + autofree(gchar) *json_new_issue = NULL; + + if (is_empty(summary) && !as_template) { + printf("Error: Missing value for summary\n"); + return false; + } + json_new_issue = g_strdup("{\"fields\":{"); + while (iter != NULL) { + jira_cfg_field = (struct jira_cfg_field_t *)(iter->data); + if (jira_cfg_field->value != NULL) { + if (jira_cfg_field->attribute != NULL) { + if (!jira_cfg_field->is_array) { + if (as_template) { + json_new_issue = g_strdup_printf( + "%s\"%s\":{\"%s\":\"{{%s}}\"},", + json_new_issue, jira_cfg_field->name, + jira_cfg_field->attribute, jira_cfg_field->name); + } else { + json_new_issue = g_strdup_printf( + "%s\"%s\":{\"%s\":\"%s\"},", + json_new_issue, jira_cfg_field->name, + jira_cfg_field->attribute, jira_cfg_field->value); + } + } else { + if (as_template) { + json_new_issue = g_strdup_printf( + "%s\"%s\":[{\"%s\":\"{{%s}}\"}],", + json_new_issue, jira_cfg_field->name, + jira_cfg_field->attribute, jira_cfg_field->name); + } else { + json_new_issue = g_strdup_printf( + "%s\"%s\":[{\"%s\":\"%s\"}],", + json_new_issue, jira_cfg_field->name, + jira_cfg_field->attribute, jira_cfg_field->value); + } + } + } else { + if (as_template) { + json_new_issue = g_strdup_printf( + "%s\"%s\":\"{{%s}}\",", + json_new_issue, jira_cfg_field->name, + jira_cfg_field->name); + } else { + json_new_issue = g_strdup_printf( + "%s\"%s\":\"%s\",", + json_new_issue, jira_cfg_field->name, + jira_cfg_field->value); + } + } + } + iter = iter->next; + } + if (g_str_has_suffix(json_new_issue,",")) { + json_new_issue = g_string_erase( + g_string_new(json_new_issue), + get_str_length(json_new_issue)-1, 1)->str; + } + if (as_template) { + json_new_issue = + g_strdup_printf("%s,\"summary\":\"{{summary}}\"", json_new_issue); + } else { + json_new_issue = + g_strdup_printf("%s,\"summary\":\"%s\"", json_new_issue, summary); + } + + *jira_json = g_strdup_printf("%s}}", json_new_issue); + return true; +} + +bool build_search_jira_issues(gchar **jira_json) +{ + *jira_json = g_strdup_printf("{\"jql\":\"%s\"}", g_jira_cfg->jql); + return true; +} + +bool check_jira_response(const gchar *jira_json) +{ + bool ret = false; + autofree(GError) *error = NULL; + JsonParser *parser = NULL; + JsonNode *node, *status = NULL; + JsonArray *records = NULL; + + if (is_empty(jira_json)) { + printf("Error: JIRA returned nothing\n"); + return false; + } + parser = json_parser_new(); + json_parser_load_from_data(parser, jira_json, -1, &error); + if (error) { + printf("Error: %s\n", error->message); + return false; + } + node = json_parser_get_root(parser); + status = json_object_get_member(json_node_get_object(node),"errorMessages"); + if (status != NULL) { + if (JSON_NODE_HOLDS_ARRAY(status)) { + records = json_node_get_array(status); + if (json_array_get_length(records)) { + printf("Error: %s\n", json_array_get_string_element(records,0)); + ret = false; + } else { + printf("Error: %s\n", jira_json); + ret = false; + } + } else { + printf("Error: JIRA responded with an unexpected message\n"); + ret = false; + } + ret = false; + } else { + ret = true; + } + json_node_free(node); + return ret; +} + +static void destroy_template(gpointer key, gpointer value, gpointer data) +{ + g_free(data); +} + +void destroy_jira_plugin(void) +{ + const GSList *iter = NULL; + struct jira_cfg_field_t *jira_cfg_field = NULL; + + if (g_jira_cfg == NULL) { + return; + } + g_free(g_jira_cfg->url); + g_free(g_jira_cfg->user); + g_free(g_jira_cfg->password); + g_free(g_jira_cfg->jql); + g_free(g_jira_cfg->search_filter); + iter = g_jira_cfg->fields; + while(iter != NULL) { + jira_cfg_field = (struct jira_cfg_field_t *)(iter->data); + g_free(jira_cfg_field->name); + g_free(jira_cfg_field->attribute); + g_free(jira_cfg_field->value); + iter = iter->next; + } + g_hash_table_foreach(g_jira_cfg->template, destroy_template, NULL); + g_hash_table_destroy(g_jira_cfg->template); + g_slice_free(struct jira_cfg_t, g_jira_cfg); + g_jira_cfg = NULL; +} + +int get_jira_issues_count(const GSList *jira_issues) +{ + int jira_issues_count = 0; + const GSList *iter = jira_issues; + + while(iter != NULL) { + ++jira_issues_count; + iter = iter->next; + } + return jira_issues_count; +} + +static bool get_jira_secret(gchar **secret) +{ + autofree(gchar) *str = NULL; + + if (is_empty(g_jira_cfg->user)) + { + printf("Error: Missing required JIRA user name\n"); + return false; + } + if (!is_empty(g_jira_cfg->password)) { + str = g_strdup_printf("%s:%s", g_jira_cfg->user, g_jira_cfg->password); + } else { + str = g_strdup_printf("%s", g_jira_cfg->user); + } + if (is_empty(str)) { + printf("Error: Unable to create an authentication token\n"); + return false; + } + *secret = g_strdup_printf("%s", str); + return true; +} + +static size_t get_str_length(const gchar *str) +{ + autofree(cve_string) *s = NULL; + if (str == NULL) { + return 0; + } + s = cve_string_dup(str); + return s->len; +} + +bool init_jira_plugin(GKeyFile *config, const gchar *path) +{ + destroy_jira_plugin(); + return (parse_jira_cfg_file(config, path)); +} + +static bool is_empty(const gchar *str) +{ + return (NULL == str); +} + +bool is_jira_alive(void) +{ + bool ret = false; + autofree(gchar) *secret = NULL; + autofree(GError) *error = NULL; + CURL *curl = NULL; + CURLcode curl_status = CURLE_OK; + struct curl_slist *headers = NULL; + struct jira_data_t response; + + if (is_empty(g_jira_cfg->url)) { + printf("Error: Missing require JIRA server URL\n"); + return false; + } + curl = curl_easy_init(); + if (!curl) { + curl_global_cleanup(); + printf("Error: Curl failed to initialize for searching issues\n"); + return false; + } + response.data = g_malloc(1); + if (response.data == NULL) { + curl_global_cleanup(); + printf("Error: Out of memory\n"); + return false; + } + response.size = 0; + headers = curl_slist_append(NULL,"Accept: application/json"); + headers = curl_slist_append(headers,"Content-Type: application/json"); + curl_easy_setopt(curl,CURLOPT_HTTPHEADER,headers); + curl_easy_setopt(curl,CURLOPT_USERAGENT,"libcurl-agent/1.0"); + curl_easy_setopt(curl,CURLOPT_URL,g_jira_cfg->url); + curl_easy_setopt(curl,CURLOPT_NOPROGRESS,1L); + curl_easy_setopt(curl,CURLOPT_SSL_VERIFYPEER,0L); + curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,write_jira_cb); + curl_easy_setopt(curl,CURLOPT_WRITEDATA,(void *)&response); + if (g_jira_cfg->timeout_secs) { + curl_easy_setopt(curl, CURLOPT_TIMEOUT, g_jira_cfg->timeout_secs); + } + if (g_jira_cfg->verbose) { + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + } + curl_status = curl_easy_perform(curl); + if (curl_status != CURLE_OK) { + printf("Error: %s\n", curl_easy_strerror(curl_status)); + ret = false; + } else { + printf("JIRA server is responsive\n"); + ret = true; + } + g_free(response.data); + curl_easy_cleanup(curl); + curl_global_cleanup(); + return ret; +} + +bool load_new_jira_issue_file(const gchar *path, const gchar *summary, gchar **jira_json) +{ + autofree(GError) *error = NULL; + autofree(gchar) *data = NULL; + autofree(gchar) *summary_tmp = g_strdup(summary); + + if (!g_file_get_contents(path, &data, NULL, &error)) { + printf("Error: Can't read file. %s\n", error->message); + return false; + } + if (summary != NULL) { + g_hash_table_insert(g_jira_cfg->template,"summary", summary_tmp); + } + *jira_json = g_strdup(template_string(data, g_jira_cfg->template)); + if (summary != NULL) { + g_hash_table_remove(g_jira_cfg->template,"summary"); + } + if (is_empty(*jira_json)) { + printf("Error: Template translation failed\n"); + return false; + } + return true; +} + +static bool parse_jira_cfg_bool(const gchar*path, GKeyFile *key_file, + const gchar *group, const gchar *key, bool *assignee, + bool ignore_error, bool default_value) +{ + autofree(GError) *error = NULL; + + *assignee = g_key_file_get_boolean(key_file,group,key,&error); + if (error) { + if (!ignore_error) { + printf("Error: Parsing JIRA %s %s value (%s): %s\n", + group, key, error->message, path); + return false; + } else { + *assignee = default_value; + } + } + return true; +} + +static bool parse_jira_cfg_field(const gchar *key, const gchar *value) +{ + int token_cnt,i = 0; + struct jira_cfg_field_t *jira_cfg_field = NULL; + gchar **tokens = NULL; + + tokens = g_strsplit(key, ".", -1); + token_cnt = g_strv_length(tokens); + if (tokens == NULL || token_cnt < 1 || token_cnt > 2) { + printf("Error: Badly formed JIRA field name %s\n", key); + return false; + } + jira_cfg_field = g_slice_new(struct jira_cfg_field_t); + if (g_jira_cfg == NULL) { + printf("Error: Out of memory\n"); + return false; + } + for (i = 0; i < token_cnt; i++) { + tokens[i] = g_strstrip(tokens[i]); + } + jira_cfg_field->name = g_strdup_printf("%s", tokens[0]); + if (!g_strcmp0(jira_cfg_field->name, "summary")) { + printf("Error: The field 'summary' is not allowed in cfg file\n"); + return false; + } + if ((token_cnt > 1) && (g_str_has_prefix(tokens[1],"[")) && + (g_str_has_suffix(tokens[1],"]"))) { + jira_cfg_field->is_array = true; + } else { + jira_cfg_field->is_array = false; + } + if (token_cnt == 2) { + if (!jira_cfg_field->is_array) { + jira_cfg_field->attribute = g_strdup_printf("%s", tokens[1]); + } else { + jira_cfg_field->attribute = g_string_erase( + g_string_erase(g_string_new(tokens[1]), + get_str_length(tokens[1])-1, 1),0,1)->str; + } + } else { + jira_cfg_field->attribute = NULL; + } + if (is_empty(value)) { + jira_cfg_field->value = NULL; + g_hash_table_insert(g_jira_cfg->template,jira_cfg_field->name,""); + } else { + jira_cfg_field->value = g_strdup(value); + g_hash_table_insert( + g_jira_cfg->template,jira_cfg_field->name, g_strdup(value)); + } + g_jira_cfg->fields = g_slist_append(g_jira_cfg->fields, jira_cfg_field); + g_strfreev(tokens); + return true; +} + +static bool parse_jira_cfg_file(GKeyFile *config, const gchar *path) +{ + autofree(GKeyFile) *key_file = g_key_file_new(); + GKeyFileFlags flags = G_KEY_FILE_KEEP_TRANSLATIONS; + autofree(GError) *error = NULL; + autofree(gchar) *value = NULL; + gchar **keys, **key = NULL; + gchar *group = NULL; + + if (config == NULL && path == NULL) { + printf("Error: Missing configuration key file and/or path\n"); + return false; + } + g_jira_cfg = g_slice_new(struct jira_cfg_t); + if (g_jira_cfg == NULL) { + printf("Error: Out of memory\n"); + return false; + } + if (config != NULL) { + key_file = config; + } else { + if (!g_key_file_load_from_file(key_file, path, flags, &error)) { + printf("Error: Reading JIRA file: %s\n", error->message); + return false; + } + } + group = "JIRA"; + if (!parse_jira_cfg_str(path, key_file, group,"url", + &g_jira_cfg->url,false, NULL)) { + return false; + } + if (!parse_jira_cfg_str(path, key_file, group,"user", + &g_jira_cfg->user,false, NULL)) { + return false; + } + if (!parse_jira_cfg_str(path, key_file, group,"password", + &g_jira_cfg->password,false, NULL)) { + return false; + } + if (!parse_jira_cfg_int(path, key_file, group,"timeout_secs", + &g_jira_cfg->timeout_secs,false, 0)) { + return false; + } + if (!parse_jira_cfg_bool(path, key_file, group,"verbose", + &g_jira_cfg->verbose,false, false)) { + return false; + } + group = "JIRA-New-Issue"; + g_jira_cfg->template = g_hash_table_new(g_str_hash, g_str_equal); + keys = g_key_file_get_keys(key_file, group, NULL, &error); + if (error) { + printf("Error: %s\n", error->message); + return 1; + } + if (keys == NULL) { + printf("Error: No JIRA json fields found.\n"); + return false; + } + key = keys; + while (*key != NULL) { + value = g_key_file_get_string(key_file, group, *key, &error); + if (error) { + g_strfreev(keys); + printf("Error: %s\n", error->message); + return false; + } + *key = g_strstrip(*key); + if (value != NULL) { + value = g_strstrip(value); + } + if (!parse_jira_cfg_field(*key, value)) { + g_strfreev(keys); + return false; + } + ++key; + } + g_strfreev(keys); + group = "JIRA-Search-Issues"; + if (!parse_jira_cfg_str(path, key_file, group,"jql", + &g_jira_cfg->jql, false, NULL)) { + return false; + } + if (!parse_jira_cfg_str(path, key_file, group,"search_filter", + &g_jira_cfg->search_filter, false, NULL)) { + return false; + } + return true; +} + +bool parse_jira_cfg_int(const gchar*path, GKeyFile *key_file, + const gchar *group, const gchar *key,int *assignee, + bool ignore_error, int default_value) +{ + autofree(GError) *error = NULL; + + *assignee = g_key_file_get_integer(key_file,group,key,&error); + if (error) { + if (!ignore_error) { + printf("Error: Parsing JIRA %s %s value (%s): %s\n", + group, key, error->message, path); + return false; + } else { + *assignee = default_value; + } + } else if (0 > *assignee) { + if (!ignore_error) { + printf("Error: Parsing JIRA %s %s value can't be negative: %s\n", + group, key, path); + return false; + } + } + return true; +} + +static bool parse_jira_cfg_str(const gchar *path, GKeyFile *key_file, + const gchar *group, const gchar *key, gchar **assignee, + bool ignore_error, gchar *default_value) +{ + autofree(GError) *error = NULL; + *assignee = g_key_file_get_string(key_file,group,key, &error); + if (error) { + if (!ignore_error) { + printf("Error: Parsing JIRA %s %s value (%s): %s\n", + group, key, error->message, path); + return false; + } + } + if (*assignee == NULL) { + *assignee = g_strdup(default_value); + } + return true; +} + +bool parse_jira_issues(const gchar *jira_issues_json, GSList **jira_issues) +{ + int issues_cnt, i = 0; + autofree(GError) *error = NULL; + JsonParser *parser = NULL; + JsonNode *node, *key, *summary, *status = NULL; + JsonObject *object, *record = NULL; + JsonArray *records = NULL; + + if (is_empty(g_jira_cfg->search_filter)) { + printf("Error: You must specify a search filter\n"); + } + if (!check_jira_response(jira_issues_json)) { + return false; + } + parser = json_parser_new(); + json_parser_load_from_data(parser, jira_issues_json, -1, &error); + if (error) { + printf("Error: %s\n", error->message); + return false; + } + node = json_object_get_member( + json_node_get_object(json_parser_get_root(parser)),"issues"); + if (node == NULL || !JSON_NODE_HOLDS_ARRAY(node)) { + printf("Error: Can't find issues field from JIRA response\n"); + return false; + } + records = json_node_get_array(node); + issues_cnt = json_array_get_length(records); + if (!issues_cnt) { + printf("Error: No issues returned from JIRA\n"); + return false; + } + for (i = 0; i < issues_cnt; i++) { + record = json_array_get_object_element(records, i); + node = json_object_get_member(record,"fields"); + object = json_node_get_object(node); + summary = json_object_get_member(object,"summary"); + if (g_pattern_match_simple(g_jira_cfg->search_filter, + json_node_get_string(summary))) { + summary = json_object_get_member(object,"summary"); + key = json_object_get_member(record,"key"); + node = json_object_get_member(object,"status"); + object = json_node_get_object(node); + status = json_object_get_member(object,"name"); + struct jira_issue_t *jira_issue = g_slice_new(struct jira_issue_t); + jira_issue->key = g_strdup(json_node_get_string(key)); + jira_issue->summary = g_strdup(json_node_get_string(summary)); + jira_issue->status = g_strdup(json_node_get_string(status)); + *jira_issues = g_slist_append(*jira_issues, jira_issue); + } + } + if (!g_slist_length(*jira_issues)) { + printf("Error: No matching JIRA issues found with filter: %s\n", + g_jira_cfg->search_filter); + return false; + } + return true; +} + +bool save(const gchar *something, const gchar *path) +{ + FILE *fp = NULL; + + if (is_empty(something)) { + printf("Error: Missing something to save\n"); + return false; + } + if (is_empty(path)) { + printf("Error: Missing path to save something\n"); + return false; + } + fp = fopen(path, "wb"); + if (fp == NULL) { + printf("Error: Can't write something to file: %s", path); + return false; + } + fprintf(fp, "%s", something); + fclose(fp); + return true; +} + +bool save_jira_issues_csv(const GSList *jira_issues, const gchar *path) +{ + const GSList *iter = jira_issues; + struct jira_issue_t *jira_issue = NULL; + FILE *fp = NULL; + int jira_issues_count = get_jira_issues_count(jira_issues); + + if (!jira_issues_count) { + printf("Error: No JIRA issues found\n"); + return false; + } + if (is_empty(path)) { + printf("Error: Missing value for CSV path\n"); + return false; + } + fp = fopen(path, "wb"); + if (fp == NULL) { + printf("Error: Can't open file for CSV writing: %s\n", path); + return false; + } + fprintf(fp, "JIRA Key,Summary,Status\n"); + while(iter != NULL) { + jira_issue = (struct jira_issue_t *)(iter->data); + fprintf(fp, "%s,%s,%s\n", + jira_issue->key, jira_issue->summary, jira_issue->status); + iter = iter->next; + } + fclose(fp); + return true; +} + +bool save_jira_issues_xml(const GSList *jira_issues, const gchar *path) +{ + const GSList *iter = jira_issues; + struct jira_issue_t *jira_issue = NULL; + const gchar *sp = " "; + FILE *fp = NULL; + int jira_issues_count = get_jira_issues_count(jira_issues); + + if (!jira_issues_count) { + printf("Error: No JIRA issues found\n"); + return false; + } + if (is_empty(path)) { + printf("Error: Missing value for XML path\n"); + return false; + } + fp = fopen(path, "wb"); + if (fp == NULL) { + printf("Error: Can't open file for XML writing: %s\n", path); + return false; + } + fprintf(fp, + " \n"); + fprintf(fp,"\n", jira_issues_count); + while(iter != NULL) { + jira_issue = (struct jira_issue_t *)(iter->data); + fprintf(fp, "%s\n", sp); + fprintf(fp, "%s%s%s\n", sp,sp,jira_issue->key); + fprintf(fp, "%s%s%s\n", sp,sp,jira_issue->summary); + fprintf(fp, "%s%s%s\n", sp,sp,jira_issue->status); + fprintf(fp, "%s\n", sp); + iter = iter->next; + } + fprintf(fp, "\n"); + fclose(fp); + return true; +} + +bool search_jira_issues(const gchar *jira_json, gchar **jira_issues_json) +{ + bool ret = false; + autofree(gchar) *secret = NULL; + autofree(GError) *error = NULL; + autofree(gchar) *url = NULL; + CURL *curl = NULL; + CURLcode curl_status = CURLE_OK; + struct curl_slist *headers = NULL; + struct jira_data_t response; + + if (is_empty(jira_json)) { + printf("Error: Missing json to search for JIRA issues\n"); + return false; + } + if (is_empty(g_jira_cfg->url)) { + printf("Error: Missing require JIRA server URL\n"); + return false; + } + if (!get_jira_secret(&secret)) { + return false; + } + url = g_strdup_printf("%s/search", g_jira_cfg->url); + if (url == NULL) { + curl_global_cleanup(); + printf("Error: Out of memory for searching issues\n"); + return false; + } + curl = curl_easy_init(); + if (!curl) { + curl_global_cleanup(); + printf("Error: Curl failed to initialize for searching issues\n"); + return false; + } + response.data = g_malloc(1); + if (response.data == NULL) { + curl_global_cleanup(); + printf("Error: Out of memory\n"); + return false; + } + response.size = 0; + headers = curl_slist_append(NULL,"Accept: application/json"); + headers = curl_slist_append(headers,"Content-Type: application/json"); + curl_easy_setopt(curl,CURLOPT_HTTPHEADER,headers); + curl_easy_setopt(curl,CURLOPT_USERAGENT,"libcurl-agent/1.0"); + curl_easy_setopt(curl,CURLOPT_URL,url); + curl_easy_setopt(curl,CURLOPT_NOPROGRESS,1L); + curl_easy_setopt(curl,CURLOPT_USERPWD,secret); + curl_easy_setopt(curl,CURLOPT_POSTFIELDS,jira_json); + curl_easy_setopt(curl, + CURLOPT_POSTFIELDSIZE,(long)get_str_length(jira_json)); + curl_easy_setopt(curl,CURLOPT_SSL_VERIFYPEER,0L); + curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,write_jira_cb); + curl_easy_setopt(curl,CURLOPT_WRITEDATA,(void *)&response); + if (g_jira_cfg->timeout_secs) { + curl_easy_setopt(curl, CURLOPT_TIMEOUT, g_jira_cfg->timeout_secs); + } + if (g_jira_cfg->verbose) { + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + printf("%s\n", jira_json); + } + curl_status = curl_easy_perform(curl); + if (curl_status != CURLE_OK) { + printf("Error: %s\n", curl_easy_strerror(curl_status)); + ret = false; + } else { + if (g_jira_cfg->verbose) { + printf("%s\n", response.data); + } + ret = check_jira_response(response.data); + if (ret) { + *jira_issues_json = g_strdup(response.data); + } + } + g_free(response.data); + curl_easy_cleanup(curl); + curl_global_cleanup(); + return ret; +} + +void set_verbose(bool verbose_flag) +{ + if (g_jira_cfg != NULL) { + g_jira_cfg->verbose = verbose_flag; + } +} + +void show_jira_issues(const GSList *jira_issues) +{ + const GSList *iter = NULL; + struct jira_issue_t *jira_issue = NULL; + int jira_issues_count = get_jira_issues_count(jira_issues); + + if (!jira_issues_count) { + printf("No JIRA issues found\n"); + return; + } + iter = jira_issues; + while(iter != NULL) { + jira_issue = (struct jira_issue_t *)(iter->data); + printf("JIRA Key: %s\n", jira_issue->key); + printf("Summary: %s\n", jira_issue->summary); + printf("Status: %s\n\n", jira_issue->status); + iter = iter->next; + } + printf("Total JIRA Issues: %u\n", jira_issues_count); +} + +static size_t write_jira_cb(void *ptr, size_t size, size_t memb, void *data) +{ + size_t new_size = size * memb; + struct jira_data_t *mem = (struct jira_data_t *)data; + + mem->data = realloc(mem->data, mem->size + new_size + 1); + if(mem->data == NULL) { + printf("Error: Out of memory\n"); + return 0; + } + memcpy(&(mem->data[mem->size]), ptr, new_size); + mem->size += new_size; + mem->data[mem->size] = 0; + return new_size; +} diff --git a/src/plugins/jira/jira.cfg b/src/plugins/jira/jira.cfg new file mode 100644 index 0000000..ca03376 --- /dev/null +++ b/src/plugins/jira/jira.cfg @@ -0,0 +1,49 @@ +###################################################################### +# Use this section to define your global JIRA configuration parameters +###################################################################### +[JIRA] +# Sets the JIRA server's REST API URL +# e.g., https:///rest/api/2 +url= + +# Sets the JIRA login user name +user= + +# Sets the JIRA login user name's password if any +password= + +# Sets the timeout in seconds in case server hangs. 0 means wait forver +timeout_secs=60 + +# Sets to true to show more output, good for debugging +verbose=false + +###################################################################### +# Use this section to define the default fields and values for adding +# a new JIRA issue. Make sure that they exist with proper permissions. +# You can add if required. If your JIRA schema is very complicated +# with many custom fields and options then consider using template instead. +# +# =value # For simple fields like summary and description +# .=value # For fields that have attributes, issuetype +# .[] # For fields that arrays of attributes, components +###################################################################### +[JIRA-New-Issue] +components.[name]=Security +description=This is CVE bug +issuetype.name=Bug +project.key=CLEAR +assignee.name=jlwhitem +#priority.name=P2 +#customfield_#####.value=Normal + +###################################################################### +# Use this section to define your JIRA search criteria +###################################################################### +[JIRA-Search-Issues] + +# Literal JIRA jql search criteria +jql=project=CLEAR AND component=Security + +# Set search token for summary fields. Globbing with wildcards ok. +search_filter=CVE-* diff --git a/src/plugins/jira/jira.h b/src/plugins/jira/jira.h new file mode 100644 index 0000000..7340a30 --- /dev/null +++ b/src/plugins/jira/jira.h @@ -0,0 +1,158 @@ +/* + * jira.h - cve-check-tool + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#define _GNU_SOURCE + +#pragma once + +#include +#include + +#include "cve-string.h" +#include "template.h" +#include "util.h" + +/** + * A Jira issue + */ +struct jira_issue_t { + gchar *key; /** +#include + +#include "jira.h" + +void show_usage(const gchar *program) +{ + printf( + "\nSeveral sanity tests for the cve-check-tool jira plugin\n\n" + "Usage: %s -t [-c ] [-V]\n" + " -c Path to Jira cfg file [default=./jira.cfg]\n" + " -V Turn on verbose [default=false]\n" + " -t Test case to run\n" + " 1: Check if Jira server is responsive\n" + " 2: Do a simple Jira search and save results to files\n" + " 3: Add a new Jira issue and save results to files \n" + " 4: Add a new Jira by creating and using a template\n\n", + g_path_get_basename(program)); +} + +/** + * A simple Jira plugin client for development purposes only. + */ +int main(int argc, char **argv) + { + int opt,test_case = -1; + bool verbose_flag = false; + gchar *jira_cfg_path = "jira.cfg"; + gchar *json_path = NULL; + autofree(gchar) *jira_issues_json = NULL; + autofree(gchar) *jira_json = NULL; + autofree(gchar) *summary = NULL; + GSList *jira_issues = NULL; + time_t now = time(NULL); + + while ((opt = getopt(argc,argv,"c:ht:V")) != -1) + switch(opt) { + case 'c': + jira_cfg_path = argv[optind-1]; + break; + case 't': + test_case = atoi(argv[optind-1]); + break; + case 'V': + verbose_flag = true; + break; + case 'h': + show_usage(argv[0]); + return 0; + } + if (test_case < 0) { + show_usage(argv[0]); + return -1; + } + printf("\n[Execute: test case %i using %s]\n\n", test_case, jira_cfg_path); + if (!init_jira_plugin(NULL, jira_cfg_path)) return 1; + set_verbose(verbose_flag); + switch(test_case) { + /* Check if Jira server is responsive */ + case 1: + if (!is_jira_alive()) + goto cleanup; + break; + /* Do a simple query */ + case 2: + if (!is_jira_alive()) + goto cleanup; + if (!build_search_jira_issues(&jira_json)) + goto cleanup; + if (!search_jira_issues(jira_json, &jira_issues_json)) + goto cleanup; + if (!parse_jira_issues(jira_issues_json, &jira_issues)) + goto cleanup; + show_jira_issues(jira_issues); + if (!save_jira_issues_csv(jira_issues,"jira_test_results1.csv")) + goto cleanup; + if (!save_jira_issues_xml(jira_issues,"jira_test_results1.xml")) + goto cleanup; + break; + /* Add a new Jira issue */ + case 3: + if (!is_jira_alive()) + goto cleanup; + summary = + g_strdup_printf("CVE-%s (Test:3)", g_strstrip(ctime(&now))); + if (!build_new_jira_issue(summary, false, &jira_json)) + goto cleanup; + printf("Adding new Jira issue: [%s]\n", summary); + if (!add_new_jira_issue(jira_json)) + goto cleanup; + g_free(jira_json); + if (!build_search_jira_issues(&jira_json)) + goto cleanup; + if (!search_jira_issues(jira_json, &jira_issues_json)) + goto cleanup; + if (!parse_jira_issues(jira_issues_json, &jira_issues)) + goto cleanup; + show_jira_issues(jira_issues); + if (!save_jira_issues_csv(jira_issues,"jira_test_results_add2.csv")) + goto cleanup; + if (!save_jira_issues_xml(jira_issues,"jira_test_results_add2.xml")) + goto cleanup; + break; + /* Add a new issue using a self-generated template */ + case 4: + summary = + g_strdup_printf("CVE-%s (Test:4)", g_strstrip(ctime(&now))); + if (!build_new_jira_issue(NULL, true, &jira_json)) + goto cleanup; + json_path = "jira_test_results_template_raw.json"; + if (!save(jira_json, json_path)) + goto cleanup; + g_free(jira_json); + if (!load_new_jira_issue_file(json_path, summary, &jira_json)) + goto cleanup; + json_path = "jira_test_results_template_resolved.json"; + if (!save(jira_json, json_path)) + goto cleanup; + if (!add_new_jira_issue(jira_json)) + goto cleanup; + g_free(jira_json); + if (!build_search_jira_issues(&jira_json)) + goto cleanup; + if (!search_jira_issues(jira_json, &jira_issues_json)) + goto cleanup; + if (!parse_jira_issues(jira_issues_json, &jira_issues)) + goto cleanup; + show_jira_issues(jira_issues); + if (!save_jira_issues_csv(jira_issues,"jira_test_results_add3.csv")) + goto cleanup; + if (!save_jira_issues_xml(jira_issues,"jira_test_results_add3.xml")) + goto cleanup; + break; + default: + show_usage(argv[0]); + printf("Error: Unknown test case: %i\n", test_case); + break; + } + printf("Test passed!\n\n"); + destroy_jira_plugin(); + return 0; +cleanup: + printf("Test failed!\n\n"); + destroy_jira_plugin(); + return 1; +} diff --git a/src/plugins/jira/run_tests.sh b/src/plugins/jira/run_tests.sh new file mode 100755 index 0000000..78fd1c1 --- /dev/null +++ b/src/plugins/jira/run_tests.sh @@ -0,0 +1,15 @@ +#!/bin/bash +############################################################################ +# Some simple sanity tests. Make sure you configure jira.cfg file first. +# Warning: Test cases 3 and 4 will insert new issues into Jira server!!! +############################################################################ +jira_cfg_path=jira.cfg +#verbose_flag=-V # Enable if you want to lots of output +rm -f ./jira_test_results* +for i in {1..4} +do + ./jira_test_client -t $i -c $jira_cfg_path $verbose_flag + if [[ $? != 0 ]]; then + exit 1 + fi +done diff --git a/src/rpm.c b/src/rpm.c new file mode 100644 index 0000000..0d2524e --- /dev/null +++ b/src/rpm.c @@ -0,0 +1,216 @@ +/* + * rpm.c - Generic RPM support + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ +#define _GNU_SOURCE +#include +#include + +#include "rpm.h" +#include "util.h" + +bool srpm_is_patched(struct source_package_t *t, char *id) +{ + if (!t->extra) { + return false; + } + + autofree(gchar) *pnamet = g_ascii_strdown((gchar*)id, -1); + autofree(gchar) *pname = g_strdup_printf("%s.patch", pnamet); + gchar **list = t->extra; + + for (int i = 0; i < g_strv_length(list); i++) { + list[i] = g_strchomp(list[i]); + if (g_str_equal(list[i], "")) { + continue; + } + if (g_str_equal(pname, list[i])) { + return true; + } + } + + return false; +} + + +struct source_package_t *rpm_inspect_srpm(const char *dir, const char *name, + const char *version, const char *release) +{ + struct source_package_t *t = NULL; + autofree(gchar) *path = NULL; + autofree(gchar) *cmdline = NULL; + autofree(GError) *error = NULL; + gchar **splits = NULL; + int exit = 0; + autofree(gchar) *output = NULL; + + t = calloc(1, sizeof(struct source_package_t)); + if (!t) { + return NULL; + } + + path = g_strdup_printf("%s%s%s-%s-%s.src.rpm", dir, G_DIR_SEPARATOR_S, + name, version, release); + if (!path) { + free(t); + return NULL; + } + if (!g_file_test(path, G_FILE_TEST_EXISTS)) { + printf("Required source rpm not present: %s\n", path); + free(t); + return NULL; + } + cmdline = g_strdup_printf("rpm -qp --queryformat \'[%%{PATCH}\t]\n\' %s", path); + if (!cmdline) { + free(t); + return NULL; + } + if (!g_spawn_command_line_sync(cmdline, &output, NULL, &exit, &error)) { + g_printerr("Unable to run command: %s\n", error->message); + free(t); + return NULL; + } + if (exit != 0) { + printf("Abnormal exit code for package %s: %s\n", name, output); + } + splits = g_strsplit(output, "\t", -1); + if (g_strv_length(splits) > 0) { + t->extra = splits; + } else { + g_strfreev(splits); + } + + t->name = g_strdup(name); + t->version = g_strdup(version); + return t; +} + + +struct source_package_t *rpm_inspect_spec(const char *filename) +{ + struct source_package_t *t = NULL; + autofree(GFile) *fi = g_file_new_for_path(filename); + autofree(GError) *error = NULL; + if (!fi) { + return NULL; + } + autofree(GFileInputStream) *fis = g_file_read(fi, NULL, &error); + if (error) { + g_printerr("Unable to read: %s\n", error->message); + return NULL; + } + + autofree(GDataInputStream) *dis = g_data_input_stream_new(G_INPUT_STREAM(fis)); + char *read = NULL; + autofree(gchar) *name = NULL; + autofree(gchar) *version = NULL; + autofree(gchar) *release = NULL; + autofree(GHashTable) *macros = NULL; + + while ((read = g_data_input_stream_read_line(dis, NULL, NULL, NULL)) != NULL) { + autofree(gstrv) *strv = NULL; + const gchar *key = NULL; + autofree(gchar) *value = NULL; + + read = g_strstrip(read); + + if (g_str_has_prefix(read, "%define") || g_str_has_prefix(read, "%global")) { + strv = g_strsplit(read, " ", 3); + if (g_strv_length(strv) != 3) { + goto clean; + } + + gchar *mkey = g_strstrip(g_strdup_printf("%%{%s}", strv[1])); + gchar *mvalue = g_strstrip(g_strdup(strv[2])); + + if (!macros) { + macros = g_hash_table_new_full(g_str_hash, g_str_equal, &g_free, &g_free); + } + + if (!g_hash_table_contains(macros, mkey)) { + g_hash_table_insert(macros, mkey, mvalue); + } else { + g_free(mkey); + g_free(mvalue); + } + goto clean; + } + + if (!strchr(read, ':')) { + goto clean; + } + + strv = g_strsplit(read, ":", -1); + if (g_strv_length(strv) < 2) { + goto clean; + } + key = g_strstrip(strv[0]); + value = g_strjoinv(":", strv+1); + value = g_strstrip(value); + + if (g_str_equal(key, "Name")) { + name = g_strdup(value); + } else if (g_str_equal(key, "Version")) { + version = g_strdup(value); + } else if (g_str_equal(key, "Release")) { + release = g_strdup(value); + } + + if (name && version && release) { + g_free(read); + break; + } +clean: + g_free(read); + } + + if (!name || !version || !release) { + return NULL; + } + + if (macros) { + name = demacro(macros, name); + version = demacro(macros, version); + release = demacro(macros, release); + } + + t = calloc(1, sizeof(struct source_package_t)); + if (!t) { + return NULL; + } + t->name = g_strdup(name); + t->version = g_strdup(version); + t->release = atoi(release); + t->path = g_path_get_dirname(filename); + + return t; +} + +bool rpm_is_patched(struct source_package_t *pkg, char *id) +{ + bool ret = false; + /* Determine if its patched. */ + autofree(gchar) *pnamet = g_ascii_strdown((gchar*)id, -1); + autofree(gchar) *pname = g_strdup_printf("%s.patch", pnamet); + autofree(gchar) *tpath = g_build_filename(G_DIR_SEPARATOR_S, pkg->path, pname, NULL); + + ret = g_file_test(tpath, G_FILE_TEST_EXISTS); + return ret; +} + +bool rpm_is_package(const char *filename) +{ + return g_str_has_suffix((const gchar*)filename, ".spec"); +} + +void rpm_locate_sources(const char *directory) +{ + /* TODO: Ensure directory name matches, need non-relative paths.. */ + find_sources(directory, &rpm_is_package); +} diff --git a/src/rpm.h b/src/rpm.h new file mode 100644 index 0000000..a2c9c65 --- /dev/null +++ b/src/rpm.h @@ -0,0 +1,42 @@ +/* + * rpm.h + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#pragma once + +#include "cve-check-tool.h" + +/** + * Inspect an RPM spec file + * + * @param filename Path to the .spec file + * @return a struct source_package_t if successful, otherwise NULL + */ +struct source_package_t *rpm_inspect_spec(const char *filename); + +/** + * Inspect a src.rpm + * + * @param dir Directory where srpm is expected to be located + * @param name Package name + * @param version Package version + * @param release Package release + * @return a struct source_package_t if successful, otherwise NULL + */ + +struct source_package_t *rpm_inspect_srpm(const char *dir, const char *name, + const char *version, const char *release); + +bool srpm_is_patched(struct source_package_t *t, char *id); +bool rpm_is_patched(struct source_package_t *pkg, char *id); + +void rpm_locate_sources(const char *directory); + +bool rpm_is_package(const char *filename); diff --git a/src/template.c b/src/template.c new file mode 100644 index 0000000..a0642aa --- /dev/null +++ b/src/template.c @@ -0,0 +1,94 @@ +/* + * template.c - cve-check-tool + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include "template.h" +#include "util.h" + +#include + +char *template_string(const char *original, GHashTable *keys) +{ + char *c = NULL, *s = NULL; + int offset = 0; + char *ret = NULL; + + if (!keys) { + return (char*) original; + } + if (!original) { + return NULL; + } + + cve_string *input = cve_string_dup(original); + if (!input) { + return NULL; + } + + while ((c = memchr(input->str+offset, '{', input->len-offset))) { + autofree(cve_string) *newstr = NULL; + autofree(cve_string) *left = NULL; + gchar *val = NULL; + + offset = (c - input->str); + + if (*(c+1) != '{') { + ++offset; + goto bail; + } + s = c; + c = memchr(c, '}', input->len); + if (!c) { + ++offset; + goto bail; + } + if (*(c+1) != '}') { + ++offset; + goto bail; + } + int start = (s-input->str); + start+=2; + int end = (c-input->str); + int length = end - start; + + newstr = cve_string_dup(input->str+start); + newstr->str[length] = '\0'; + newstr->len = length; + + left = cve_string_dup(input->str); + left->str[start-2] = '\0'; + left->len = (start-2); + c += 2; + + cve_string *fullstr = NULL; + val = g_hash_table_lookup(keys, newstr->str); + if (val) { + /* Ensure null termination and correct length */ + autofree(cve_string) *dup = cve_string_dup(val); + fullstr = cve_string_dup_printf("%s%s%s", left->str, val, c); + offset += dup->len; + } else { + fullstr = cve_string_dup_printf("%s %s\n", left->str, c); + offset += newstr->len; + } + + cve_string_free(input); + input = fullstr; +bail: + if (offset >= input->len) { + break; + } + continue; + } + + ret = input->str; + free(input); + return ret; +} diff --git a/src/template.h b/src/template.h new file mode 100644 index 0000000..6660dab --- /dev/null +++ b/src/template.h @@ -0,0 +1,27 @@ +/* + * template.h - cve-check-tool + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#pragma once + +#include + + +/** + * Replace all {{style}} variables in a string with the string values + * in the given GHashTable. This method only supports string->string right + * now, however in future support for named sections and lists will be + * added. + * + * @param original The string to perform replacements on + * @param keys A string->string kv GHashTable + * @return A newly allocated string with all replacements performed + */ +char *template_string(const char *original, GHashTable *keys); diff --git a/src/util.c b/src/util.c new file mode 100644 index 0000000..df25589 --- /dev/null +++ b/src/util.c @@ -0,0 +1,252 @@ +/* + * util.c - cve-check-tool + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include + +#include "util.h" +#include "cve-check-tool.h" + +#include "eopkg.h" +#include "rpm.h" + +void find_sources(const char *directory, package_match_func match) +{ + autofree(GFile) *root = NULL; + GFileInfo *info = NULL; + autofree(GFileEnumerator) *enu = NULL; + const gchar* name = NULL; + GFileType type; + + root = g_file_new_for_path(directory); + + enu = g_file_enumerate_children(root, G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL); + if (!enu) { + return; + } + while ((info = g_file_enumerator_next_file(enu, NULL, NULL)) != NULL) { + autofree(gchar) *tpath = NULL; + type = g_file_info_get_file_type(info); + name = g_file_info_get_name(info); + tpath = g_build_path(G_DIR_SEPARATOR_S, directory, g_file_info_get_name(info), NULL); + if (!tpath) { + abort(); + } + + switch (type) { + case G_FILE_TYPE_DIRECTORY: + find_sources(tpath, match); + break; + case G_FILE_TYPE_REGULAR: + if (match(name)) { + cve_add_package((const char*)tpath); + } + break; + default: + /* bail */ + break; + } + g_object_unref(info); + } + root = NULL; +} + +PackageType guess_package_type(gchar *path, bool recurse) +{ + autofree(GFileEnumerator) *enu = NULL; + autofree(GFile) *root = NULL; + GFileInfo *info = NULL; + PackageType type = PACKAGE_TYPE_UNKNOWN; + + if (g_file_test(path, G_FILE_TEST_IS_DIR) && recurse) { + root = g_file_new_for_path(path); + enu = g_file_enumerate_children(root, G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL); + if (!enu) { + return PACKAGE_TYPE_UNKNOWN; + } + while ((info = g_file_enumerator_next_file(enu, NULL, NULL)) != NULL) { + autofree(gchar) *tpath = NULL; + if (g_file_info_get_file_type(info) != G_FILE_TYPE_REGULAR) { + goto end; + } + tpath = g_build_path(G_DIR_SEPARATOR_S, path, g_file_info_get_name(info), NULL); + + if (rpm_is_package(tpath)) { + type = PACKAGE_TYPE_RPM; + g_object_unref(info); + break; + } else if (eopkg_is_package(tpath)) { + type = PACKAGE_TYPE_EOPKG; + g_object_unref(info); + break; + } +end: + g_object_unref(info); + } + return type; + } else { + if (eopkg_is_package(path)) { + return PACKAGE_TYPE_EOPKG; + } else if (rpm_is_package(path)) { + return PACKAGE_TYPE_RPM; + } + } + return PACKAGE_TYPE_UNKNOWN; +} + +bool is_package_list(char *path) +{ + return g_str_has_suffix((gchar*)path, "packages") && + g_file_test((gchar*)path, G_FILE_TEST_IS_REGULAR); +} + + +gchar *demacro(GHashTable *macros, gchar *str) +{ + gchar *key = NULL, *value = NULL; + + if (!macros) { + return str; + } + + while (true) { + bool hit = false; + GHashTableIter iter; + g_hash_table_iter_init(&iter, macros); + while (g_hash_table_iter_next(&iter, (void**)&key, (void**)&value)) { + if (str_contains(str, key)) { + hit = true; + str = str_replace(str, key, value); + } + } + if (!hit) { + break; + } + } + return str; +} + +int64_t parse_xml_date(const char *date) +{ + autofree(cve_string) *tmp = cve_string_dup(date); + autofree(GTimeZone) *tz = NULL; + autofree(GDateTime) *t = NULL, *t2 = NULL; + char *c = NULL; + int64_t ret = -1; + + /* Example XML string: + * 2015-03-05T08:24:10.220-05:00 + */ + if (!tmp) { + return -1; + } + if (!(c = memchr(tmp->str, 'T', tmp->len))) { + return -1; + } + if (!(c = memchr(c, '-', tmp->len - (tmp->str -c)))) { + return -1; + } + gint y, m, d, h, min, s; + if (sscanf(date, "%4d-%2d-%2dT%2d:%2d:%2d", &y, &m, &d, + &h, &min, &s) != 6) { + return -1; + } + tz = g_time_zone_new(c); + if (!tz) { + return -1; + } + + t = g_date_time_new(tz, y, m, d, h, min, (gdouble)s); + if (!t) { + return -1; + } + t2 = g_date_time_to_local(t); + + ret = (int64_t)g_date_time_to_unix(t2); + + return ret; +} + +bool load_cve_mapping(const char *path, char **product, char **vendor) +{ + autofree(GFile) *fi = NULL; + autofree(GFileInputStream) *fis = NULL; + autofree(GDataInputStream) *dis = NULL; + gsize size; + gchar *line = NULL; + gchar *kproduct = NULL; + gchar *kvendor = NULL; + + fi = g_file_new_for_path(path); + if (!fi) { + return false; + } + fis = g_file_read(fi, NULL, NULL); + if (!fis) { + return false; + } + dis = g_data_input_stream_new(G_INPUT_STREAM(fis)); + + while ((line = g_data_input_stream_read_line(dis, &size, NULL, NULL))) { + char *c = NULL; + char *val = NULL; + line = g_strstrip(line); + + if (g_str_equal(line, "")) { + goto clean; + } + if (!(c = memchr(line, '=', size))) { + goto clean; + } + val = c+1; + line[(c-line)] = '\0'; + + line = g_strstrip(line); + val = g_strstrip(val); + + if (g_str_equal(line, "vendor")) { + kvendor = g_strdup(val); + } else if (g_str_equal(line, "product")) { + kproduct = g_strdup(val); + } + +clean: + g_free(line); + } + + if (kvendor && kproduct && product && vendor) { + *vendor = kvendor; + *product = kproduct; + return true; + } + + if (kvendor) { + g_free(kvendor); + } + if (kproduct) { + g_free(kproduct); + } + return false; +} + +gchar *get_absolute_path(const char *path) +{ + autofree(GFile) *file = NULL; + + file = g_file_new_for_path(path); + if (!file) { + return NULL; + } + return g_file_get_path(file); +} diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..bd48058 --- /dev/null +++ b/src/util.h @@ -0,0 +1,208 @@ +/* + * util.h - cve-check-tool + * + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#pragma once + +#include +#include +#include +#include + +#include "cve-string.h" + +/** + * Indicates the (supported) package type + */ +typedef enum { + PACKAGE_TYPE_MIN, + PACKAGE_TYPE_RPM, /**value) + * @param str String to perform replacement on + * + * @return newly allocated string after replacements are performed + */ +gchar *demacro(GHashTable *macros, gchar *str); + + +/** + * Convert an XML formatted date into unix seconds + * + * @param date XML input + * @return int64_t unix timestamp, or -1 if it doesn't parse + */ +int64_t parse_xml_date(const char *date); + +/** + * Search the repo source directory for all matching sources + * + * @param directory Base directory to recurse + * @param match A function to determine "matching" source packages + */ +void find_sources(const char *directory, package_match_func match); + +/** + * Utility method to get absolute path of filename + * + * @param path Input path, if any + * @return newly allocated string defining the absolute path + */ +gchar *get_absolute_path(const char *path); + +/** + * Implemented in a *similar* fashion to how g_autoptr is intended to + * work in future, but without the concerns of MSVC, etc.. + */ +#define DEF_AUTOFREE(N,C) \ + static inline void _autofree_func_##N (void *p) {\ + if (p && *(N**)p) { \ + /* To debug: printf("Freeing %s\n", #N); */\ + C (*(N**)p);\ + (*(void**)p) = NULL;\ + } \ + } + +#define autofree(N) __attribute__ ((cleanup( _autofree_func_##N ))) N + +/** + * Enable easier integration with autofree. Note this is still a single + * pointer, you need to use it like: gstrv*, NOT gstrv. + */ +typedef gchar* gstrv; + +/** + * Autofree helper: Cleanup a GFileEnumerator + * + * @param enu A valid GFileEnumerator pointer + */ +static inline void cve_io_enum_close(GFileEnumerator *enu) +{ + if (!enu) { + return; + } + g_file_enumerator_close(enu, NULL, NULL); + g_object_unref(enu); +} + +/** + * We don't allow direct use, as this handles multiple GInputStream + * subclasses.. */ +static inline void cve_io_close(void *stream) +{ + if (!stream) { + return; + } + g_input_stream_close(stream, NULL, NULL); + g_object_unref(stream); +} +static inline void cve_io_close_out(void *stream) +{ + if (!stream) { + return; + } + g_output_stream_close(stream, NULL, NULL); + g_object_unref(stream); +} + +/** + * Load mapping to real product name from the given ini file + * + * @note product/vendor will be newly allocated strings + * + * @param product Where to store the product name + * @param vendor Where to store the product vendor name + * @return a boolean value, indicating success or failure + */ +bool load_cve_mapping(const char *path, char **product, char **vendor); + +DEF_AUTOFREE(GDateTime, g_date_time_unref) +DEF_AUTOFREE(GOptionContext, g_option_context_free) +DEF_AUTOFREE(gchar, g_free) +DEF_AUTOFREE(GError, g_error_free) +DEF_AUTOFREE(gstrv, g_strfreev) +DEF_AUTOFREE(GFileEnumerator, cve_io_enum_close) +DEF_AUTOFREE(GFile, g_object_unref) +DEF_AUTOFREE(GFileInputStream, cve_io_close) +DEF_AUTOFREE(GFileOutputStream, cve_io_close_out) +DEF_AUTOFREE(GOutputStream, cve_io_close_out) +DEF_AUTOFREE(GDataInputStream, cve_io_close) +DEF_AUTOFREE(GHashTable, g_hash_table_unref) +DEF_AUTOFREE(GTimeZone, g_time_zone_unref) +DEF_AUTOFREE(GZlibDecompressor, g_object_unref) +DEF_AUTOFREE(GKeyFile, g_key_file_unref) +DEF_AUTOFREE(cve_string, cve_string_free) + +/** + * Suger-utility: Determine if a string contains a certain word + * + * @param word String to check + * @param needle "word" to search for + * + * @return a boolean value, true if the string contains the word + */ +static inline bool str_contains(const gchar *word, const gchar *needle) +{ + return strstr(word, needle) != NULL; +} + +/** + * Utility to replace one word with another in a string + * + * @note Only use on allocated strings, as this will free the original + * string! + * + * @param source (Allocated) source string to perform replacement on + * @param word Word to replace + * @param replace Replacement word + * + * @return A newly allocated string with the replacement performed + */ +static inline gchar *str_replace(gchar *source, const gchar *word, const gchar *replace) +{ + autofree(gstrv) *splits = NULL; + gchar *ret = NULL; + splits = g_strsplit(source, word, -1); + ret = g_strjoinv(replace, splits); + g_free(source); + return ret; +} diff --git a/tests/Makefile.am b/tests/Makefile.am new file mode 100644 index 0000000..7c38221 --- /dev/null +++ b/tests/Makefile.am @@ -0,0 +1,25 @@ +-include $(top_srcdir)/common.mk + + +TESTS = \ + check_core + +check_PROGRAMS = \ + check_core + + +check_core_SOURCES = \ + check-core.c + +check_core_CFLAGS = \ + $(GLIB_CFLAGS) \ + $(GIO_CFLAGS) \ + $(LIBXML2_CFLAGS) \ + $(AM_CFLAGS) \ + $(CHECK_CFLAGS) + +check_core_LDADD = \ + $(GLIB_LIBS) \ + $(GIO_LIBS) \ + $(LIBXML2_LIBS) \ + $(CHECK_LIBS) diff --git a/tests/check-core.c b/tests/check-core.c new file mode 100644 index 0000000..8e8b0cb --- /dev/null +++ b/tests/check-core.c @@ -0,0 +1,108 @@ +/* + * This file is part of cve-check-tool + * Copyright (C) 2015 Intel Corporation + * + * cve-check-tool is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#define _GNU_SOURCE +#include +#include +#include + +#include "cve-string.c" +#include "util.h" +#include "util.c" +#include "rpm.c" +#include "eopkg.c" + +#include "config.h" + +/** + * Kept here as a no-op for now (linking) + */ +void cve_add_package(const char *path) +{ + +} + +/** + * Ensure parse_xml_date works + */ +START_TEST(cve_date_function) +{ + const char *date = "2015-03-10T08:24:10.220-05:00"; + int64_t t1, t2; + + t1 = parse_xml_date(date); + fail_if(t1 < 0, "Failed to parse XML date"); + + date = "2015-03-10T08:34:10.220-05:00"; + t2 = parse_xml_date(date); + fail_if(t2 < 0, "Failed to parse second XML date"); + + fail_if(t1 > t2, "XML Date 1 should be less than Date 2"); +} +END_TEST + +/** + * Ensure scope based management is functional + */ +static bool reclaimed = false; +static void reaper(void *v) +{ + free(v); + v = NULL; + fprintf(stderr, "Freeing tmp var\n"); + reclaimed = true; +} +DEF_AUTOFREE(char, reaper) + +START_TEST(cve_memory_test) +{ + { + autofree(char) *tmp = NULL; + + if (!asprintf(&tmp, "Allocation test")) { + fail("Unable to allocate memory"); + } + } + fail_if(!reclaimed, "Scope based tmp var was not reclaimed!"); +} +END_TEST + +static Suite *core_suite(void) +{ + Suite *s = NULL; + TCase *tc = NULL; + + s = suite_create("cve_core"); + tc = tcase_create("cve_core_functions"); + tcase_add_test(tc, cve_date_function); + tcase_add_test(tc, cve_memory_test); + suite_add_tcase(s, tc); + + return s; +} + +int main(void) +{ + Suite *s; + SRunner *sr; + int fail; + + s = core_suite(); + sr = srunner_create(s); + srunner_run_all(sr, CK_VERBOSE); + fail = srunner_ntests_failed(sr); + srunner_free(sr); + + if (fail > 0) { + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +}
CVEPackageDescriptionCVSS (Score)Status