1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.owasp.dependencycheck.data.central;
19
20 import org.apache.hc.client5.http.impl.classic.AbstractHttpClientResponseHandler;
21 import org.apache.hc.core5.http.HttpEntity;
22 import org.apache.hc.core5.http.message.BasicHeader;
23 import org.owasp.dependencycheck.utils.DownloadFailedException;
24 import org.owasp.dependencycheck.utils.Downloader;
25 import org.owasp.dependencycheck.utils.ResourceNotFoundException;
26 import org.owasp.dependencycheck.utils.TooManyRequestsException;
27 import java.io.FileNotFoundException;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.net.MalformedURLException;
31 import java.net.URISyntaxException;
32 import java.net.URL;
33 import java.util.ArrayList;
34 import java.util.List;
35 import javax.annotation.concurrent.ThreadSafe;
36 import javax.xml.parsers.DocumentBuilder;
37 import javax.xml.parsers.ParserConfigurationException;
38 import javax.xml.xpath.XPath;
39 import javax.xml.xpath.XPathConstants;
40 import javax.xml.xpath.XPathExpressionException;
41 import javax.xml.xpath.XPathFactory;
42 import org.apache.commons.jcs3.access.exception.CacheException;
43 import org.owasp.dependencycheck.data.cache.DataCache;
44 import org.owasp.dependencycheck.data.cache.DataCacheFactory;
45 import org.owasp.dependencycheck.data.nexus.MavenArtifact;
46 import org.owasp.dependencycheck.utils.Settings;
47 import org.owasp.dependencycheck.utils.XmlUtils;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50 import org.w3c.dom.Document;
51 import org.w3c.dom.NodeList;
52 import org.xml.sax.SAXException;
53
54
55
56
57
58
59 @ThreadSafe
60 public class CentralSearch {
61
62
63
64
65 private final String rootURL;
66
67
68
69
70 private final String query;
71
72
73
74
75 private final boolean useProxy;
76
77
78
79
80 private static final Logger LOGGER = LoggerFactory.getLogger(CentralSearch.class);
81
82
83
84 private final Settings settings;
85
86
87
88 private DataCache<List<MavenArtifact>> cache;
89
90
91
92
93
94
95
96 public CentralSearch(Settings settings) throws MalformedURLException {
97 this.settings = settings;
98
99 final String searchUrl = settings.getString(Settings.KEYS.ANALYZER_CENTRAL_URL);
100 LOGGER.debug("Central Search URL: {}", searchUrl);
101 if (isInvalidURL(searchUrl)) {
102 throw new MalformedURLException(String.format("The configured central analyzer URL is invalid: %s", searchUrl));
103 }
104 this.rootURL = searchUrl;
105 final String queryStr = settings.getString(Settings.KEYS.ANALYZER_CENTRAL_QUERY);
106 LOGGER.debug("Central Search Query: {}", queryStr);
107 if (!queryStr.matches("^%s.*%s.*$")) {
108 final String msg = String.format("The configured central analyzer query parameter is invalid (it must have two %%s): %s", queryStr);
109 throw new MalformedURLException(msg);
110 }
111 this.query = queryStr;
112 LOGGER.debug("Central Search Full URL: {}", String.format(query, rootURL, "[SHA1]"));
113 if (null != settings.getString(Settings.KEYS.PROXY_SERVER) || null != System.getProperty("https.proxyHost")) {
114 useProxy = true;
115 LOGGER.debug("Using proxy");
116 } else {
117 useProxy = false;
118 LOGGER.debug("Not using proxy");
119 }
120 if (settings.getBoolean(Settings.KEYS.ANALYZER_CENTRAL_USE_CACHE, true)) {
121 try {
122 final DataCacheFactory factory = new DataCacheFactory(settings);
123 cache = factory.getCentralCache();
124 } catch (CacheException ex) {
125 settings.setBoolean(Settings.KEYS.ANALYZER_CENTRAL_USE_CACHE, false);
126 LOGGER.debug("Error creating cache, disabling caching", ex);
127 }
128 }
129 }
130
131
132
133
134
135
136
137
138
139
140
141
142
143 public List<MavenArtifact> searchSha1(String sha1) throws IOException, TooManyRequestsException {
144 if (null == sha1 || !sha1.matches("^[0-9A-Fa-f]{40}$")) {
145 throw new IllegalArgumentException("Invalid SHA1 format");
146 }
147 if (cache != null) {
148 final List<MavenArtifact> cached = cache.get(sha1);
149 if (cached != null) {
150 LOGGER.debug("cache hit for Central: " + sha1);
151 if (cached.isEmpty()) {
152 throw new FileNotFoundException("Artifact not found in Central");
153 }
154 return cached;
155 }
156 }
157 final List<MavenArtifact> result = new ArrayList<>();
158 final URL url = new URL(String.format(query, rootURL, sha1));
159
160 LOGGER.trace("Searching Central url {}", url);
161
162
163
164 final BasicHeader acceptHeader = new BasicHeader("Accept", "application/xml");
165 final AbstractHttpClientResponseHandler<Document> handler = new AbstractHttpClientResponseHandler<>() {
166 @Override
167 public Document handleEntity(HttpEntity entity) throws IOException {
168 try (InputStream in = entity.getContent()) {
169 final DocumentBuilder builder = XmlUtils.buildSecureDocumentBuilder();
170 return builder.parse(in);
171 } catch (ParserConfigurationException | SAXException | IOException e) {
172
173 final String errorMessage = "Failed to parse MavenCentral XML Response: " + e.getMessage();
174 throw new IOException(errorMessage, e);
175 }
176 }
177 };
178 try {
179 final Document doc = Downloader.getInstance().fetchAndHandle(url, handler, List.of(acceptHeader), useProxy);
180 final boolean missing = addMavenArtifacts(doc, result);
181
182 if (missing) {
183 if (cache != null) {
184 cache.put(sha1, result);
185 }
186 throw new FileNotFoundException("Artifact not found in Central");
187 }
188 } catch (XPathExpressionException e) {
189 final String errorMessage = "Failed to parse MavenCentral XML Response: " + e.getMessage();
190 throw new IOException(errorMessage, e);
191 } catch (TooManyRequestsException e) {
192 final String errorMessage = "Too many requests sent to MavenCentral; additional requests are being rejected.";
193 throw new TooManyRequestsException(errorMessage, e);
194 } catch (ResourceNotFoundException | DownloadFailedException e) {
195 final String errorMessage = "Could not connect to MavenCentral " + e.getMessage();
196 throw new IOException(errorMessage, e);
197 }
198 if (cache != null) {
199 cache.put(sha1, result);
200 }
201 return result;
202 }
203
204
205
206
207
208
209
210 private boolean addMavenArtifacts(Document doc, List<MavenArtifact> result) throws XPathExpressionException {
211 boolean missing = false;
212 final XPath xpath = XPathFactory.newInstance().newXPath();
213 final String numFound = xpath.evaluate("/response/result/@numFound", doc);
214 if ("0".equals(numFound)) {
215 missing = true;
216 } else {
217 final NodeList docs = (NodeList) xpath.evaluate("/response/result/doc", doc, XPathConstants.NODESET);
218 for (int i = 0; i < docs.getLength(); i++) {
219 final String g = xpath.evaluate("./str[@name='g']", docs.item(i));
220 LOGGER.trace("GroupId: {}", g);
221 final String a = xpath.evaluate("./str[@name='a']", docs.item(i));
222 LOGGER.trace("ArtifactId: {}", a);
223 final String v = xpath.evaluate("./str[@name='v']", docs.item(i));
224 final NodeList attributes = (NodeList) xpath.evaluate("./arr[@name='ec']/str", docs.item(i), XPathConstants.NODESET);
225 boolean pomAvailable = false;
226 boolean jarAvailable = false;
227 for (int x = 0; x < attributes.getLength(); x++) {
228 final String tmp = xpath.evaluate(".", attributes.item(x));
229 if (".pom".equals(tmp)) {
230 pomAvailable = true;
231 } else if (".jar".equals(tmp)) {
232 jarAvailable = true;
233 }
234 }
235 final String centralContentUrl = settings.getString(Settings.KEYS.CENTRAL_CONTENT_URL);
236 String artifactUrl = null;
237 String pomUrl = null;
238 if (jarAvailable) {
239
240 artifactUrl = centralContentUrl + g.replace('.', '/') + '/' + a + '/'
241 + v + '/' + a + '-' + v + ".jar";
242 }
243 if (pomAvailable) {
244
245 pomUrl = centralContentUrl + g.replace('.', '/') + '/' + a + '/'
246 + v + '/' + a + '-' + v + ".pom";
247 }
248 result.add(new MavenArtifact(g, a, v, artifactUrl, pomUrl));
249 }
250 }
251 return missing;
252 }
253
254
255
256
257
258
259
260 private boolean isInvalidURL(String url) {
261 try {
262 final URL u = new URL(url);
263 u.toURI();
264 } catch (MalformedURLException | URISyntaxException e) {
265 LOGGER.trace("URL is invalid: {}", url);
266 return true;
267 }
268 return false;
269 }
270 }