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