##### Coverage Report - org.owasp.dependencycheck.analyzer.CPEAnalyzer

Classes in this File | Line Coverage | Branch Coverage | Complexity
CPEAnalyzer |
89% | 175/196
84% | 108/128
| 4.0625;4.062
CPEAnalyzer$IdentifierConfidence |
100% | 3/3
N/A | N/A
| 4.0625;4.062
CPEAnalyzer$IdentifierMatch |
38% | 15/39
16% | 4/24
| 4.0625;4.062

1 |  |
     /*
2 |  |
      * This file is part of dependency-check-core.
3 |  |
      *
4 |  |
      * Dependency-check-core is free software: you can redistribute it and/or modify it
5 |  |
      * under the terms of the GNU General Public License as published by the Free
6 |  |
      * Software Foundation, either version 3 of the License, or (at your option) any
7 |  |
      * later version.
8 |  |
      *
9 |  |
      * Dependency-check-core is distributed in the hope that it will be useful, but
10 |  |
      * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 |  |
      * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12 |  |
      * details.
13 |  |
      *
14 |  |
      * You should have received a copy of the GNU General Public License along with
15 |  |
      * dependency-check-core. If not, see http://www.gnu.org/licenses/.
16 |  |
      *
17 |  |
      * Copyright (c) 2012 Jeremy Long. All Rights Reserved.
18 |  |
      */
19 |  |
     package org.owasp.dependencycheck.analyzer;
20 |  |
21 |  |
     import java.io.IOException;
22 |  |
     import java.io.UnsupportedEncodingException;
23 |  |
     import java.net.URLEncoder;
24 |  |
     import java.sql.SQLException;
25 |  |
     import java.util.ArrayList;
26 |  |
     import java.util.Collections;
27 |  |
     import java.util.List;
28 |  |
     import java.util.Set;
29 |  |
     import java.util.StringTokenizer;
30 |  |
     import java.util.logging.Level;
31 |  |
     import java.util.logging.Logger;
32 |  |
     import org.apache.lucene.document.Document;
33 |  |
     import org.apache.lucene.index.CorruptIndexException;
34 |  |
     import org.apache.lucene.queryparser.classic.ParseException;
35 |  |
     import org.apache.lucene.search.ScoreDoc;
36 |  |
     import org.apache.lucene.search.TopDocs;
37 |  |
     import org.owasp.dependencycheck.Engine;
38 |  |
     import org.owasp.dependencycheck.data.lucene.LuceneUtils;
39 |  |
     import org.owasp.dependencycheck.dependency.Dependency;
40 |  |
     import org.owasp.dependencycheck.dependency.Evidence;
41 |  |
     import org.owasp.dependencycheck.dependency.Evidence.Confidence;
42 |  |
     import org.owasp.dependencycheck.dependency.EvidenceCollection;
43 |  |
     import org.owasp.dependencycheck.data.cpe.CpeIndexReader;
44 |  |
     import org.owasp.dependencycheck.data.cpe.Fields;
45 |  |
     import org.owasp.dependencycheck.data.cpe.IndexEntry;
46 |  |
     import org.owasp.dependencycheck.data.nvdcve.CveDB;
47 |  |
     import org.owasp.dependencycheck.data.nvdcve.DatabaseException;
48 |  |
     import org.owasp.dependencycheck.dependency.Identifier;
49 |  |
     import org.owasp.dependencycheck.dependency.VulnerableSoftware;
50 |  |
     import org.owasp.dependencycheck.utils.DependencyVersion;
51 |  |
     import org.owasp.dependencycheck.utils.DependencyVersionUtil;
52 |  |
53 |  |
     /**
54 |  |
      * CPEAnalyzer is a utility class that takes a project dependency and attempts
55 |  |
      * to discern if there is an associated CPE. It uses the evidence contained
56 |  |
      * within the dependency to search the Lucene index.
57 |  |
      *
58 |  |
      * @author Jeremy Long (jeremy.long@owasp.org)
59 |  |
      */
