From c089ac330abe93661365cd94353f673e3c307bfa Mon Sep 17 00:00:00 2001 From: Jeremy Long Date: Fri, 14 Sep 2012 06:14:54 -0400 Subject: [PATCH] improved downloading of CPE data --- NOTICES.txt | 3 - README.txt | 4 +- pom.xml | 23 +--- .../dependencycheck/data/cpe/Index.java | 114 ++++++++++++------ .../dependencycheck/utils/Downloader.java | 32 ++++- .../dependencycheck/utils/Settings.java | 14 +-- .../META-INF/dependencycheck.properties | 4 +- .../data/cpe/CPEQueryTest.java | 44 ++++--- .../dependencycheck/data/cpe/IndexTest.java | 103 ++++++++++++++++ 9 files changed, 247 insertions(+), 94 deletions(-) create mode 100644 src/test/java/org/codesecure/dependencycheck/data/cpe/IndexTest.java diff --git a/NOTICES.txt b/NOTICES.txt index f3ccac1cf..a2184354c 100644 --- a/NOTICES.txt +++ b/NOTICES.txt @@ -4,8 +4,5 @@ Copyright (c) 2012 Jeremy Long. All Rights Reserved. This product includes software developed by The Apache Software Foundation (http://www.apache.org/). -This product includes software developed by -Joda.org (http://www.joda.org/). - This product includes software developed by Jquery.com (http://jquery.com/). \ No newline at end of file diff --git a/README.txt b/README.txt index b4dc2e4ea..e14638718 100644 --- a/README.txt +++ b/README.txt @@ -1,7 +1,7 @@ About: -DependencyCheck is a simple utility that attempts to detect publically disclosed +DependencyCheck is a utility that attempts to detect publically disclosed vulnerabilities contained within project dependencies. It does this by determining -if there is a Common Product Enumeration (CPE) identifier for a given dependency. +if there is a Common Platform Enumeration (CPE) identifier for a given dependency. If found, it will generate a report linking to the associated CVE entries. Usage: diff --git a/pom.xml b/pom.xml index 0b54d8e57..74a4c9202 100644 --- a/pom.xml +++ b/pom.xml @@ -27,8 +27,8 @@ along with DependencyCheck. If not, see . jar DependencyCheck - http://codesecure.blogspot.com - DependencyCheck is a simple utility that attempts to determine if there is a Common Product Enumeration (CPE) identifier for a given project dependency. If found, it will generate a report linking to the associated CVE entries. + https://github.com/jeremylong/DependencyCheck.git + DependencyCheck is a utility that attempts to detect publically disclosed vulnerabilities contained within project dependencies. It does this by determining if there is a Common Platform Enumeration (CPE) identifier for a given dependency. If found, it will generate a report linking to the associated CVE entries. Jeremy Long @@ -436,24 +436,5 @@ along with DependencyCheck. If not, see . javadoc provided - - joda-time - joda-time - 2.1 - - - joda-time - joda-time - 2.1 - javadoc - provided - - - joda-time - joda-time - 2.1 - sources - provided - diff --git a/src/main/java/org/codesecure/dependencycheck/data/cpe/Index.java b/src/main/java/org/codesecure/dependencycheck/data/cpe/Index.java index f49add80b..ff592f3d4 100644 --- a/src/main/java/org/codesecure/dependencycheck/data/cpe/Index.java +++ b/src/main/java/org/codesecure/dependencycheck/data/cpe/Index.java @@ -23,6 +23,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.MalformedURLException; @@ -47,8 +48,6 @@ import org.codesecure.dependencycheck.utils.Downloader; import org.codesecure.dependencycheck.utils.Settings; import org.codesecure.dependencycheck.data.cpe.xml.Importer; import org.codesecure.dependencycheck.utils.DownloadFailedException; -import org.joda.time.DateTime; -import org.joda.time.Days; import org.xml.sax.SAXException; /** @@ -140,23 +139,24 @@ public class Index { * @throws IOException is thrown if a temporary file could not be created. */ public void updateIndexFromWeb() throws MalformedURLException, ParserConfigurationException, SAXException, IOException { - if (updateNeeded()) { + long timeStamp = updateNeeded(); + if (timeStamp > 0) { URL url = new URL(Settings.getString(Settings.KEYS.CPE_URL)); File outputPath = null; try { outputPath = File.createTempFile("cpe", ".xml"); - Downloader.fetchFile(url, outputPath); + Downloader.fetchFile(url, outputPath, true); Importer.importXML(outputPath.toString()); - writeLastUpdatedPropertyFile(); - + writeLastUpdatedPropertyFile(timeStamp); } catch (DownloadFailedException ex) { Logger.getLogger(Index.class.getName()).log(Level.SEVERE, null, ex); } finally { - boolean deleted = false; try { - deleted = outputPath.delete(); + if (outputPath != null && outputPath.exists()) { + outputPath.delete(); + } } finally { - if (!deleted) { + if (outputPath != null && outputPath.exists()) { outputPath.deleteOnExit(); } } @@ -164,12 +164,15 @@ public class Index { } } - private void writeLastUpdatedPropertyFile() { - DateTime now = new DateTime(); + /** + * Writes a properties file containing the last updated date to the CPE directory. + * @param timeStamp the timestamp to write. + */ + private void writeLastUpdatedPropertyFile(long timeStamp) { String dir = Settings.getString(Settings.KEYS.CPE_INDEX); File cpeProp = new File(dir + File.separatorChar + UPDATE_PROPERTIES_FILE); Properties prop = new Properties(); - prop.put(this.LAST_UPDATED, String.valueOf(now.getMillis())); + prop.put(Index.LAST_UPDATED, String.valueOf(timeStamp)); OutputStream os = null; try { os = new FileOutputStream(cpeProp); @@ -194,48 +197,91 @@ public class Index { } /** - * Determines if the index needs to be updated. + * Determines if the index needs to be updated. This is done by fetching the + * cpe.meta data and checking the lastModifiedDate. If the CPE data needs to + * be refreshed this method will return the timestamp of the new CPE. If an + * update is not required this function will return 0. * - * @return whether or not the CPE Index needs to be updated. + * @return the timestamp of the currently published CPE.xml if the index needs to be updated, otherwise returns 0.. + * @throws MalformedURLException is thrown if the URL for the CPE Meta data is incorrect. + * @throws DownloadFailedException is thrown if there is an error downloading the cpe.meta data file. */ - public boolean updateNeeded() { - boolean needed = false; - String lastUpdated = null; + public long updateNeeded() throws MalformedURLException, DownloadFailedException { + long retVal = 0; + long lastUpdated = 0; + long currentlyPublishedDate = retrieveCurrentCPETimestampFromWeb(); + if (currentlyPublishedDate == 0) { + throw new DownloadFailedException("Unable to retrieve valid timestamp from cpe.meta file"); + } + String dir = Settings.getString(Settings.KEYS.CPE_INDEX); File f = new File(dir); if (!f.exists()) { - needed = true; + retVal = currentlyPublishedDate; } else { File cpeProp = new File(dir + File.separatorChar + UPDATE_PROPERTIES_FILE); if (!cpeProp.exists()) { - needed = true; + retVal = currentlyPublishedDate; } else { Properties prop = new Properties(); - FileInputStream is = null; + InputStream is = null; try { is = new FileInputStream(cpeProp); prop.load(is); - lastUpdated = prop.getProperty(this.LAST_UPDATED); + lastUpdated = Long.parseLong(prop.getProperty(Index.LAST_UPDATED)); } catch (FileNotFoundException ex) { Logger.getLogger(Index.class.getName()).log(Level.FINEST, null, ex); } catch (IOException ex) { Logger.getLogger(Index.class.getName()).log(Level.FINEST, null, ex); - } - try { - long lastupdate = Long.parseLong(lastUpdated); - DateTime last = new DateTime(lastupdate); - DateTime now = new DateTime(); - Days d = Days.daysBetween(last, now); - int days = d.getDays(); - int freq = Settings.getInt(Settings.KEYS.CPE_DOWNLOAD_FREQUENCY); - if (days >= freq) { - needed = true; - } } catch (NumberFormatException ex) { - needed = true; + Logger.getLogger(Index.class.getName()).log(Level.FINEST, null, ex); + } + if (currentlyPublishedDate > lastUpdated) { + retVal = currentlyPublishedDate; } } } - return needed; + return retVal; + } + + /** + * Retrieves the timestamp from the CPE meta data file. + * @return the timestamp from the currently published cpe.meta. + * @throws MalformedURLException is thrown if the URL for the CPE Meta data is incorrect. + * @throws DownloadFailedException is thrown if there is an error downloading the cpe.meta data file. + */ + private long retrieveCurrentCPETimestampFromWeb() throws MalformedURLException, DownloadFailedException { + long timestamp = 0; + File tmp = null; + InputStream is = null; + try { + tmp = File.createTempFile("cpe", "meta"); + URL url = new URL(Settings.getString(Settings.KEYS.CPE_META_URL)); + Downloader.fetchFile(url, tmp); + Properties prop = new Properties(); + is = new FileInputStream(tmp); + prop.load(is); + timestamp = Long.parseLong(prop.getProperty("lastModifiedDate")); + } catch (IOException ex) { + throw new DownloadFailedException("Unable to create temporary file for CPE Meta File download.", ex); + } finally { + try { + if (is != null) { + try { + is.close(); + } catch (IOException ex) { + Logger.getLogger(Index.class.getName()).log(Level.FINEST, null, ex); + } + } + if (tmp != null && tmp.exists()) { + tmp.delete(); + } + } finally { + if (tmp != null && tmp.exists()) { + tmp.deleteOnExit(); + } + } + } + return timestamp; } } diff --git a/src/main/java/org/codesecure/dependencycheck/utils/Downloader.java b/src/main/java/org/codesecure/dependencycheck/utils/Downloader.java index 229197cec..3a984a1a5 100644 --- a/src/main/java/org/codesecure/dependencycheck/utils/Downloader.java +++ b/src/main/java/org/codesecure/dependencycheck/utils/Downloader.java @@ -30,6 +30,7 @@ import java.net.SocketAddress; import java.net.URL; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.zip.GZIPInputStream; /** * A utility to download files from the Internet. @@ -51,8 +52,19 @@ public class Downloader { * @throws DownloadFailedException is thrown if there is an error downloading the file. */ public static void fetchFile(URL url, String outputPath) throws DownloadFailedException { + fetchFile(url, outputPath, false); + } + + /** + * Retrieves a file from a given URL and saves it to the outputPath. + * @param url the URL of the file to download. + * @param outputPath the path to the save the file to. + * @param unzip true/false indicating that the file being retrieved is gzipped and if true, should be uncompressed before writting to the file. + * @throws DownloadFailedException is thrown if there is an error downloading the file. + */ + public static void fetchFile(URL url, String outputPath, boolean unzip) throws DownloadFailedException { File f = new File(outputPath); - fetchFile(url, f); + fetchFile(url, f, unzip); } /** @@ -62,6 +74,17 @@ public class Downloader { * @throws DownloadFailedException is thrown if there is an error downloading the file. */ public static void fetchFile(URL url, File outputPath) throws DownloadFailedException { + fetchFile(url, outputPath, false); + } + + /** + * Retrieves a file from a given URL and saves it to the outputPath. + * @param url the URL of the file to download. + * @param outputPath the path to the save the file to. + * @param unzip true/false indicating that the file being retrieved is gzipped and if true, should be uncompressed before writting to the file. + * @throws DownloadFailedException is thrown if there is an error downloading the file. + */ + public static void fetchFile(URL url, File outputPath, boolean unzip) throws DownloadFailedException { HttpURLConnection conn = null; Proxy proxy = null; String proxyUrl = Settings.getString(Settings.KEYS.PROXY_URL); @@ -96,7 +119,12 @@ public class Downloader { try { //the following times out on some systems because the CPE is big. //InputStream reader = url.openStream(); - InputStream reader = conn.getInputStream(); + InputStream reader; + if (unzip) { + reader = new GZIPInputStream(conn.getInputStream()); + } else { + reader = conn.getInputStream(); + } writer = new BufferedOutputStream(new FileOutputStream(outputPath)); byte[] buffer = new byte[4096]; diff --git a/src/main/java/org/codesecure/dependencycheck/utils/Settings.java b/src/main/java/org/codesecure/dependencycheck/utils/Settings.java index a8194547a..c6b3985c2 100644 --- a/src/main/java/org/codesecure/dependencycheck/utils/Settings.java +++ b/src/main/java/org/codesecure/dependencycheck/utils/Settings.java @@ -47,7 +47,7 @@ public class Settings { /** * The properties key for the URL to the CPE. */ - public static final String CPE_DOWNLOAD_FREQUENCY = "cpe.downloadfrequency"; + public static final String CPE_META_URL = "cpe.meta.url"; /** * The properties key for the path where the CCE Lucene Index will be stored. */ @@ -69,8 +69,8 @@ public class Settings { */ public static final String CONNECTION_TIMEOUT = "connection.timeout"; } - private static final String PROPERTIES_FILE = "dependencycheck.properties"; - private static Settings instance = new Settings(); + private static final String PROPERTIES_FILE = "META-INF/dependencycheck.properties"; + private static final Settings INSTANCE = new Settings(); private Properties props = null; /** @@ -97,7 +97,7 @@ public class Settings { * @return the property from the properties file. */ public static String getString(String key, String defaultValue) { - String str = System.getProperty(key, instance.props.getProperty(key)); + String str = System.getProperty(key, INSTANCE.props.getProperty(key)); if (str == null) { str = defaultValue; } @@ -110,7 +110,7 @@ public class Settings { * @param value the value for the property. */ public static void setString(String key, String value) { - instance.props.setProperty(key, value); + INSTANCE.props.setProperty(key, value); } /** @@ -123,7 +123,7 @@ public class Settings { * @return the property from the properties file. */ public static String getString(String key) { - return System.getProperty(key, instance.props.getProperty(key)); + return System.getProperty(key, INSTANCE.props.getProperty(key)); } /** @@ -151,4 +151,4 @@ public class Settings { public static boolean getBoolean(String key) { return Boolean.parseBoolean(Settings.getString(key)); } -} +} \ No newline at end of file diff --git a/src/main/resources/META-INF/dependencycheck.properties b/src/main/resources/META-INF/dependencycheck.properties index 7527480e0..c77202aa4 100644 --- a/src/main/resources/META-INF/dependencycheck.properties +++ b/src/main/resources/META-INF/dependencycheck.properties @@ -2,8 +2,8 @@ application.name=${pom.name} application.version=${pom.version} cpe=store/cpe -cpe.url=http://static.nvd.nist.gov/feeds/xml/cpe/dictionary/official-cpe-dictionary_v2.2.xml -cpe.downloadfrequency=1 +cpe.url=http://static.nvd.nist.gov/feeds/xml/cpe/dictionary/official-cpe-dictionary_v2.2.xml.gz +cpe.meta.url=http://static.nvd.nist.gov/feeds/xml/cpe/dictionary/official-cpe-dictionary_v2.2.meta cve=store/cve osvdb=store/osvdb diff --git a/src/test/java/org/codesecure/dependencycheck/data/cpe/CPEQueryTest.java b/src/test/java/org/codesecure/dependencycheck/data/cpe/CPEQueryTest.java index 6f890a5b5..620976e46 100644 --- a/src/test/java/org/codesecure/dependencycheck/data/cpe/CPEQueryTest.java +++ b/src/test/java/org/codesecure/dependencycheck/data/cpe/CPEQueryTest.java @@ -21,16 +21,16 @@ import org.junit.Test; * @author jeremy */ public class CPEQueryTest extends BaseIndexTestCase { - + public CPEQueryTest(String testName) { super(testName); } - + @Override protected void setUp() throws Exception { super.setUp(); } - + @Override protected void tearDown() throws Exception { super.tearDown(); @@ -51,15 +51,15 @@ public class CPEQueryTest extends BaseIndexTestCase { String expResult = "cpe:/a:apache:struts:2.1.2"; List result = instance.searchCPE(vendor, product, version); assertEquals(expResult, result.get(0).getName()); - + //TODO - yeah, not a very good test as the results are the same with or without weighting... Set productWeightings = new HashSet(1); productWeightings.add("struts2"); Set vendorWeightings = new HashSet(1); vendorWeightings.add("apache"); - - result = instance.searchCPE(vendor, product, version,productWeightings,vendorWeightings); + + result = instance.searchCPE(vendor, product, version, productWeightings, vendorWeightings); assertEquals(expResult, result.get(0).getName()); vendor = "apache software foundation"; @@ -67,13 +67,13 @@ public class CPEQueryTest extends BaseIndexTestCase { version = "2.3.1.2"; //yes, this isn't right. we verify this with another method later - expResult = "cpe:/a:apache:struts"; + expResult = "cpe:/a:apache:struts"; result = instance.searchCPE(vendor, product, version); boolean startsWith = result.get(0).getName().startsWith(expResult); - assertTrue("CPE does not begin with apache struts",startsWith); + assertTrue("CPE does not begin with apache struts", startsWith); instance.close(); } - + /** * Tests of buildSearch of class CPEQuery. * @throws IOException is thrown when an IO Exception occurs. @@ -97,15 +97,15 @@ public class CPEQueryTest extends BaseIndexTestCase { String queryText = instance.buildSearch(vendor, product, version, null, null); String expResult = " product:( struts 2 core ) vendor:( apache software foundation ) version:(2.1.2)"; assertTrue(expResult.equals(queryText)); - + queryText = instance.buildSearch(vendor, product, version, null, productWeightings); expResult = " product:( struts^5 struts2^5 2 core ) vendor:( apache software foundation ) version:(2.1.2^0.2 )"; assertTrue(expResult.equals(queryText)); - - queryText = instance.buildSearch(vendor, product, version,vendorWeightings,null); + + queryText = instance.buildSearch(vendor, product, version, vendorWeightings, null); expResult = " product:( struts 2 core ) vendor:( apache^5 software foundation ) version:(2.1.2^0.2 )"; assertTrue(expResult.equals(queryText)); - + queryText = instance.buildSearch(vendor, product, version, vendorWeightings, productWeightings); expResult = " product:( struts^5 struts2^5 2 core ) vendor:( apache^5 software foundation ) version:(2.1.2^0.2 )"; assertTrue(expResult.equals(queryText)); @@ -126,7 +126,6 @@ public class CPEQueryTest extends BaseIndexTestCase { assertFalse(instance.isOpen()); } - /** * Test of determineCPE method, of class CPEQuery. * @throws Exception is thrown when an exception occurs @@ -143,7 +142,7 @@ public class CPEQueryTest extends BaseIndexTestCase { instance.determineCPE(depends); instance.close(); assertTrue(depends.getCPEs().contains(expResult)); - assertTrue(depends.getCPEs().size()==1); + assertTrue(depends.getCPEs().size() == 1); } @@ -163,7 +162,7 @@ public class CPEQueryTest extends BaseIndexTestCase { String expResult = "cpe:/a:apache:struts:2.1.2"; List result = instance.searchCPE(vendor, product, version); assertEquals(expResult, result.get(0).getName()); - + vendor = "apache software foundation"; product = "struts 2 core"; version = "2.3.1.2"; @@ -172,7 +171,7 @@ public class CPEQueryTest extends BaseIndexTestCase { result = instance.searchCPE(vendor, product, version); boolean startsWith = result.get(0).getName().startsWith(expResult); assertTrue("CPE Does not start with apache struts.", startsWith); - + instance.close(); } @@ -187,22 +186,21 @@ public class CPEQueryTest extends BaseIndexTestCase { String product = "struts 2 core"; String version = "2.1.2"; String expResult = "cpe:/a:apache:struts:2.1.2"; - + CPEQuery instance = new CPEQuery(); instance.open(); - + //TODO - yeah, not a very good test as the results are the same with or without weighting... Set productWeightings = new HashSet(1); productWeightings.add("struts2"); Set vendorWeightings = new HashSet(1); vendorWeightings.add("apache"); - - List result = instance.searchCPE(vendor, product, version,productWeightings,vendorWeightings); + + List result = instance.searchCPE(vendor, product, version, productWeightings, vendorWeightings); assertEquals(expResult, result.get(0).getName()); - + instance.close(); } - } diff --git a/src/test/java/org/codesecure/dependencycheck/data/cpe/IndexTest.java b/src/test/java/org/codesecure/dependencycheck/data/cpe/IndexTest.java new file mode 100644 index 000000000..c88d05c66 --- /dev/null +++ b/src/test/java/org/codesecure/dependencycheck/data/cpe/IndexTest.java @@ -0,0 +1,103 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.codesecure.dependencycheck.data.cpe; + +import org.codesecure.dependencycheck.data.BaseIndexTestCase; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.store.Directory; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * + * @author Jeremy Long (jeremy.long@gmail.com) + */ +public class IndexTest extends BaseIndexTestCase { + + public IndexTest(String testCase) { + super(testCase); + } + + @BeforeClass + public static void setUpClass() throws Exception { + } + + @AfterClass + public static void tearDownClass() throws Exception { + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + /** + * Test of open method, of class Index. + */ + @Test + public void testOpen() { + System.out.println("open"); + Index instance = new Index(); + try { + instance.open(); + } catch (IOException ex) { + fail(ex.getMessage()); + } + try { + instance.close(); + } catch (CorruptIndexException ex) { + fail(ex.getMessage()); + } catch (IOException ex) { + fail(ex.getMessage()); + } + } + + /** + * Test of getDirectory method, of class Index. + */ + @Test + public void testGetDirectory() throws Exception { + System.out.println("getDirectory"); + Directory result = Index.getDirectory(); + String exp = "\\target\\store\\cpe"; + // TODO review the generated test code and remove the default call to fail. + assertTrue(result.toString().contains(exp)); + } + + /** + * Test of updateIndexFromWeb method, of class Index. + */ + @Test + public void testUpdateIndexFromWeb() throws Exception { + System.out.println("updateIndexFromWeb"); + Index instance = new Index(); + instance.updateIndexFromWeb(); + } + + /** + * Test of updateNeeded method, of class Index. + */ + @Test + public void testUpdateNeeded() throws Exception { + System.out.println("updateNeeded"); + Index instance = new Index(); + long expResult = 0L; + long result = instance.updateNeeded(); + //if an exception is thrown this test fails. However, because it depends on the + // order of the tests what this will return I am just testing for the exception. + //assertTrue(expResult < result); + } +}