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 *
230 * @param str the string to test
231 * @return true if the string only contains 0-9, otherwise false.
232 */
233 private static boolean isPositiveInteger(final String str) {
234 if (str == null || str.isEmpty()) {
235 return false;
236 }
237 for (int i = 0; i < str.length(); i++) {
238 final char c = str.charAt(i);
239 if (c < '0' || c > '9') {
240 return false;
241 }
242 }
243 return true;
244 }
245 /**
246 * The name of the cpe.
247 */
248 private String name;
249
250 /**
251 * Get the value of name.
252 *
253 * @return the value of name
254 */
255 public String getName() {
256 return name;
257 }
258
259 /**
260 * Set the value of name.
261 *
262 * @param name new value of name
263 */
264 public void setName(String name) {
265 this.name = name;
266 }
267 /**
268 * The product version number.
269 */
270 private String version;
271
272 /**
273 * Get the value of version.
274 *
275 * @return the value of version
276 */
277 public String getVersion() {
278 return version;
279 }
280
281 /**
282 * Set the value of version.
283 *
284 * @param version new value of version
285 */
286 public void setVersion(String version) {
287 this.version = version;
288 }
289 /**
290 * The product update version.
291 */
292 private String update;
293
294 /**
295 * Get the value of update.
296 *
297 * @return the value of update
298 */
299 public String getUpdate() {
300 return update;
301 }
302
303 /**
304 * Set the value of update.
305 *
306 * @param update new value of update
307 */
308 public void setUpdate(String update) {
309 this.update = update;
310 }
311 /**
312 * The product edition.
313 */
314 private String edition;
315
316 /**
317 * Get the value of edition.
318 *
319 * @return the value of edition
320 */
321 public String getEdition() {
322 return edition;
323 }
324
325 /**
326 * Set the value of edition.
327 *
328 * @param edition new value of edition
329 */
330 public void setEdition(String edition) {
331 this.edition = edition;
332 }
333
334 /**
335 * Replaces '+' with '%2B' and then URL Decodes the string attempting first UTF-8, then ASCII, then default.
336 *
337 * @param string the string to URL Decode
338 * @return the URL Decoded string
339 */
340 private String urlDecode(String string) {
341 final String text = string.replace("+", "%2B");
342 String result;
343 try {
344 result = URLDecoder.decode(text, "UTF-8");
345 } catch (UnsupportedEncodingException ex) {
346 try {
347 result = URLDecoder.decode(text, "ASCII");
348 } catch (UnsupportedEncodingException ex1) {
349 result = defaultUrlDecode(text);
350 }
351 }
352 return result;
353 }
354
355 /**
356 * Call {@link java.net.URLDecoder#decode(String)} to URL decode using the default encoding.
357 *
358 * @param text www-form-encoded URL to decode
359 * @return the newly decoded String
360 */
361 @SuppressWarnings("deprecation")
362 private String defaultUrlDecode(final String text) {
363 return URLDecoder.decode(text);
364 }
365 }