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.IOException;
21  import java.net.MalformedURLException;
22  import java.net.URISyntaxException;
23  import java.net.URL;
24  import java.security.SecureRandom;
25  import java.util.ArrayList;
26  import java.util.List;
27  import javax.annotation.concurrent.ThreadSafe;
28  
29  import org.apache.hc.client5.http.HttpResponseException;
30  import org.apache.hc.core5.http.ContentType;
31  import org.apache.hc.core5.http.Header;
32  import org.apache.hc.core5.http.HttpHeaders;
33  import org.apache.hc.core5.http.message.BasicHeader;
34  import org.json.JSONException;
35  import org.json.JSONObject;
36  import org.owasp.dependencycheck.utils.DownloadFailedException;
37  import org.owasp.dependencycheck.utils.Downloader;
38  import org.owasp.dependencycheck.utils.ResourceNotFoundException;
39  import org.owasp.dependencycheck.utils.Settings;
40  import org.owasp.dependencycheck.utils.TooManyRequestsException;
41  import org.slf4j.Logger;
42  import org.slf4j.LoggerFactory;
43  
44  import javax.json.JsonObject;
45  import org.apache.commons.jcs3.access.exception.CacheException;
46  
47  import static org.owasp.dependencycheck.analyzer.NodeAuditAnalyzer.DEFAULT_URL;
48  
49  import org.owasp.dependencycheck.analyzer.exception.SearchException;
50  import org.owasp.dependencycheck.analyzer.exception.UnexpectedAnalysisException;
51  import org.owasp.dependencycheck.data.cache.DataCache;
52  import org.owasp.dependencycheck.data.cache.DataCacheFactory;
53  import org.owasp.dependencycheck.utils.Checksum;
54  
55  /**
56   * Class of methods to search via Node Audit API.
57   *
58   * @author Steve Springett
59   */
60  @ThreadSafe
61  public class NodeAuditSearch {
62  
63      /**
64       * The URL for the public Node Audit API.
65       */
66      private final URL nodeAuditUrl;
67  
68      /**
69       * Whether to use the Proxy when making requests.
70       */
71      private final boolean useProxy;
72      /**
73       * The configured settings.
74       */
75      private final Settings settings;
76      /**
77       * Used for logging.
78       */
79      private static final Logger LOGGER = LoggerFactory.getLogger(NodeAuditSearch.class);
80      /**
81       * Persisted disk cache for `npm audit` results.
82       */
83      private DataCache<List<Advisory>> cache;
84  
85      /**
86       * Creates a NodeAuditSearch for the given repository URL.
87       *
88       * @param settings the configured settings
89       * @throws java.net.MalformedURLException thrown if the configured URL is
90       * invalid
91       */
92      public NodeAuditSearch(Settings settings) throws MalformedURLException {
93          final String searchUrl = settings.getString(Settings.KEYS.ANALYZER_NODE_AUDIT_URL, DEFAULT_URL);
94          LOGGER.debug("Node Audit Search URL: {}", searchUrl);
95          this.nodeAuditUrl = new URL(searchUrl);
96          this.settings = settings;
97          if (null != settings.getString(Settings.KEYS.PROXY_SERVER)) {
98              useProxy = true;
99              LOGGER.debug("Using proxy");
100         } else {
101             useProxy = false;
102             LOGGER.debug("Not using proxy");
103         }
104         if (settings.getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_USE_CACHE, true)) {
105             try {
106                 final DataCacheFactory factory = new DataCacheFactory(settings);
107                 cache = factory.getNodeAuditCache();
108             } catch (CacheException ex) {
109                 settings.setBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_USE_CACHE, false);
110                 LOGGER.debug("Error creating cache, disabling caching", ex);
111             }
112         }
113     }
114 
115     /**
116      * Submits the package.json file to the Node Audit API and returns a list of
117      * zero or more Advisories.
118      *
119      * @param packageJson the package.json file retrieved from the Dependency
120      * @return a List of zero or more Advisory object
121      * @throws SearchException if Node Audit API is unable to analyze the
122      * package
123      * @throws IOException if it's unable to connect to Node Audit API
124      */
125     public List<Advisory> submitPackage(JsonObject packageJson) throws SearchException, IOException {
126         String key = null;
127         if (cache != null) {
128             key = Checksum.getSHA256Checksum(packageJson.toString());
129             final List<Advisory> cached = cache.get(key);
130             if (cached != null) {
131                 LOGGER.debug("cache hit for node audit: " + key);
132                 return cached;
133             }
134         }
135         return submitPackage(packageJson, key, 0);
136     }
137 
138     /**
139      * Submits the package.json file to the Node Audit API and returns a list of
140      * zero or more Advisories.
141      *
142      * @param packageJson the package.json file retrieved from the Dependency
143      * @param key the key for the cache entry
144      * @param count the current retry count
145      * @return a List of zero or more Advisory object
146      * @throws SearchException if Node Audit API is unable to analyze the
147      * package
148      * @throws IOException if it's unable to connect to Node Audit API
149      */
150     private List<Advisory> submitPackage(JsonObject packageJson, String key, int count) throws SearchException, IOException {
151         if (LOGGER.isTraceEnabled()) {
152             LOGGER.trace("----------------------------------------");
153             LOGGER.trace("Node Audit Payload:");
154             LOGGER.trace(packageJson.toString());
155             LOGGER.trace("----------------------------------------");
156             LOGGER.trace("----------------------------------------");
157         }
158         final List<Header> additionalHeaders = new ArrayList<>();
159         additionalHeaders.add(new BasicHeader(HttpHeaders.USER_AGENT, "npm/6.1.0 node/v10.5.0 linux x64"));
160         additionalHeaders.add(new BasicHeader("npm-in-ci", "false"));
161         additionalHeaders.add(new BasicHeader("npm-scope", ""));
162         additionalHeaders.add(new BasicHeader("npm-session", generateRandomSession()));
163 
164         try {
165             final String response = Downloader.getInstance().postBasedFetchContent(nodeAuditUrl.toURI(),
166                     packageJson.toString(), ContentType.APPLICATION_JSON, additionalHeaders);
167             final JSONObject jsonResponse = new JSONObject(response);
168             final NpmAuditParser parser = new NpmAuditParser();
169             final List<Advisory> advisories = parser.parse(jsonResponse);
170             if (cache != null) {
171                 cache.put(key, advisories);
172             }
173             return advisories;
174         } catch (RuntimeException | URISyntaxException | JSONException | TooManyRequestsException | ResourceNotFoundException ex) {
175             LOGGER.debug("Error connecting to Node Audit API. Error: {}",
176                     ex.getMessage());
177             throw new SearchException("Could not connect to Node Audit API: " + ex.getMessage(), ex);
178         } catch (DownloadFailedException e) {
179             if (e.getCause() instanceof HttpResponseException) {
180                 final HttpResponseException hre = (HttpResponseException) e.getCause();
181                 switch (hre.getStatusCode()) {
182                     case 503:
183                         LOGGER.debug("Node Audit API returned `{} {}` - retrying request.",
184                                 hre.getStatusCode(), hre.getReasonPhrase());
185                         if (count < 5) {
186                             final int next = count + 1;
187                             try {
188                                 Thread.sleep(1500L * next);
189                             } catch (InterruptedException ex) {
190                                 Thread.currentThread().interrupt();
191                                 throw new UnexpectedAnalysisException(ex);
192                             }
193                             return submitPackage(packageJson, key, next);
194                         }
195                         throw new SearchException("Could not perform Node Audit analysis - service returned a 503.", e);
196                     case 400:
197                         LOGGER.debug("Invalid payload submitted to Node Audit API. Received response code: {} {}",
198                                 hre.getStatusCode(), hre.getReasonPhrase());
199                         throw new SearchException("Could not perform Node Audit analysis. Invalid payload submitted to Node Audit API.", e);
200                     default:
201                         LOGGER.debug("Could not connect to Node Audit API. Received response code: {} {}",
202                                 hre.getStatusCode(), hre.getReasonPhrase());
203                         throw new IOException("Could not connect to Node Audit API", e);
204                 }
205             } else {
206                 LOGGER.debug("Could not connect to Node Audit API. Received generic DownloadException", e);
207                 throw new IOException("Could not connect to Node Audit API", e);
208             }
209         }
210     }
211 
212     /**
213      * Generates a random 16 character lower-case hex string.
214      *
215      * @return a random 16 character lower-case hex string
216      */
217     private String generateRandomSession() {
218         final int length = 16;
219         final SecureRandom r = new SecureRandom();
220         final StringBuilder sb = new StringBuilder();
221         while (sb.length() < length) {
222             sb.append(Integer.toHexString(r.nextInt()));
223         }
224         return sb.substring(0, length);
225     }
226 }