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             JsonNode jsonNode = MAPPER.readTree(dependencyFile);
127             JsonNode v2Version = jsonNode.path("version");
128             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 
159         } catch (IOException e) {
160             System.out.println("e");
161             return;
162         }
163 
164         engine.removeDependency(dependency);
165 
166         if (deps == null) {
167             deps = Collections.emptyList();
168         }
169 
170         for (MavenDependency dep : deps) {
171             if (dep.getCoord() == null) {
172                 LOGGER.warn("Unexpected null coordinate in {}", dependency.getActualFilePath());
173                 continue;
174             }
175 
176             LOGGER.debug("Analyzing {}", dep.getCoord());
177             final String[] pieces = dep.getCoord().split(":");
178             if (pieces.length < 3 || pieces.length > 5) {
179                 LOGGER.warn("Invalid maven coordinate {}", dep.getCoord());
180                 continue;
181             }
182 
183             final String group = pieces[0];
184             final String artifact = pieces[1];
185             final String version;
186             String classifier = null;
187             switch (pieces.length) {
188                 case 3:
189                     version = pieces[2];
190                     break;
191                 case 4:
192                     classifier = pieces[2];
193                     version = pieces[3];
194                     break;
195                 default:
196                     // length == 5 as guaranteed above.
197                     classifier = pieces[3];
198                     version = pieces[4];
199                     break;
200             }
201 
202             if ("sources".equals(classifier) || "javadoc".equals(classifier)) {
203                 LOGGER.debug("Skipping sources jar {}", dep.getCoord());
204                 continue;
205             }
206 
207             final Dependency d = new Dependency(dependency.getActualFile(), true);
208             d.setEcosystem(Ecosystem.JAVA);
209             d.addEvidence(EvidenceType.VENDOR, "project", "groupid", group, Confidence.HIGHEST);
210             d.addEvidence(EvidenceType.PRODUCT, "project", "artifactid", artifact, Confidence.HIGHEST);
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         public MavenDependency(String coord) {
329             this.coord = coord;
330         }
331 
332         public MavenDependency() {
333         }
334         /**
335          * The standard Maven coordinate string
336          * {@code group:artifact[:optional classifier][:optional packaging]:version}.
337          */
338         @JsonProperty("coord")
339         private String coord;
340 
341         /**
342          * Returns the value of coord.
343          *
344          * @return the value of coord
345          */
346         public String getCoord() {
347             return coord;
348         }
349     }
350 
351     /**
352      * A reusable reader for {@link InstallFile}.
353      */
354     private static final ObjectReader INSTALL_FILE_READER;
355     private static final ObjectReader INSTALL_FILE_V2_READER;
356     private static final ObjectMapper MAPPER;
357 
358     static {
359         MAPPER = new ObjectMapper();
360         MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
361         INSTALL_FILE_READER = MAPPER.readerFor(InstallFile.class);
362         INSTALL_FILE_V2_READER = MAPPER.readerFor(InstallFileV2.class);
363     }
364 
365     /**
366      * Represents the entire pinned Maven dependency set in an install.json
367      * file.
368      *
369      * <p>
370      * At the time of writing, the latest version is 2, and the dependencies
371      * are stored in {@code .artifacts}.
372      *
373      * <p>
374      * The top-level keys we care about are {@code .artifacts}. {@code .version}.
375      */
376     private static class InstallFileV2 {
377 
378         /**
379          * The file format version.
380          */
381         @JsonProperty("version")
382         private String version;
383 
384         /**
385          * A list of Maven dependencies made available. Note that this map is transitively closed and
386          * pinned to a specific version of each artifact.
387          * <p>
388          * The key is the Maven coordinate string, less the version
389          * {@code group:artifact[:optional classifier][:optional packaging]}.
390          * <p>
391          * The value contains the version of the artifact.
392          */
393         @JsonProperty("artifacts")
394         private Map<String, Artifactv2> artifacts;
395 
396         /**
397          * A sentinel value placed in the file to indicate that it is an auto-generated pinned maven
398          * install file.
399          */
400         @JsonProperty("__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY")
401         private String autogeneratedSentinel;
402 
403         /**
404          * Returns artifacts.
405          *
406          * @return artifacts
407          */
408         public Map<String, Artifactv2> getArtifacts() {
409             return artifacts;
410         }
411 
412         /**
413          * Returns version.
414          *
415          * @return version
416          */
417         public String getVersion() {
418             return version;
419         }
420 
421         /**
422          * Returns autogeneratedSentinel.
423          *
424          * @return autogeneratedSentinel
425          */
426         public String getAutogeneratedSentinel() {
427             return autogeneratedSentinel;
428         }
429     }
430     private static class Artifactv2 {
431 
432           /**
433           * The version of the artifact.
434           */
435           @JsonProperty("version")
436           private String version;
437 
438           /**
439           * Returns the value of version.
440           *
441           * @return the value of version
442           */
443           public String getVersion() {
444               return version;
445           }
446     }
447 
448 
449 }