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> 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 }