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) 2018 Steve Springett. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.data.nodeaudit;
19  
20  import java.io.BufferedInputStream;
21  import java.io.BufferedOutputStream;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.net.HttpURLConnection;
26  import java.net.MalformedURLException;
27  import java.net.URL;
28  import java.nio.charset.StandardCharsets;
29  import java.security.SecureRandom;
30  import java.util.List;
31  import javax.annotation.concurrent.ThreadSafe;
32  
33  import org.json.JSONObject;
34  import org.owasp.dependencycheck.utils.Settings;
35  import org.owasp.dependencycheck.utils.URLConnectionFactory;
36  import org.slf4j.Logger;
37  import org.slf4j.LoggerFactory;
38  
39  import javax.json.Json;
40  import javax.json.JsonObject;
41  import javax.json.JsonReader;
42  import org.apache.commons.jcs3.access.exception.CacheException;
43  
44  import static org.owasp.dependencycheck.analyzer.NodeAuditAnalyzer.DEFAULT_URL;
45  
46  import org.owasp.dependencycheck.analyzer.exception.SearchException;
47  import org.owasp.dependencycheck.analyzer.exception.UnexpectedAnalysisException;
48  import org.owasp.dependencycheck.data.cache.DataCache;
49  import org.owasp.dependencycheck.data.cache.DataCacheFactory;
50  import org.owasp.dependencycheck.utils.Checksum;
51  import org.owasp.dependencycheck.utils.URLConnectionFailureException;
52  
53  /**
54   * Class of methods to search via Node Audit API.
55   *
56   * @author Steve Springett
57   */
58  @ThreadSafe
59  public class NodeAuditSearch {
60  
61      /**
62       * The URL for the public Node Audit API.
63       */
64      private final URL nodeAuditUrl;
65  
66      /**
67       * Whether to use the Proxy when making requests.
68       */
69      private final boolean useProxy;
70      /**
71       * The configured settings.
72       */
73      private final Settings settings;
74      /**
75       * Used for logging.
76       */
77      private static final Logger LOGGER = LoggerFactory.getLogger(NodeAuditSearch.class);
78      /**
79       * Persisted disk cache for `npm audit` results.
80       */
81      private DataCache<List<Advisory>> cache;
82  
83      /**
84       * Creates a NodeAuditSearch for the given repository URL.
85       *
86       * @param settings the configured settings
87       * @throws java.net.MalformedURLException thrown if the configured URL is
88       * invalid
89       */
90      public NodeAuditSearch(Settings settings) throws MalformedURLException {
91          final String searchUrl = settings.getString(Settings.KEYS.ANALYZER_NODE_AUDIT_URL, DEFAULT_URL);
92          LOGGER.debug("Node Audit Search URL: {}", searchUrl);
93          this.nodeAuditUrl = new URL(searchUrl);
94          this.settings = settings;
95          if (null != settings.getString(Settings.KEYS.PROXY_SERVER)) {
96              useProxy = true;
97              LOGGER.debug("Using proxy");
98          } else {
99              useProxy = false;
100             LOGGER.debug("Not using proxy");
101         }
102         if (settings.getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_USE_CACHE, true)) {
103             try {
104                 final DataCacheFactory factory = new DataCacheFactory(settings);
105                 cache = factory.getNodeAuditCache();
106             } catch (CacheException ex) {
107                 settings.setBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_USE_CACHE, false);
108                 LOGGER.debug("Error creating cache, disabling caching", ex);
109             }
110         }
111     }
112 
113     /**
114      * Submits the package.json file to the Node Audit API and returns a list of
115      * zero or more Advisories.
116      *
117      * @param packageJson the package.json file retrieved from the Dependency
118      * @return a List of zero or more Advisory object
119      * @throws SearchException if Node Audit API is unable to analyze the
120      * package
121      * @throws IOException if it's unable to connect to Node Audit API
122      */
123     public List<Advisory> submitPackage(JsonObject packageJson) throws SearchException, IOException {
124         String key = null;
125         if (cache != null) {
126             key = Checksum.getSHA256Checksum(packageJson.toString());
127             final List<Advisory> cached = cache.get(key);
128             if (cached != null) {
129                 LOGGER.debug("cache hit for node audit: " + key);
130                 return cached;
131             }
132         }
133         return submitPackage(packageJson, key, 0);
134     }
135 
136     /**
137      * Submits the package.json file to the Node Audit API and returns a list of
138      * zero or more Advisories.
139      *
140      * @param packageJson the package.json file retrieved from the Dependency
141      * @param key the key for the cache entry
142      * @param count the current retry count
143      * @return a List of zero or more Advisory object
144      * @throws SearchException if Node Audit API is unable to analyze the
145      * package
146      * @throws IOException if it's unable to connect to Node Audit API
147      */
148     private List<Advisory> submitPackage(JsonObject packageJson, String key, int count) throws SearchException, IOException {
149         try {
150             if (LOGGER.isTraceEnabled()) {
151                 LOGGER.trace("----------------------------------------");
152                 LOGGER.trace("Node Audit Payload:");
153                 LOGGER.trace(packageJson.toString());
154                 LOGGER.trace("----------------------------------------");
155                 LOGGER.trace("----------------------------------------");
156             }
157             final byte[] packageDatabytes = packageJson.toString().getBytes(StandardCharsets.UTF_8);
158             final URLConnectionFactory factory = new URLConnectionFactory(settings);
159             final HttpURLConnection conn = factory.createHttpURLConnection(nodeAuditUrl, useProxy);
160             conn.setDoOutput(true);
161             conn.setDoInput(true);
162             conn.setRequestMethod("POST");
163             conn.setRequestProperty("user-agent", "npm/6.1.0 node/v10.5.0 linux x64");
164             conn.setRequestProperty("npm-in-ci", "false");
165             conn.setRequestProperty("npm-scope", "");
166             conn.setRequestProperty("npm-session", generateRandomSession());
167             conn.setRequestProperty("content-type", "application/json");
168             conn.setRequestProperty("Content-Length", Integer.toString(packageDatabytes.length));
169             conn.connect();
170 
171             try (OutputStream os = new BufferedOutputStream(conn.getOutputStream())) {
172                 os.write(packageDatabytes);
173                 os.flush();
174             }
175 
176             switch (conn.getResponseCode()) {
177                 case 200:
178                     try (InputStream in = new BufferedInputStream(conn.getInputStream());
179                             JsonReader jsonReader = Json.createReader(in)) {
180                         final JSONObject jsonResponse = new JSONObject(jsonReader.readObject().toString());
181                         final NpmAuditParser parser = new NpmAuditParser();
182                         final List<Advisory> advisories = parser.parse(jsonResponse);
183                         if (cache != null) {
184                             cache.put(key, advisories);
185                         }
186                         return advisories;
187                     } catch (Exception ex) {
188                         LOGGER.debug("Error connecting to Node Audit API. Error: {}",
189                                 ex.getMessage());
190                         throw new SearchException("Could not connect to Node Audit API: " + ex.getMessage(), ex);
191                     }
192                 case 503:
193                     LOGGER.debug("Node Audit API returned `{} {}` - retrying request.",
194                             conn.getResponseCode(), conn.getResponseMessage());
195                     if (count < 5) {
196                         final int next = count + 1;
197                         try {
198                             Thread.sleep(1500L * next);
199                         } catch (InterruptedException ex) {
200                             Thread.currentThread().interrupt();
201                             throw new UnexpectedAnalysisException(ex);
202                         }
203                         return submitPackage(packageJson, key, next);
204                     }
205                     throw new SearchException("Could not perform Node Audit analysis - service returned a 503.");
206                 case 400:
207                     LOGGER.debug("Invalid payload submitted to Node Audit API. Received response code: {} {}",
208                             conn.getResponseCode(), conn.getResponseMessage());
209                     throw new SearchException("Could not perform Node Audit analysis. Invalid payload submitted to Node Audit API.");
210                 default:
211                     LOGGER.debug("Could not connect to Node Audit API. Received response code: {} {}",
212                             conn.getResponseCode(), conn.getResponseMessage());
213                     throw new IOException("Could not connect to Node Audit API");
214             }
215         } catch (IOException ex) {
216             if (ex instanceof javax.net.ssl.SSLHandshakeException
217                     && ex.getMessage().contains("unable to find valid certification path to requested target")) {
218                 final String msg = String.format("Unable to connect to '%s' - the Java trust store does not contain a trusted root for the cert. "
219                         + " Please see https://github.com/jeremylong/InstallCert for one method of updating the trusted certificates.", nodeAuditUrl);
220                 throw new URLConnectionFailureException(msg, ex);
221             }
222             throw ex;
223         }
224     }
225 
226     /**
227      * Generates a random 16 character lower-case hex string.
228      *
229      * @return a random 16 character lower-case hex string
230      */
231     private String generateRandomSession() {
232         final int length = 16;
233         final SecureRandom r = new SecureRandom();
234         final StringBuilder sb = new StringBuilder();
235         while (sb.length() < length) {
236             sb.append(Integer.toHexString(r.nextInt()));
237         }
238         return sb.toString().substring(0, length);
239     }
240 }