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) 2020 The OWASP Foundation. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.analyzer;
19  
20  import com.fasterxml.jackson.annotation.JsonProperty;
21  import com.fasterxml.jackson.databind.DeserializationFeature;
22  import com.fasterxml.jackson.databind.JsonNode;
23  import com.fasterxml.jackson.databind.ObjectMapper;
24  import com.fasterxml.jackson.databind.ObjectReader;
25  import com.github.packageurl.MalformedPackageURLException;
26  import com.github.packageurl.PackageURL;
27  import com.github.packageurl.PackageURLBuilder;
28  import org.owasp.dependencycheck.Engine;
29  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
30  import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
31  import org.owasp.dependencycheck.dependency.Confidence;
32  import org.owasp.dependencycheck.dependency.Dependency;
33  import org.owasp.dependencycheck.dependency.EvidenceType;
34  import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
35  import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
36  import org.owasp.dependencycheck.utils.Settings;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  import javax.annotation.concurrent.ThreadSafe;
41  import java.io.File;
42  import java.io.FileFilter;
43  import java.io.IOException;
44  import java.util.Collections;
45  import java.util.List;
46  import java.util.Map;
47  import java.util.Objects;
48  import java.util.regex.Pattern;
49  import java.util.stream.Collectors;
50  
51  import static org.owasp.dependencycheck.utils.FileUtils.existsWithContent;
52  
53  /**
54   * Used to analyze Maven pinned dependency files named {@code *install*.json}, a
55   * Java Maven dependency lockfile like Python's {@code requirements.txt}.
56   *
57   * @author dhalperi
58   * @see
59   * <a href="https://github.com/bazelbuild/rules_jvm_external#pinning-artifacts-and-integration-with-bazels-downloader">rules_jvm_external</a>
60   */
61  @Experimental
62  @ThreadSafe
63  public class PinnedMavenInstallAnalyzer extends AbstractFileTypeAnalyzer {
64  
65      /**
66       * The logger.
67       */
68      private static final Logger LOGGER = LoggerFactory.getLogger(PinnedMavenInstallAnalyzer.class);
69  
70      /**
71       * The name of the analyzer.
72       */
73      private static final String ANALYZER_NAME = "Pinned Maven install Analyzer";
74  
75      /**
76       * The phase that this analyzer is intended to run in.
77       */
78      private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
79  
80      /**
81       * Pattern matching files with "install" in the basename and extension
82       * "json".
83       *
84       * <p>
85       * This regex is designed to explicitly skip files named
86       * {@code install.json} since those are used for Cloudflare installations
87       * and this will save on work.
88       */
89      private static final Pattern MAVEN_INSTALL_JSON_PATTERN = Pattern.compile("(.+install.*|.*install.+)\\.json");
90  
91      /**
92       * Match any files that look like *install*.json.
93       */
94      private static final FileFilter FILTER = (File file) -> MAVEN_INSTALL_JSON_PATTERN.matcher(file.getName()).matches();
95  
96      @Override
97      protected FileFilter getFileFilter() {
98          return FILTER;
99      }
100 
101     @Override
102     public String getName() {
103         return ANALYZER_NAME;
104     }
105 
106     @Override
107     public AnalysisPhase getAnalysisPhase() {
108         return ANALYSIS_PHASE;
109     }
110 
111     @Override
112     protected String getAnalyzerEnabledSettingKey() {
113         return Settings.KEYS.ANALYZER_MAVEN_INSTALL_ENABLED;
114     }
115 
116     @Override
117     protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
118         LOGGER.debug("Checking file {}", dependency.getActualFilePath());
119 
120         final File dependencyFile = dependency.getActualFile();
121         if (!existsWithContent(dependencyFile)) {
122             return;
123         }
124 
125         final DependencyTree tree;
126         List<MavenDependency> deps;
127         try {
128             final JsonNode jsonNode = MAPPER.readTree(dependencyFile);
129             final JsonNode v2Version = jsonNode.path("version");
130             final JsonNode v010Version = jsonNode.path("dependency_tree").path("version");
131 
132             if (v2Version.isTextual()) {
133                 final InstallFileV2 installFile = INSTALL_FILE_V2_READER.readValue(dependencyFile);
134                 if (!Objects.equals(installFile.getAutogeneratedSentinel(), "THERE_IS_NO_DATA_ONLY_ZUUL")) {
135                     return;
136                 }
137                 if (!Objects.equals(installFile.getVersion(), "2")) {
138                     LOGGER.warn("Unsupported pinned maven_install.json version {}. Continuing optimistically.", installFile.getVersion());
139                 }
140                 deps = installFile.getArtifacts().entrySet().stream().map(entry -> new MavenDependency(
141                         entry.getKey() + ":" + entry.getValue().getVersion()
142                 )).collect(Collectors.toList());
143             } else if (v010Version.isTextual()) {
144                 final InstallFile installFile = INSTALL_FILE_READER.readValue(dependencyFile);
145                 tree = installFile.getDependencyTree();
146                 if (tree == null) {
147                     return;
148                 } else if (!Objects.equals(tree.getAutogeneratedSentinel(), "THERE_IS_NO_DATA_ONLY_ZUUL")) {
149                     return;
150                 }
151                 if (!Objects.equals(tree.getVersion(), "0.1.0")) {
152                     LOGGER.warn("Unsupported pinned maven_install.json version {}. Continuing optimistically.", tree.getVersion());
153                 }
154                 deps = tree.getDependencies();
155             } else {
156                 LOGGER.warn("No pinned maven_install.json version found. Cannot Parse");
157                 return;
158             }
159 
160         } catch (IOException e) {
161             System.out.println("e");
162             return;
163         }
164 
165         engine.removeDependency(dependency);
166 
167         if (deps == null) {
168             deps = Collections.emptyList();
169         }
170 
171         for (MavenDependency dep : deps) {
172             if (dep.getCoord() == null) {
173                 LOGGER.warn("Unexpected null coordinate in {}", dependency.getActualFilePath());
174                 continue;
175             }
176 
177             LOGGER.debug("Analyzing {}", dep.getCoord());
178             final String[] pieces = dep.getCoord().split(":");
179             if (pieces.length < 3 || pieces.length > 5) {
180                 LOGGER.warn("Invalid maven coordinate {}", dep.getCoord());
181                 continue;
182             }
183 
184             final String group = pieces[0];
185             final String artifact = pieces[1];
186             final String version;
187             String classifier = null;
188             switch (pieces.length) {
189                 case 3:
190                     version = pieces[2];
191                     break;
192                 case 4:
193                     classifier = pieces[2];
194                     version = pieces[3];
195                     break;
196                 default:
197                     // length == 5 as guaranteed above.
198                     classifier = pieces[3];
199                     version = pieces[4];
200                     break;
201             }
202 
203             if ("sources".equals(classifier) || "javadoc".equals(classifier)) {
204                 LOGGER.debug("Skipping sources jar {}", dep.getCoord());
205                 continue;
206             }
207 
208             final Dependency d = new Dependency(dependency.getActualFile(), true);
209             d.setEcosystem(Ecosystem.JAVA);
210             d.addEvidence(EvidenceType.VENDOR, "project", "groupid", group, Confidence.HIGHEST);
211             d.addEvidence(EvidenceType.PRODUCT, "project", "artifactid", artifact, Confidence.HIGHEST);
212             d.addEvidence(EvidenceType.VENDOR, "project", "artifactid", artifact, Confidence.HIGH);
213             d.addEvidence(EvidenceType.VERSION, "project", "version", version, Confidence.HIGHEST);
214             d.setName(String.format("%s:%s", group, artifact));
215             d.setFilePath(String.format("%s>>%s", dependency.getActualFile(), dep.getCoord()));
216             d.setFileName(dep.getCoord());
217             try {
218                 final PackageURLBuilder purl = PackageURLBuilder.aPackageURL()
219                         .withType(PackageURL.StandardTypes.MAVEN)
220                         .withNamespace(group)
221                         .withName(artifact)
222                         .withVersion(version);
223                 if (classifier != null) {
224                     purl.withQualifier("classifier", classifier);
225                 }
226                 d.addSoftwareIdentifier(new PurlIdentifier(purl.build(), Confidence.HIGHEST));
227             } catch (MalformedPackageURLException e) {
228                 d.addSoftwareIdentifier(new GenericIdentifier("maven_install JSON coord " + dep.getCoord(), Confidence.HIGH));
229             }
230             d.setVersion(version);
231             engine.addDependency(d);
232         }
233     }
234 
235     @Override
236     protected void prepareFileTypeAnalyzer(Engine engine) {
237         // No initialization needed.
238     }
239 
240     /**
241      * Represents the entire pinned Maven dependency set in an install.json
242      * file.
243      *
244      * <p>
245      * At the time of writing, the latest version is 0.1.0, and the dependencies
246      * are stored in {@code .dependency_tree.dependencies[].coord}.
247      *
248      * <p>
249      * The only top-level key we care about is {@code .dependency_tree}.
250      */
251     private static class InstallFile {
252 
253         /**
254          * The dependency tree.
255          */
256         @JsonProperty("dependency_tree")
257         private DependencyTree dependencyTree;
258 
259         /**
260          * Returns dependencyTree.
261          *
262          * @return dependencyTree
263          */
264         public DependencyTree getDependencyTree() {
265             return dependencyTree;
266         }
267     }
268 
269     /**
270      * Represents the values at {@code .dependency_tree} in the
271      * {@link InstallFile install file}.
272      */
273     private static class DependencyTree {
274 
275         /**
276          * A sentinel value placed in the file to indicate that it is an
277          * auto-generated pinned maven install file.
278          */
279         @JsonProperty("__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY")
280         private String autogeneratedSentinel;
281 
282         /**
283          * A list of Maven dependencies made available. Note that this list is
284          * transitively closed and pinned to a specific version of each
285          * artifact.
286          */
287         @JsonProperty("dependencies")
288         private List<MavenDependency> dependencies;
289 
290         /**
291          * The file format version.
292          */
293         @JsonProperty("version")
294         private String version;
295 
296         /**
297          * Returns autogeneratedSentinel.
298          *
299          * @return autogeneratedSentinel
300          */
301         public String getAutogeneratedSentinel() {
302             return autogeneratedSentinel;
303         }
304 
305         /**
306          * Returns dependencies.
307          *
308          * @return dependencies
309          */
310         public List<MavenDependency> getDependencies() {
311             return dependencies;
312         }
313 
314         /**
315          * Returns version.
316          *
317          * @return version
318          */
319         public String getVersion() {
320             return version;
321         }
322 
323     }
324 
325     /**
326      * Represents a single dependency in the list at
327      * {@code .dependency_tree.dependencies}.
328      */
329     private static class MavenDependency {
330 
331         MavenDependency(String coord) {
332             this.coord = coord;
333         }
334 
335         MavenDependency() {
336         }
337         /**
338          * The standard Maven coordinate string
339          * {@code group:artifact[:optional classifier][:optional packaging]:version}.
340          */
341         @JsonProperty("coord")
342         private String coord;
343 
344         /**
345          * Returns the value of coord.
346          *
347          * @return the value of coord
348          */
349         public String getCoord() {
350             return coord;
351         }
352     }
353 
354     /**
355      * A reusable reader for {@link InstallFile}.
356      */
357     private static final ObjectReader INSTALL_FILE_READER;
358     /**
359      * A reusable reader for {@link InstallFileV2}.
360      */
361     private static final ObjectReader INSTALL_FILE_V2_READER;
362     /**
363      * A reusable object mapper.
364      */
365     private static final ObjectMapper MAPPER;
366 
367     static {
368         MAPPER = new ObjectMapper();
369         MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
370         INSTALL_FILE_READER = MAPPER.readerFor(InstallFile.class);
371         INSTALL_FILE_V2_READER = MAPPER.readerFor(InstallFileV2.class);
372     }
373 
374     /**
375      * Represents the entire pinned Maven dependency set in an install.json
376      * file.
377      *
378      * <p>
379      * At the time of writing, the latest version is 2, and the dependencies are
380      * stored in {@code .artifacts}.
381      *
382      * <p>
383      * The top-level keys we care about are {@code .artifacts}.
384      * {@code .version}.
385      */
386     private static class InstallFileV2 {
387 
388         /**
389          * The file format version.
390          */
391         @JsonProperty("version")
392         private String version;
393 
394         /**
395          * A list of Maven dependencies made available. Note that this map is
396          * transitively closed and pinned to a specific version of each
397          * artifact.
398          * <p>
399          * The key is the Maven coordinate string, less the version
400          * {@code group:artifact[:optional classifier][:optional packaging]}.
401          * <p>
402          * The value contains the version of the artifact.
403          */
404         @JsonProperty("artifacts")
405         private Map<String, Artifactv2> artifacts;
406 
407         /**
408          * A sentinel value placed in the file to indicate that it is an
409          * auto-generated pinned maven install file.
410          */
411         @JsonProperty("__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY")
412         private String autogeneratedSentinel;
413 
414         /**
415          * Returns artifacts.
416          *
417          * @return artifacts
418          */
419         public Map<String, Artifactv2> getArtifacts() {
420             return artifacts;
421         }
422 
423         /**
424          * Returns version.
425          *
426          * @return version
427          */
428         public String getVersion() {
429             return version;
430         }
431 
432         /**
433          * Returns autogeneratedSentinel.
434          *
435          * @return autogeneratedSentinel
436          */
437         public String getAutogeneratedSentinel() {
438             return autogeneratedSentinel;
439         }
440     }
441 
442     private static class Artifactv2 {
443 
444         /**
445          * The version of the artifact.
446          */
447         @JsonProperty("version")
448         private String version;
449 
450         /**
451          * Returns the value of version.
452          *
453          * @return the value of version
454          */
455         public String getVersion() {
456             return version;
457         }
458     }
459 
460 }