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