60 | 11 |
     public class CPEAnalyzer implements Analyzer {
61 |  |
62 |  |
         /**
63 |  |
          * The maximum number of query results to return.
64 |  |
          */
65 |  |
         static final int MAX_QUERY_RESULTS = 25;
66 |  |
         /**
67 |  |
          * The weighting boost to give terms when constructing the Lucene query.
68 |  |
          */
69 |  |
         static final String WEIGHTING_BOOST = "^5";
70 |  |
         /**
71 |  |
          * A string representation of a regular expression defining characters
72 |  |
          * utilized within the CPE Names.
73 |  |
          */
74 |  |
         static final String CLEANSE_CHARACTER_RX = "[^A-Za-z0-9 ._-]";
75 |  |
         /**
76 |  |
          * A string representation of a regular expression used to remove all but
77 |  |
          * alpha characters.
78 |  |
          */
79 |  |
         static final String CLEANSE_NONALPHA_RX = "[^A-Za-z]*";
80 |  |
         /**
81 |  |
          * The additional size to add to a new StringBuilder to account for extra
82 |  |
          * data that will be written into the string.
83 |  |
          */
84 |  |
         static final int STRING_BUILDER_BUFFER = 20;
85 |  |
         /**
86 |  |
          * The CPE Index Reader.
87 |  |
          */
88 |  |
         private CpeIndexReader cpe;
89 |  |
         /**
90 |  |
          * The CVE Database.
91 |  |
          */
92 |  |
         private CveDB cve;
93 |  |
94 |  |
         /**
95 |  |
          * Opens the data source.
96 |  |
          *
97 |  |
          * @throws IOException when the Lucene directory to be queried does not
98 |  |
          * exist or is corrupt.
99 |  |
          * @throws DatabaseException when the database throws an exception. This
100 |  |
          * usually occurs when the database is in use by another process.
101 |  |
          */
102 |  |
         public void open() throws IOException, DatabaseException {
103 | 12 |
             cpe = new CpeIndexReader();
104 | 12 |
             cpe.open();
105 | 12 |
             cve = new CveDB();
106 |  |
             try {
107 | 12 |
                 cve.open();
108 | 0 |
             } catch (SQLException ex) {
109 | 0 |
                 Logger.getLogger(CPEAnalyzer.class.getName()).log(Level.FINE, null, ex);
110 | 0 |
                 throw new DatabaseException("Unable to open the cve db", ex);
111 | 0 |
             } catch (ClassNotFoundException ex) {
112 | 0 |
                 Logger.getLogger(CPEAnalyzer.class.getName()).log(Level.FINE, null, ex);
113 | 0 |
                 throw new DatabaseException("Unable to open the cve db", ex);
114 | 12 |
             }
115 | 12 |
         }
116 |  |
117 |  |
         /**
118 |  |
          * Closes the data source.
119 |  |
          */
120 |  |
         @Override
121 |  |
         public void close() {
122 | 12 |
             if (cpe != null) {
123 | 12 |
                 cpe.close();
124 |  |
             }
125 | 12 |
             if (cve != null) {
126 | 12 |
                 cve.close();
127 |  |
             }
128 | 12 |
         }
129 |  |
130 |  |
         /**
131 |  |
          * Returns the status of the data source - is the index open.
132 |  |
          *
133 |  |
          * @return true or false.
134 |  |
          */
135 |  |
         public boolean isOpen() {
136 | 10 |
             return (cpe != null) && cpe.isOpen();
137 |  |
         }
138 |  |
139 |  |
         /**
140 |  |
          * Ensures that the Lucene index is closed.
141 |  |
          *
142 |  |
          * @throws Throwable when a throwable is thrown.
143 |  |
          */
144 |  |
         @Override
145 |  |
         protected void finalize() throws Throwable {
146 | 7 |
             super.finalize();
147 | 7 |
             if (isOpen()) {
148 | 0 |
                 close();
149 |  |
             }
150 | 7 |
         }
151 |  |
152 |  |
         /**
153 |  |
          * Searches the data store of CPE entries, trying to identify the CPE for
154 |  |
          * the given dependency based on the evidence contained within. The
155 |  |
          * dependency passed in is updated with any identified CPE values.
156 |  |
          *
157 |  |
          * @param dependency the dependency to search for CPE entries on.
158 |  |
          * @throws CorruptIndexException is thrown when the Lucene index is corrupt.
159 |  |
          * @throws IOException is thrown when an IOException occurs.
160 |  |
          * @throws ParseException is thrown when the Lucene query cannot be parsed.
161 |  |
          */
162 |  |
         protected void determineCPE(Dependency dependency) throws CorruptIndexException, IOException, ParseException {
163 | 19 |
             Confidence vendorConf = Confidence.HIGHEST;
164 | 19 |
             Confidence productConf = Confidence.HIGHEST;
165 |  |
166 | 19 |
             String vendors = addEvidenceWithoutDuplicateTerms("", dependency.getVendorEvidence(), vendorConf);
167 | 19 |
             String products = addEvidenceWithoutDuplicateTerms("", dependency.getProductEvidence(), productConf);
168 |  |
169 | 19 |
             int ctr = 0;
170 |  |
             do {
171 | 76 |
                 if (!vendors.isEmpty() && !products.isEmpty()) {
172 | 72 |
                     final List<IndexEntry> entries = searchCPE(vendors, products, dependency.getProductEvidence().getWeighting(),
173 |  |
                             dependency.getVendorEvidence().getWeighting());
174 |  |
175 | 72 |
                     for (IndexEntry e : entries) {
176 | 722 |
                         if (verifyEntry(e, dependency)) {
177 | 71 |
                             final String vendor = e.getVendor();
178 | 71 |
                             final String product = e.getProduct();
179 | 71 |
                             determineIdentifiers(dependency, vendor, product);
180 | 722 |
                         }
181 |  |
                     }
182 |  |
                 }
183 | 76 |
                 vendorConf = reduceConfidence(vendorConf);
184 | 76 |
                 if (dependency.getVendorEvidence().contains(vendorConf)) {
185 | 61 |
                     vendors = addEvidenceWithoutDuplicateTerms(vendors, dependency.getVendorEvidence(), vendorConf);
186 |  |
                 }
187 | 76 |
                 productConf = reduceConfidence(productConf);
188 | 76 |
                 if (dependency.getProductEvidence().contains(productConf)) {
189 | 64 |
                     products = addEvidenceWithoutDuplicateTerms(products, dependency.getProductEvidence(), productConf);
190 |  |
                 }
191 | 76 |
             } while ((++ctr) < 4);
192 | 19 |
         }
193 |  |
194 |  |
         /**
195 |  |
          * Returns the text created by concatenating the text and the values from
196 |  |
          * the EvidenceCollection (filtered for a specific confidence). This
197 |  |
          * attempts to prevent duplicate terms from being added.<br/<br/> Note, if
198 |  |
          * the evidence is longer then 200 characters it will be truncated.
199 |  |
          *
200 |  |
          * @param text the base text.
201 |  |
          * @param ec an EvidenceCollection
202 |  |
          * @param confidenceFilter a Confidence level to filter the evidence by.
203 |  |
          * @return the new evidence text
204 |  |
          */
205 |  |
         private String addEvidenceWithoutDuplicateTerms(final String text, final EvidenceCollection ec, Confidence confidenceFilter) {
206 | 163 |
             final String txt = (text == null) ? "" : text;
207 | 163 |
             final StringBuilder sb = new StringBuilder(txt.length() + (20 * ec.size()));
208 | 163 |
             sb.append(' ').append(txt).append(' ');
209 | 163 |
             for (Evidence e : ec.iterator(confidenceFilter)) {
210 | 355 |
                 String value = e.getValue();
211 |  |
212 |  |
                 //hack to get around the fact that lucene does a really good job of recognizing domains and not
213 |  |
                 // splitting them. TODO - put together a better lucene analyzer specific to the domain.
214 | 355 |
                 if (value.startsWith("http://")) {
215 | 32 |
                     value = value.substring(7).replaceAll("\\.", " ");
216 |  |
                 }
217 | 355 |
                 if (value.startsWith("https://")) {
218 | 0 |
                     value = value.substring(8).replaceAll("\\.", " ");
219 |  |
                 }
220 | 355 |
                 if (sb.indexOf(" " + value + " ") < 0) {
221 | 222 |
                     sb.append(value).append(' ');
222 |  |
                 }
223 | 355 |
             }
224 | 163 |
             return sb.toString().trim();
225 |  |
         }
226 |  |
227 |  |
         /**
228 |  |
          * Reduces the given confidence by one level. This returns LOW if the
229 |  |
          * confidence passed in is not HIGH.
230 |  |
          *
231 |  |
          * @param c the confidence to reduce.
232 |  |
          * @return One less then the confidence passed in.
233 |  |
          */
234 |  |
         private Confidence reduceConfidence(final Confidence c) {
235 | 152 |
             if (c == Confidence.HIGHEST) {
236 | 38 |
                 return Confidence.HIGH;
237 | 114 |
             } else if (c == Confidence.HIGH) {
238 | 38 |
                 return Confidence.MEDIUM;
239 |  |
             } else {
240 | 76 |
                 return Confidence.LOW;
241 |  |
             }
242 |  |
         }
243 |  |
244 |  |
         /**
245 |  |
          * <p>Searches the Lucene CPE index to identify possible CPE entries
246 |  |
          * associated with the supplied vendor, product, and version.</p>
247 |  |
          *
248 |  |
          * <p>If either the vendorWeightings or productWeightings lists have been
249 |  |
          * populated this data is used to add weighting factors to the search.</p>
250 |  |
          *
251 |  |
          * @param vendor the text used to search the vendor field
252 |  |
          * @param product the text used to search the product field
253 |  |
          * @param vendorWeightings a list of strings to use to add weighting factors
254 |  |
          * to the vendor field
255 |  |
          * @param productWeightings Adds a list of strings that will be used to add
256 |  |
          * weighting factors to the product search
257 |  |
          * @return a list of possible CPE values
258 |  |
          * @throws CorruptIndexException when the Lucene index is corrupt
259 |  |
          * @throws IOException when the Lucene index is not found
260 |  |
          * @throws ParseException when the generated query is not valid
261 |  |
          */
262 |  |
         protected List<IndexEntry> searchCPE(String vendor, String product,
263 |  |
                 Set<String> vendorWeightings, Set<String> productWeightings)
264 |  |
                 throws CorruptIndexException, IOException, ParseException {
265 | 73 |
             final ArrayList<IndexEntry> ret = new ArrayList<IndexEntry>(MAX_QUERY_RESULTS);
266 |  |
267 | 73 |
             final String searchString = buildSearch(vendor, product, vendorWeightings, productWeightings);
268 | 73 |
             if (searchString == null) {
269 | 0 |
                 return ret;
270 |  |
             }
271 |  |
272 | 73 |
             final TopDocs docs = cpe.search(searchString, MAX_QUERY_RESULTS);
273 | 1212 |
             for (ScoreDoc d : docs.scoreDocs) {
274 | 1139 |
                 if (d.score >= 0.08) {
275 | 741 |
                     final Document doc = cpe.getDocument(d.doc);
276 | 741 |
                     final IndexEntry entry = new IndexEntry();
277 | 741 |
                     entry.setVendor(doc.get(Fields.VENDOR));
278 | 741 |
                     entry.setProduct(doc.get(Fields.PRODUCT));
279 |  |
     //                if (d.score < 0.08) {
280 |  |
     //                    System.out.print(entry.getVendor());
281 |  |
     //                    System.out.print(":");
282 |  |
     //                    System.out.print(entry.getProduct());
283 |  |
     //                    System.out.print(":");
284 |  |
     //                    System.out.println(d.score);
285 |  |
     //                }
286 | 741 |
                     entry.setSearchScore(d.score);
287 | 741 |
                     if (!ret.contains(entry)) {
288 | 741 |
                         ret.add(entry);
289 |  |
                     }
290 |  |
                 }
291 |  |
             }
292 | 73 |
             return ret;
293 |  |
         }
294 |  |
295 |  |
         /**
296 |  |
          * <p>Builds a Lucene search string by properly escaping data and
297 |  |
          * constructing a valid search query.</p>
298 |  |
          *
299 |  |
          * <p>If either the possibleVendor or possibleProducts lists have been
300 |  |
          * populated this data is used to add weighting factors to the search string
301 |  |
          * generated.</p>
302 |  |
          *
303 |  |
          * @param vendor text to search the vendor field
304 |  |
          * @param product text to search the product field
305 |  |
          * @param vendorWeighting a list of strings to apply to the vendor to boost
306 |  |
          * the terms weight
307 |  |
          * @param productWeightings a list of strings to apply to the product to
308 |  |
          * boost the terms weight
309 |  |
          * @return the Lucene query
310 |  |
          */
311 |  |
         protected String buildSearch(String vendor, String product,
312 |  |
                 Set<String> vendorWeighting, Set<String> productWeightings) {
313 | 77 |
             final String v = vendor; //.replaceAll("[^\\w\\d]", " ");
314 | 77 |
             final String p = product; //.replaceAll("[^\\w\\d]", " ");
315 | 77 |
             final StringBuilder sb = new StringBuilder(v.length() + p.length()
316 |  |
                     + Fields.PRODUCT.length() + Fields.VENDOR.length() + STRING_BUILDER_BUFFER);
317 |  |
318 | 77 |
             if (!appendWeightedSearch(sb, Fields.PRODUCT, p, productWeightings)) {
319 | 0 |
                 return null;
320 |  |
             }
321 | 77 |
             sb.append(" AND ");
322 | 77 |
             if (!appendWeightedSearch(sb, Fields.VENDOR, v, vendorWeighting)) {
323 | 0 |
                 return null;
324 |  |
             }
325 | 77 |
             return sb.toString();
326 |  |
         }
327 |  |
328 |  |
         /**
329 |  |
          * This method constructs a Lucene query for a given field. The searchText
330 |  |
          * is split into separate words and if the word is within the list of
331 |  |
          * weighted words then an additional weighting is applied to the term as it
332 |  |
          * is appended into the query.
333 |  |
          *
334 |  |
          * @param sb a StringBuilder that the query text will be appended to.
335 |  |
          * @param field the field within the Lucene index that the query is
336 |  |
          * searching.
337 |  |
          * @param searchText text used to construct the query.
338 |  |
          * @param weightedText a list of terms that will be considered higher
339 |  |
          * importance when searching.
340 |  |
          * @return if the append was successful.
341 |  |
          */
342 |  |
         private boolean appendWeightedSearch(StringBuilder sb, String field, String searchText, Set<String> weightedText) {
343 | 154 |
             sb.append(" ").append(field).append(":( ");
344 |  |
345 | 154 |
             final String cleanText = cleanseText(searchText);
346 |  |
347 | 154 |
             if ("".equals(cleanText)) {
348 | 0 |
                 return false;
349 |  |
             }
350 |  |
351 | 154 |
             if (weightedText == null || weightedText.isEmpty()) {
352 | 40 |
                 LuceneUtils.appendEscapedLuceneQuery(sb, cleanText);
353 |  |
             } else {
354 | 114 |
                 final StringTokenizer tokens = new StringTokenizer(cleanText);
355 | 1211 |
                 while (tokens.hasMoreElements()) {
356 | 1097 |
                     final String word = tokens.nextToken();
357 | 1097 |
                     String temp = null;
358 | 1097 |
                     for (String weighted : weightedText) {
359 | 2202 |
                         final String weightedStr = cleanseText(weighted);
360 | 2202 |
                         if (equalsIgnoreCaseAndNonAlpha(word, weightedStr)) {
361 | 251 |
                             temp = LuceneUtils.escapeLuceneQuery(word) + WEIGHTING_BOOST;
362 | 251 |
                             if (!word.equalsIgnoreCase(weightedStr)) {
363 | 18 |
                                 temp += " " + LuceneUtils.escapeLuceneQuery(weightedStr) + WEIGHTING_BOOST;
364 |  |
                             }
365 |  |
                         }
366 | 2202 |
                     }
367 | 1097 |
                     if (temp == null) {
368 | 846 |
                         temp = LuceneUtils.escapeLuceneQuery(word);
369 |  |
                     }
370 | 1097 |
                     sb.append(" ").append(temp);
371 | 1097 |
                 }
372 |  |
             }
373 | 154 |
             sb.append(" ) ");
374 | 154 |
             return true;
375 |  |
         }
376 |  |
377 |  |
         /**
378 |  |
          * Removes characters from the input text that are not used within the CPE
379 |  |
          * index.
380 |  |
          *
381 |  |
          * @param text is the text to remove the characters from.
382 |  |
          * @return the text having removed some characters.
383 |  |
          */
384 |  |
         private String cleanseText(String text) {
385 | 2356 |
             return text.replaceAll(CLEANSE_CHARACTER_RX, " ");
386 |  |
         }
387 |  |
388 |  |
         /**
389 |  |
          * Compares two strings after lower casing them and removing the non-alpha
390 |  |
          * characters.
391 |  |
          *
392 |  |
          * @param l string one to compare.
393 |  |
          * @param r string two to compare.
394 |  |
          * @return whether or not the two strings are similar.
395 |  |
          */
396 |  |
         private boolean equalsIgnoreCaseAndNonAlpha(String l, String r) {
397 | 2202 |
             if (l == null || r == null) {
398 | 0 |
                 return false;
399 |  |
             }
400 |  |
401 | 2202 |
             final String left = l.replaceAll(CLEANSE_NONALPHA_RX, "");
402 | 2202 |
             final String right = r.replaceAll(CLEANSE_NONALPHA_RX, "");
403 | 2202 |
             return left.equalsIgnoreCase(right);
404 |  |
         }
405 |  |
406 |  |
         /**
407 |  |
          * Ensures that the CPE Identified matches the dependency. This validates
408 |  |
          * that the product, vendor, and version information for the CPE are
409 |  |
          * contained within the dependencies evidence.
410 |  |
          *
411 |  |
          * @param entry a CPE entry.
412 |  |
          * @param dependency the dependency that the CPE entries could be for.
413 |  |
          * @return whether or not the entry is valid.
414 |  |
          */
415 |  |
         private boolean verifyEntry(final IndexEntry entry, final Dependency dependency) {
416 | 722 |
             boolean isValid = false;
417 |  |
418 | 722 |
             if (collectionContainsString(dependency.getProductEvidence(), entry.getProduct())
419 |  |
                     && collectionContainsString(dependency.getVendorEvidence(), entry.getVendor())) {
420 |  |
                 //&& collectionContainsVersion(dependency.getVersionEvidence(), entry.getVersion())
421 | 71 |
                 isValid = true;
422 |  |
             }
423 | 722 |
             return isValid;
424 |  |
         }
425 |  |
426 |  |
         /**
427 |  |
          * Used to determine if the EvidenceCollection contains a specific string.
428 |  |
          *
429 |  |
          * @param ec an EvidenceCollection
430 |  |
          * @param text the text to search for
431 |  |
          * @return whether or not the EvidenceCollection contains the string
432 |  |
          */
433 |  |
         private boolean collectionContainsString(EvidenceCollection ec, String text) {
434 |  |
435 |  |
             //<editor-fold defaultstate="collapsed" desc="This code fold contains an old version of the code, delete once more testing is done">
436 |  |
             //        String[] splitText = text.split("[\\s_-]");
437 |  |
             //
438 |  |
             //        for (String search : splitText) {
439 |  |
             //            //final String search = text.replaceAll("[\\s_-]", "").toLowerCase();
440 |  |
             //            if (ec.containsUsedString(search)) {
441 |  |
             //                return true;
442 |  |
             //            }
443 |  |
             //        }
444 |  |
             //</editor-fold>
445 |  |
446 |  |
             //TODO - likely need to change the split... not sure if this will work for CPE with special chars
447 | 799 |
             if (text == null) {
448 | 0 |
                 return false;
449 |  |
             }
450 | 799 |
             final String[] words = text.split("[\\s_-]");
451 | 799 |
             final List<String> list = new ArrayList<String>();
452 | 799 |
             String tempWord = null;
453 | 2254 |
             for (String word : words) {
454 |  |
                 //single letter words should be concatonated with the next word.
455 |  |
                 // so { "m", "core", "sample" } -> { "mcore", "sample" }
456 | 1455 |
                 if (tempWord != null) {
457 | 17 |
                     list.add(tempWord + word);
458 | 17 |
                     tempWord = null;
459 | 1438 |
                 } else if (word.length() <= 2) {
460 | 41 |
                     tempWord = word;
461 |  |
                 } else {
462 | 1397 |
                     list.add(word);
463 |  |
                 }
464 |  |
             }
465 | 799 |
             if (tempWord != null && !list.isEmpty()) {
466 | 20 |
                 final String tmp = list.get(list.size() - 1) + tempWord;
467 | 20 |
                 list.add(tmp);
468 |  |
             }
469 | 799 |
             boolean contains = true;
470 | 799 |
             for (String word : list) {
471 | 1434 |
                 contains &= ec.containsUsedString(word);
472 |  |
             }
473 | 799 |
             return contains;
474 |  |
         }
475 |  |
476 |  |
         /**
477 |  |
          * Analyzes a dependency and attempts to determine if there are any CPE
478 |  |
          * identifiers for this dependency.
479 |  |
          *
480 |  |
          * @param dependency The Dependency to analyze.
481 |  |
          * @param engine The analysis engine
482 |  |
          * @throws AnalysisException is thrown if there is an issue analyzing the
483 |  |
          * dependency.
484 |  |
          */
485 |  |
         @Override
486 |  |
         public void analyze(Dependency dependency, Engine engine) throws AnalysisException {
487 |  |
             try {
488 | 15 |
                 determineCPE(dependency);
489 | 0 |
             } catch (CorruptIndexException ex) {
490 | 0 |
                 throw new AnalysisException("CPE Index is corrupt.", ex);
491 | 0 |
             } catch (IOException ex) {
492 | 0 |
                 throw new AnalysisException("Failure opening the CPE Index.", ex);
493 | 0 |
             } catch (ParseException ex) {
494 | 0 |
                 throw new AnalysisException("Unable to parse the generated Lucene query for this dependency.", ex);
495 | 15 |
             }
496 | 15 |
         }
497 |  |
498 |  |
         /**
499 |  |
          * Returns true because this analyzer supports all dependency types.
500 |  |
          *
501 |  |
          * @return true.
502 |  |
          */
503 |  |
         @Override
504 |  |
         public Set<String> getSupportedExtensions() {
505 | 132 |
             return null;
506 |  |
         }
507 |  |
508 |  |
         /**
509 |  |
          * Returns the name of this analyzer.
510 |  |
          *
511 |  |
          * @return the name of this analyzer.
512 |  |
          */
513 |  |
         @Override
514 |  |
         public String getName() {
515 | 9 |
             return "CPE Analyzer";
516 |  |
         }
517 |  |
518 |  |
         /**
519 |  |
          * Returns true because this analyzer supports all dependency types.
520 |  |
          *
521 |  |
          * @param extension the file extension of the dependency being analyzed.
522 |  |
          * @return true.
523 |  |
          */
524 |  |
         @Override
525 |  |
         public boolean supportsExtension(String extension) {
526 | 9 |
             return true;
527 |  |
         }
528 |  |
529 |  |
         /**
530 |  |
          * Returns the analysis phase that this analyzer should run in.
531 |  |
          *
532 |  |
          * @return the analysis phase that this analyzer should run in.
533 |  |
          */
534 |  |
         @Override
535 |  |
         public AnalysisPhase getAnalysisPhase() {
536 | 6 |
             return AnalysisPhase.IDENTIFIER_ANALYSIS;
537 |  |
         }
538 |  |
539 |  |
         /**
540 |  |
          * Opens the CPE Lucene Index.
541 |  |
          *
542 |  |
          * @throws Exception is thrown if there is an issue opening the index.
543 |  |
          */
544 |  |
         @Override
545 |  |
         public void initialize() throws Exception {
546 | 3 |
             this.open();
547 | 3 |
         }
548 |  |
549 |  |
         /**
550 |  |
          * Retrieves a list of CPE values from the CveDB based on the vendor and
551 |  |
          * product passed in. The list is then validated to find only CPEs that are
552 |  |
          * valid for the given dependency. It is possible that the CPE identified is
553 |  |
          * a best effort "guess" based on the vendor, product, and version
554 |  |
          * information.
555 |  |
          *
556 |  |
          * @param dependency the Dependency being analyzed
557 |  |
          * @param vendor the vendor for the CPE being analyzed
558 |  |
          * @param product the product for the CPE being analyzed
559 |  |
          * @throws UnsupportedEncodingException is thrown if UTF-8 is not supported
560 |  |
          */
561 |  |
         private void determineIdentifiers(Dependency dependency, String vendor, String product) throws UnsupportedEncodingException {
562 | 71 |
             final Set<VulnerableSoftware> cpes = cve.getCPEs(vendor, product);
563 | 71 |
             DependencyVersion bestGuess = new DependencyVersion("-");
564 | 71 |
             Confidence bestGuessConf = null;
565 | 71 |
             final List<IdentifierMatch> collected = new ArrayList<IdentifierMatch>();
566 | 355 |
             for (Confidence conf : Confidence.values()) {
567 | 284 |
                 for (Evidence evidence : dependency.getVersionEvidence().iterator(conf)) {
568 | 160 |
                     final DependencyVersion evVer = DependencyVersionUtil.parseVersion(evidence.getValue());
569 | 160 |
                     if (evVer == null) {
570 | 0 |
                         continue;
571 |  |
                     }
572 | 160 |
                     for (VulnerableSoftware vs : cpes) {
573 |  |
                         DependencyVersion dbVer;
574 | 9080 |
                         if (vs.getRevision() != null && !vs.getRevision().isEmpty()) {
575 | 2855 |
                             dbVer = DependencyVersionUtil.parseVersion(vs.getVersion() + "." + vs.getRevision());
576 |  |
                         } else {
577 | 6225 |
                             dbVer = DependencyVersionUtil.parseVersion(vs.getVersion());
578 |  |
                         }
579 | 9080 |
                         if (dbVer == null //special case, no version specified - everything is vulnerable
580 |  |
                                 || evVer.equals(dbVer)) { //woot exect match
581 | 169 |
                             final String url = String.format("http://web.nvd.nist.gov/view/vuln/search?cpe=%s", URLEncoder.encode(vs.getName(), "UTF-8"));
582 | 169 |
                             final IdentifierMatch match = new IdentifierMatch("cpe", vs.getName(), url, IdentifierConfidence.EXACT_MATCH, conf);
583 | 169 |
                             collected.add(match);
584 | 169 |
                         } else {
585 |  |
                             //TODO the following isn't quite right is it? need to think about this guessing game a bit more.
586 | 8911 |
                             if (evVer.getVersionParts().size() <= dbVer.getVersionParts().size()
587 |  |
                                     && evVer.matchesAtLeastThreeLevels(dbVer)) {
588 | 412 |
                                 if (bestGuessConf == null || bestGuessConf.compareTo(conf) > 0) {
589 | 10 |
                                     if (bestGuess.getVersionParts().size() < dbVer.getVersionParts().size()) {
590 | 10 |
                                         bestGuess = dbVer;
591 | 10 |
                                         bestGuessConf = conf;
592 |  |
                                     }
593 |  |
                                 }
594 |  |
                             }
595 |  |
                         }
596 | 9080 |
                     }
597 | 160 |
                     if (bestGuessConf == null || bestGuessConf.compareTo(conf) > 0) {
598 | 49 |
                         if (bestGuess.getVersionParts().size() < evVer.getVersionParts().size()) {
599 | 49 |
                             bestGuess = evVer;
600 | 49 |
                             bestGuessConf = conf;
601 |  |
                         }
602 |  |
                     }
603 | 160 |
                 }
604 |  |
             }
605 | 71 |
             final String cpeName = String.format("cpe:/a:%s:%s:%s", vendor, product, bestGuess.toString());
606 | 71 |
             final String url = null; //String.format("http://web.nvd.nist.gov/view/vuln/search?cpe=%s", URLEncoder.encode(cpeName, "UTF-8"));
607 | 71 |
             if (bestGuessConf == null) {
608 | 12 |
                 bestGuessConf = Confidence.LOW;
609 |  |
             }
610 | 71 |
             final IdentifierMatch match = new IdentifierMatch("cpe", cpeName, url, IdentifierConfidence.BEST_GUESS, bestGuessConf);
611 | 71 |
             collected.add(match);
612 |  |
613 | 71 |
             Collections.sort(collected);
614 | 71 |
             final IdentifierConfidence bestIdentifierQuality = collected.get(0).getConfidence();
615 | 71 |
             final Confidence bestEvidenceQuality = collected.get(0).getEvidenceConfidence();
616 | 71 |
             for (IdentifierMatch m : collected) {
617 | 240 |
                 if (bestIdentifierQuality.equals(m.getConfidence())
618 |  |
                         && bestEvidenceQuality.equals(m.getEvidenceConfidence())) {
619 | 88 |
                     dependency.addIdentifier(m.getIdentifier());
620 |  |
                 }
621 |  |
             }
622 | 71 |
         }
623 |  |
624 |  |
         /**
625 |  |
          * The confidence whether the identifier is an exact match, or a best guess.
626 |  |
          */
627 | 3 |
         private enum IdentifierConfidence {
628 |  |
629 |  |
             /**
630 |  |
              * An exact match for the CPE.
631 |  |
              */
632 | 1 |
             EXACT_MATCH,
633 |  |
             /**
634 |  |
              * A best guess for the CPE.
635 |  |
              */
636 | 1 |
             BEST_GUESS
637 |  |
         }
638 |  |
639 |  |
         /**
640 |  |
          * A simple object to hold an identifier and carry information about the
641 |  |
          * confidence in the identifier.
642 |  |
          */
643 | 225 |
         private static class IdentifierMatch implements Comparable<IdentifierMatch> {
644 |  |
645 |  |
             /**
646 |  |
              * Constructs an IdentiferMatch.
647 |  |
              *
648 |  |
              * @param type the type of identifier (such as CPE)
649 |  |
              * @param value the value of the identifier
650 |  |
              * @param url the URL of the identifier
651 |  |
              * @param identifierConfidence the confidence in the identifier: best
652 |  |
              * guess or exact match
653 |  |
              * @param evidenceConfidence the confidence of the evidence used to find
654 |  |
              * the identifier
655 |  |
              */
656 | 240 |
             IdentifierMatch(String type, String value, String url, IdentifierConfidence identifierConfidence, Confidence evidenceConfidence) {
657 | 240 |
                 this.identifier = new Identifier(type, value, url);
658 | 240 |
                 this.confidence = identifierConfidence;
659 | 240 |
                 this.evidenceConfidence = evidenceConfidence;
660 | 240 |
             }
661 |  |
             //<editor-fold defaultstate="collapsed" desc="Property implementations: evidenceConfidence, confidence, identifier">
662 |  |
             /**
663 |  |
              * The confidence in the evidence used to identify this match.
664 |  |
              */
665 |  |
             private Confidence evidenceConfidence;
666 |  |
667 |  |
             /**
668 |  |
              * Get the value of evidenceConfidence
669 |  |
              *
670 |  |
              * @return the value of evidenceConfidence
671 |  |
              */
672 |  |
             public Confidence getEvidenceConfidence() {
673 | 263 |
                 return evidenceConfidence;
674 |  |
             }
675 |  |
676 |  |
             /**
677 |  |
              * Set the value of evidenceConfidence
678 |  |
              *
679 |  |
              * @param evidenceConfidence new value of evidenceConfidence
680 |  |
              */
681 |  |
             public void setEvidenceConfidence(Confidence evidenceConfidence) {
682 | 0 |
                 this.evidenceConfidence = evidenceConfidence;
683 | 0 |
             }
684 |  |
             /**
685 |  |
              * The confidence whether this is an exact match, or a best guess.
686 |  |
              */
687 |  |
             private IdentifierConfidence confidence;
688 |  |
689 |  |
             /**
690 |  |
              * Get the value of confidence.
691 |  |
              *
692 |  |
              * @return the value of confidence
693 |  |
              */
694 |  |
             public IdentifierConfidence getConfidence() {
695 | 311 |
                 return confidence;
696 |  |
             }
697 |  |
698 |  |
             /**
699 |  |
              * Set the value of confidence.
700 |  |
              *
701 |  |
              * @param confidence new value of confidence
702 |  |
              */
703 |  |
             public void setConfidence(IdentifierConfidence confidence) {
704 | 0 |
                 this.confidence = confidence;
705 | 0 |
             }
706 |  |
             /**
707 |  |
              * The CPE identifier.
708 |  |
              */
709 |  |
             private Identifier identifier;
710 |  |
711 |  |
             /**
712 |  |
              * Get the value of identifier.
713 |  |
              *
714 |  |
              * @return the value of identifier
715 |  |
              */
716 |  |
             public Identifier getIdentifier() {
717 | 88 |
                 return identifier;
718 |  |
             }
719 |  |
720 |  |
             /**
721 |  |
              * Set the value of identifier.
722 |  |
              *
723 |  |
              * @param identifier new value of identifier
724 |  |
              */
725 |  |
             public void setIdentifier(Identifier identifier) {
726 | 0 |
                 this.identifier = identifier;
727 | 0 |
             }
728 |  |
             //</editor-fold>
729 |  |
             //<editor-fold defaultstate="collapsed" desc="Standard implementations of toString, hashCode, and equals">
730 |  |
731 |  |
             /**
732 |  |
              * Standard toString() implementation.
733 |  |
              *
734 |  |
              * @return the string representation of the object
735 |  |
              */
736 |  |
             @Override
737 |  |
             public String toString() {
738 | 0 |
                 return "IdentifierMatch{" + "evidenceConfidence=" + evidenceConfidence
739 |  |
                         + ", confidence=" + confidence + ", identifier=" + identifier + '}';
740 |  |
             }
741 |  |
742 |  |
             /**
743 |  |
              * Standard hashCode() implementation.
744 |  |
              *
745 |  |
              * @return the hashCode
746 |  |
              */
747 |  |
             @Override
748 |  |
             public int hashCode() {
749 | 0 |
                 int hash = 5;
750 | 0 |
                 hash = 97 * hash + (this.evidenceConfidence != null ? this.evidenceConfidence.hashCode() : 0);
751 | 0 |
                 hash = 97 * hash + (this.confidence != null ? this.confidence.hashCode() : 0);
752 | 0 |
                 hash = 97 * hash + (this.identifier != null ? this.identifier.hashCode() : 0);
753 | 0 |
                 return hash;
754 |  |
             }
755 |  |
756 |  |
             /**
757 |  |
              * Standard equals implementation.
758 |  |
              *
759 |  |
              * @param obj the object to compare
760 |  |
              * @return true if the objects are equal, otherwise false
761 |  |
              */
762 |  |
             @Override
763 |  |
             public boolean equals(Object obj) {
764 | 0 |
                 if (obj == null) {
765 | 0 |
                     return false;
766 |  |
                 }
767 | 0 |
                 if (getClass() != obj.getClass()) {
768 | 0 |
                     return false;
769 |  |
                 }
770 | 0 |
                 final IdentifierMatch other = (IdentifierMatch) obj;
771 | 0 |
                 if (this.evidenceConfidence != other.evidenceConfidence) {
772 | 0 |
                     return false;
773 |  |
                 }
774 | 0 |
                 if (this.confidence != other.confidence) {
775 | 0 |
                     return false;
776 |  |
                 }
777 | 0 |
                 if (this.identifier != other.identifier && (this.identifier == null || !this.identifier.equals(other.identifier))) {
778 | 0 |
                     return false;
779 |  |
                 }
780 | 0 |
                 return true;
781 |  |
             }
782 |  |
             //</editor-fold>
783 |  |
784 |  |
             /**
785 |  |
              * Standard implementation of compareTo that compares identifier
786 |  |
              * confidence, evidence confidence, and then the identifier.
787 |  |
              *
788 |  |
              * @param o the IdentifierMatch to compare to
789 |  |
              * @return the natural ordering of IdentifierMatch
790 |  |
              */
791 |  |
             @Override
792 |  |
             public int compareTo(IdentifierMatch o) {
793 | 225 |
                 int conf = this.confidence.compareTo(o.confidence);
794 | 225 |
                 if (conf == 0) {
795 | 177 |
                     conf = this.evidenceConfidence.compareTo(o.evidenceConfidence);
796 | 177 |
                     if (conf == 0) {
797 | 83 |
                         conf = identifier.compareTo(o.identifier);
798 |  |
                     }
799 |  |
                 }
800 | 225 |
                 return conf;
801 |  |
             }
802 |  |
         }
803 |  |
     }