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.getName() == null) ? (other.getName() != null) : !this.getName().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.getName() != null ? this.getName().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=" + name + ", previousVersion=" + 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.getName().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 //                                final long iLeft = Long.parseLong(subLeft[x]);
192 //                                final long iRight = Long.parseLong(subRight[x]);
193 //                                if (iLeft != iRight) {
194 //                                    if (iLeft > iRight) {
195 //                                        result = 2;
196 //                                    } else {
197 //                                        result = -2;
198 //                                    }
199 //                                }
200                             } catch (NumberFormatException ex) {
201                                 //ignore the exception - they obviously aren't numbers
202                                 if (!subLeft[x].equalsIgnoreCase(subRight[x])) {
203                                     result = subLeft[x].compareToIgnoreCase(subRight[x]);
204                                 }
205                             }
206                         } else {
207                             result = subLeft[x].compareToIgnoreCase(subRight[x]);
208                         }
209                     }
210                     if (result == 0) {
211                         if (subLeft.length > subRight.length) {
212                             result = 2;
213                         }
214                         if (subRight.length > subLeft.length) {
215                             result = -2;
216                         }
217                     }
218                 } else {
219                     result = left[i].compareToIgnoreCase(right[i]);
220                 }
221             }
222             if (result == 0) {
223                 if (left.length > right.length) {
224                     result = 2;
225                 }
226                 if (right.length > left.length) {
227                     result = -2;
228                 }
229             }
230         } else {
231             result = this.getName().compareToIgnoreCase(vs.getName());
232         }
233         return result;
234     }
235 
236     /**
237      * Determines if the string passed in is a positive integer.
238      *
239      * @param str the string to test
240      * @return true if the string only contains 0-9, otherwise false.
241      */
242     private static boolean isPositiveInteger(final String str) {
243         if (str == null || str.isEmpty()) {
244             return false;
245         }
246         for (int i = 0; i < str.length(); i++) {
247             final char c = str.charAt(i);
248             if (c < '0' || c > '9') {
249                 return false;
250             }
251         }
252         return true;
253     }
254     /**
255      * The name of the cpe.
256      */
257     private String name;
258 
259     /**
260      * Get the value of name.
261      *
262      * @return the value of name
263      */
264     public String getName() {
265         return name;
266     }
267 
268     /**
269      * Set the value of name.
270      *
271      * @param name new value of name
272      */
273     public void setName(String name) {
274         this.name = name;
275     }
276     /**
277      * The product version number.
278      */
279     private String version;
280 
281     /**
282      * Get the value of version.
283      *
284      * @return the value of version
285      */
286     public String getVersion() {
287         return version;
288     }
289 
290     /**
291      * Set the value of version.
292      *
293      * @param version new value of version
294      */
295     public void setVersion(String version) {
296         this.version = version;
297     }
298     /**
299      * The product update version.
300      */
301     private String update;
302 
303     /**
304      * Get the value of update.
305      *
306      * @return the value of update
307      */
308     public String getUpdate() {
309         return update;
310     }
311 
312     /**
313      * Set the value of update.
314      *
315      * @param update new value of update
316      */
317     public void setUpdate(String update) {
318         this.update = update;
319     }
320     /**
321      * The product edition.
322      */
323     private String edition;
324 
325     /**
326      * Get the value of edition.
327      *
328      * @return the value of edition
329      */
330     public String getEdition() {
331         return edition;
332     }
333 
334     /**
335      * Set the value of edition.
336      *
337      * @param edition new value of edition
338      */
339     public void setEdition(String edition) {
340         this.edition = edition;
341     }
342 
343     /**
344      * Replaces '+' with '%2B' and then URL Decodes the string attempting first UTF-8, then ASCII, then default.
345      *
346      * @param string the string to URL Decode
347      * @return the URL Decoded string
348      */
349     private String urlDecode(String string) {
350         final String text = string.replace("+", "%2B");
351         String result;
352         try {
353             result = URLDecoder.decode(text, "UTF-8");
354         } catch (UnsupportedEncodingException ex) {
355             try {
356                 result = URLDecoder.decode(text, "ASCII");
357             } catch (UnsupportedEncodingException ex1) {
358                 result = defaultUrlDecode(text);
359             }
360         }
361         return result;
362     }
363 
364     /**
365      * Call {@link java.net.URLDecoder#decode(String)} to URL decode using the default encoding.
366      *
367      * @param text www-form-encoded URL to decode
368      * @return the newly decoded String
369      */
370     @SuppressWarnings("deprecation")
371     private String defaultUrlDecode(final String text) {
372         return URLDecoder.decode(text);
373     }
374 }