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