View Javadoc
1   /*
2    * This file is part of dependency-check-core.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * Copyright (c) 2012 Jeremy Long. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.dependency;
19  
20  import java.io.Serializable;
21  import java.io.UnsupportedEncodingException;
22  import java.net.URLDecoder;
23  import org.owasp.dependencycheck.data.cpe.IndexEntry;
24  import org.slf4j.Logger;
25  import org.slf4j.LoggerFactory;
26  
27  /**
28   * A record containing information about vulnerable software. This is referenced from a vulnerability.
29   *
30   * @author Jeremy Long
31   */
32  public class VulnerableSoftware extends IndexEntry implements Serializable, Comparable<VulnerableSoftware> {
33  
34      /**
35       * The logger.
36       */
37      private static final Logger LOGGER = LoggerFactory.getLogger(VulnerableSoftware.class);
38      /**
39       * The serial version UID.
40       */
41      private static final long serialVersionUID = 307319490326651052L;
42  
43      /**
44       * Parse a CPE entry from the cpe string representation.
45       *
46       * @param cpe a cpe entry (e.g. cpe:/a:vendor:software:version)
47       */
48      public void setCpe(String cpe) {
49          try {
50              parseName(cpe);
51          } catch (UnsupportedEncodingException ex) {
52              LOGGER.warn("Character encoding is unsupported for CPE '{}'.", cpe);
53              LOGGER.debug("", ex);
54              setName(cpe);
55          }
56      }
57  
58      /**
59       * <p>
60       * Parses a name attribute value, from the cpe.xml, into its corresponding parts: vendor, product, version, update.</p>
61       * <p>
62       * Example:</p>
63       * <code>&nbsp;&nbsp;&nbsp;cpe:/a:apache:struts:1.1:rc2</code>
64       *
65       * <p>
66       * Results in:</p> <ul> <li>Vendor: apache</li> <li>Product: struts</li>
67       * <li>Version: 1.1</li> <li>Revision: rc2</li> </ul>
68       *
69       * @param cpeName the cpe name
70       * @throws UnsupportedEncodingException should never be thrown...
71       */
72      @Override
73      public void parseName(String cpeName) throws UnsupportedEncodingException {
74          this.name = cpeName;
75          if (cpeName != null && cpeName.length() > 7) {
76              final String[] data = cpeName.substring(7).split(":");
77              if (data.length >= 1) {
78                  this.setVendor(urlDecode(data[0]));
79              }
80              if (data.length >= 2) {
81                  this.setProduct(urlDecode(data[1]));
82              }
83              if (data.length >= 3) {
84                  version = urlDecode(data[2]);
85              }
86              if (data.length >= 4) {
87                  update = urlDecode(data[3]);
88              }
89              if (data.length >= 5) {
90                  edition = urlDecode(data[4]);
91              }
92          }
93      }
94      /**
95       * If present, indicates that previous version are vulnerable.
96       */
97      private String previousVersion;
98  
99      /**
100      * Indicates if previous versions of this software are vulnerable.
101      *
102      * @return if previous versions of this software are vulnerable
103      */
104     public boolean hasPreviousVersion() {
105         return previousVersion != null;
106     }
107 
108     /**
109      * Get the value of previousVersion.
110      *
111      * @return the value of previousVersion
112      */
113     public String getPreviousVersion() {
114         return previousVersion;
115     }
116 
117     /**
118      * Set the value of previousVersion.
119      *
120      * @param previousVersion new value of previousVersion
121      */
122     public void setPreviousVersion(String previousVersion) {
123         this.previousVersion = previousVersion;
124     }
125 
126     /**
127      * Standard equals implementation to compare this VulnerableSoftware to another object.
128      *
129      * @param obj the object to compare
130      * @return whether or not the objects are equal
131      */
132     @Override
133     public boolean equals(Object obj) {
134         if (obj == null) {
135             return false;
136         }
137         if (getClass() != obj.getClass()) {
138             return false;
139         }
140         final VulnerableSoftware other = (VulnerableSoftware) obj;
141         if ((this.name == null) ? (other.getName() != null) : !this.name.equals(other.getName())) {
142             return false;
143         }
144         return true;
145     }
146 
147     /**
148      * Standard implementation of hashCode.
149      *
150      * @return the hashCode for the object
151      */
152     @Override
153     public int hashCode() {
154         int hash = 7;
155         hash = 83 * hash + (this.name != null ? this.name.hashCode() : 0);
156         return hash;
157     }
158 
159     /**
160      * Standard toString() implementation display the name and whether or not previous versions are also affected.
161      *
162      * @return a string representation of the object
163      */
164     @Override
165     public String toString() {
166         return "VulnerableSoftware{" + name + "[" + previousVersion + "]}";
167     }
168 
169     /**
170      * Implementation of the comparable interface.
171      *
172      * @param vs the VulnerableSoftware to compare
173      * @return an integer indicating the ordering of the two objects
174      */
175     @Override
176     public int compareTo(VulnerableSoftware vs) {
177         int result = 0;
178         final String[] left = this.name.split(":");
179         final String[] right = vs.getName().split(":");
180         final int max = (left.length <= right.length) ? left.length : right.length;
181         if (max > 0) {
182             for (int i = 0; result == 0 && i < max; i++) {
183                 final String[] subLeft = left[i].split("(\\.|-)");
184                 final String[] subRight = right[i].split("(\\.|-)");
185                 final int subMax = (subLeft.length <= subRight.length) ? subLeft.length : subRight.length;
186                 if (subMax > 0) {
187                     for (int x = 0; result == 0 && x < subMax; x++) {
188                         if (isPositiveInteger(subLeft[x]) && isPositiveInteger(subRight[x])) {
189                             try {
190                                 result = Long.valueOf(subLeft[x]).compareTo(Long.valueOf(subRight[x]));
191                             } catch (NumberFormatException ex) {
192                                 //ignore the exception - they obviously aren't numbers
193                                 if (!subLeft[x].equalsIgnoreCase(subRight[x])) {
194                                     result = subLeft[x].compareToIgnoreCase(subRight[x]);
195                                 }
196                             }
197                         } else {
198                             result = subLeft[x].compareToIgnoreCase(subRight[x]);
199                         }
200                     }
201                     if (result == 0) {
202                         if (subLeft.length > subRight.length) {
203                             result = 2;
204                         }
205                         if (subRight.length > subLeft.length) {
206                             result = -2;
207                         }
208                     }
209                 } else {
210                     result = left[i].compareToIgnoreCase(right[i]);
211                 }
212             }
213             if (result == 0) {
214                 if (left.length > right.length) {
215                     result = 2;
216                 }
217                 if (right.length > left.length) {
218                     result = -2;
219                 }
220             }
221         } else {
222             result = this.getName().compareToIgnoreCase(vs.getName());
223         }
224         return result;
225     }
226 
227     /**
228      * Determines if the string passed in is a positive integer.
229      * To be counted as a positive integer, the string must only contain 0-9
230      * and must not have any leading zeros (though "0" is a valid positive
231      * integer).
232      *
233      * @param str the string to test
234      * @return true if the string only contains 0-9, otherwise false.
235      */
236     static boolean isPositiveInteger(final String str) {
237         if (str == null || str.isEmpty()) {
238             return false;
239         }
240 
241         // numbers with leading zeros should not be treated as numbers
242         // (e.g. when comparing "01" <-> "1")
243         if (str.charAt(0) == '0' && str.length() > 1) {
244             return false;
245         }
246 
247         for (int i = 0; i < str.length(); i++) {
248             final char c = str.charAt(i);
249             if (c < '0' || c > '9') {
250                 return false;
251             }
252         }
253         return true;
254     }
255     /**
256      * The name of the cpe.
257      */
258     private String name;
259 
260     /**
261      * Get the value of name.
262      *
263      * @return the value of name
264      */
265     public String getName() {
266         return name;
267     }
268 
269     /**
270      * Set the value of name.
271      *
272      * @param name new value of name
273      */
274     public void setName(String name) {
275         this.name = name;
276     }
277     /**
278      * The product version number.
279      */
280     private String version;
281 
282     /**
283      * Get the value of version.
284      *
285      * @return the value of version
286      */
287     public String getVersion() {
288         return version;
289     }
290 
291     /**
292      * Set the value of version.
293      *
294      * @param version new value of version
295      */
296     public void setVersion(String version) {
297         this.version = version;
298     }
299     /**
300      * The product update version.
301      */
302     private String update;
303 
304     /**
305      * Get the value of update.
306      *
307      * @return the value of update
308      */
309     public String getUpdate() {
310         return update;
311     }
312 
313     /**
314      * Set the value of update.
315      *
316      * @param update new value of update
317      */
318     public void setUpdate(String update) {
319         this.update = update;
320     }
321     /**
322      * The product edition.
323      */
324     private String edition;
325 
326     /**
327      * Get the value of edition.
328      *
329      * @return the value of edition
330      */
331     public String getEdition() {
332         return edition;
333     }
334 
335     /**
336      * Set the value of edition.
337      *
338      * @param edition new value of edition
339      */
340     public void setEdition(String edition) {
341         this.edition = edition;
342     }
343 
344     /**
345      * Replaces '+' with '%2B' and then URL Decodes the string attempting first UTF-8, then ASCII, then default.
346      *
347      * @param string the string to URL Decode
348      * @return the URL Decoded string
349      */
350     private String urlDecode(String string) {
351         final String text = string.replace("+", "%2B");
352         String result;
353         try {
354             result = URLDecoder.decode(text, "UTF-8");
355         } catch (UnsupportedEncodingException ex) {
356             try {
357                 result = URLDecoder.decode(text, "ASCII");
358             } catch (UnsupportedEncodingException ex1) {
359                 result = defaultUrlDecode(text);
360             }
361         }
362         return result;
363     }
364 
365     /**
366      * Call {@link java.net.URLDecoder#decode(String)} to URL decode using the default encoding.
367      *
368      * @param text www-form-encoded URL to decode
369      * @return the newly decoded String
370      */
371     @SuppressWarnings("deprecation")
372     private String defaultUrlDecode(final String text) {
373         return URLDecoder.decode(text);
374     }
375 }