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) 2022 Marc Rödder. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.analyzer;
19  
20  import com.fasterxml.jackson.databind.JsonNode;
21  import com.fasterxml.jackson.databind.ObjectMapper;
22  import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
23  import com.github.packageurl.MalformedPackageURLException;
24  import com.github.packageurl.PackageURL;
25  import com.github.packageurl.PackageURLBuilder;
26  import org.owasp.dependencycheck.Engine;
27  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
28  import org.owasp.dependencycheck.dependency.Confidence;
29  import org.owasp.dependencycheck.dependency.Dependency;
30  import org.owasp.dependencycheck.dependency.EvidenceType;
31  import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
32  import org.owasp.dependencycheck.utils.Checksum;
33  import org.owasp.dependencycheck.utils.FileFilterBuilder;
34  import org.owasp.dependencycheck.utils.Settings;
35  import org.slf4j.Logger;
36  import org.slf4j.LoggerFactory;
37  
38  import javax.annotation.concurrent.ThreadSafe;
39  import java.io.File;
40  import java.io.FileFilter;
41  import java.io.IOException;
42  import java.util.Iterator;
43  import java.util.Map;
44  
45  /**
46   * This analyzer is used to analyze Dart packages by collecting information from
47   * pubspec lock and yaml files. See https://dart.dev/tools/pub/pubspec
48   *
49   * @author Marc Rödder (http://github.com/sticksen)
50   */
51  @Experimental
52  @ThreadSafe
53  public class DartAnalyzer extends AbstractFileTypeAnalyzer {
54  
55  //    /**
56  //     * A descriptor for the type of dependencies processed or added by this
57  //     * analyzer.
58  //     */
59  //    // currently not supported by Sonatype OSS
60  //    public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.DART;
61      /**
62       * The logger.
63       */
64      private static final Logger LOGGER = LoggerFactory.getLogger(DartAnalyzer.class);
65  
66      /**
67       * The lock file name.
68       */
69      private static final String LOCK_FILE = "pubspec.lock";
70      /**
71       * The YAML file name.
72       */
73      private static final String YAML_FILE = "pubspec.yaml";
74  
75      @Override
76      protected FileFilter getFileFilter() {
77          return FileFilterBuilder.newInstance().addFilenames(LOCK_FILE, YAML_FILE).build();
78      }
79  
80      @Override
81      protected void prepareFileTypeAnalyzer(Engine engine) {
82          // NO-OP
83      }
84  
85      @Override
86      public String getName() {
87          return "Dart Package Analyzer";
88      }
89  
90      /**
91       * @return the phase that the analyzer is intended to run in.
92       */
93      @Override
94      public AnalysisPhase getAnalysisPhase() {
95          return AnalysisPhase.INFORMATION_COLLECTION;
96      }
97  
98      /**
99       * Returns the key used in the properties file to reference the analyzer's
100      * enabled property.
101      *
102      * @return the analyzer's enabled property setting key
103      */
104     @Override
105     protected String getAnalyzerEnabledSettingKey() {
106         return Settings.KEYS.ANALYZER_DART_ENABLED;
107     }
108 
109     @Override
110     protected void analyzeDependency(Dependency dependency, Engine engine)
111             throws AnalysisException {
112         final String fileName = dependency.getFileName();
113         LOGGER.debug("Checking file {}", fileName);
114 
115         switch (fileName) {
116             case LOCK_FILE:
117                 analyzeLockFileDependencies(dependency, engine);
118                 break;
119             case YAML_FILE:
120                 analyzeYamlFileDependencies(dependency, engine);
121                 break;
122             default:
123             //do nothing
124         }
125     }
126 
127     private void analyzeYamlFileDependencies(Dependency yamlFileDependency, Engine engine) throws AnalysisException {
128         engine.removeDependency(yamlFileDependency);
129         final File yamlFile = yamlFileDependency.getActualFile();
130         if (YAML_FILE.equals(yamlFile.getName())) {
131             final File lock = new File(yamlFile.getParentFile(), LOCK_FILE);
132             if (lock.isFile()) {
133                 LOGGER.debug("Skipping {} because {} exists", yamlFile, lock);
134                 return;
135             }
136         }
137 
138         final JsonNode rootNode;
139         final ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
140         try {
141             rootNode = objectMapper.readTree(yamlFile);
142         } catch (IOException e) {
143             throw new AnalysisException("Problem occurred while reading dependency file.", e);
144         }
145 
146         if (rootNode.hasNonNull("dependencies")) {
147             final Iterator<Map.Entry<String, JsonNode>> dependencies = rootNode.get("dependencies").fields();
148             addYamlDependenciesToEngine(dependencies, yamlFile, engine);
149         }
150         //TODO - add configuration to allow skipping dev dependencies.
151         if (rootNode.hasNonNull("dev_dependencies")) {
152             final Iterator<Map.Entry<String, JsonNode>> devDependencies = rootNode.get("dev_dependencies").fields();
153             addYamlDependenciesToEngine(devDependencies, yamlFile, engine);
154         }
155 
156         addYamlDartDependencyToEngine(rootNode, yamlFile, engine);
157     }
158 
159     private void analyzeLockFileDependencies(Dependency lockFileDependency, Engine engine) throws AnalysisException {
160         engine.removeDependency(lockFileDependency);
161 
162         final JsonNode rootNode;
163         final File lockFile = lockFileDependency.getActualFile();
164         final ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
165         try {
166             rootNode = objectMapper.readTree(lockFile);
167         } catch (IOException e) {
168             throw new AnalysisException("Problem occurred while reading dependency lockFile.", e);
169         }
170 
171         addLockFileDependenciesToEngine(lockFile, engine, rootNode);
172         addLockFileDartVersionToEngine(lockFile, engine, rootNode);
173     }
174 
175     private void addLockFileDartVersionToEngine(File file, Engine engine, JsonNode rootNode) throws AnalysisException {
176         final String dartVersion = rootNode.get("sdks").get("dart").textValue();
177         final String minimumVersion = extractMinimumVersion(dartVersion);
178 
179         engine.addDependency(
180                 createDependencyFromNameAndVersion(file, "dart_software_development_kit", minimumVersion));
181     }
182 
183     private void addLockFileDependenciesToEngine(File file, Engine engine, JsonNode rootNode) throws AnalysisException {
184         for (JsonNode nextPackage : rootNode.get("packages")) {
185             final JsonNode description = nextPackage.get("description");
186             if (description == null) {
187                 continue;
188             }
189 
190             final JsonNode nameNode = description.get("name");
191             if (nameNode == null) {
192                 continue;
193             }
194 
195             final JsonNode versionNode = nextPackage.get("version");
196             if (versionNode == null) {
197                 continue;
198             }
199 
200             final String name = nameNode.asText();
201             final String version = versionNode.asText();
202             LOGGER.debug("Found dependency in {} file, name: {}, version: {}", LOCK_FILE, name, version);
203 
204             engine.addDependency(
205                     createDependencyFromNameAndVersion(file, name, version)
206             );
207         }
208     }
209 
210     private void addYamlDependenciesToEngine(Iterator<Map.Entry<String, JsonNode>> dependencies, File file, Engine engine) throws AnalysisException {
211         while (dependencies.hasNext()) {
212             final Map.Entry<String, JsonNode> entry = dependencies.next();
213 
214             final String name = entry.getKey();
215             final String versionRaw = entry.getValue().asText();
216             final String version = extractMinimumVersion(versionRaw);
217             LOGGER.debug("Found dependency in {} file, name: {}, version: {}", YAML_FILE, name, version);
218 
219             engine.addDependency(
220                     createDependencyFromNameAndVersion(file, name, version)
221             );
222         }
223     }
224 
225     private void addYamlDartDependencyToEngine(JsonNode rootNode, File file, Engine engine) throws AnalysisException {
226         if (rootNode.hasNonNull("environment") && rootNode.get("environment").hasNonNull("sdk")) {
227             final String dartVersion = rootNode.get("environment").get("sdk").textValue();
228             final String minimumVersion = extractMinimumVersion(dartVersion);
229 
230             engine.addDependency(
231                     createDependencyFromNameAndVersion(file, "dart_software_development_kit", minimumVersion));
232         }
233     }
234 
235     private Dependency createDependencyFromNameAndVersion(File file, String name, String version) throws AnalysisException {
236         final Dependency dependency = new Dependency(file, true);
237 
238         // currently not supported by Sonatype OSS
239 //            // dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
240         dependency.setName(name);
241         dependency.setVersion(version);
242 
243         final PackageURL packageURL;
244         try {
245             packageURL = PackageURLBuilder
246                     .aPackageURL()
247                     .withType("pub")
248                     .withName(dependency.getName())
249                     .withVersion(version.isEmpty() ? null : version)
250                     .build();
251 
252         } catch (MalformedPackageURLException e) {
253             throw new AnalysisException("Problem occurred while reading dependency file.", e);
254         }
255 
256         dependency.addSoftwareIdentifier(new PurlIdentifier(packageURL, Confidence.HIGHEST));
257 
258         dependency.addEvidence(EvidenceType.PRODUCT, file.getName(), "name", name, Confidence.HIGHEST);
259         dependency.addEvidence(EvidenceType.VENDOR, file.getName(), "name", name, Confidence.HIGHEST);
260         dependency.addEvidence(EvidenceType.VENDOR, file.getName(), "name", "dart", Confidence.HIGHEST);
261         if (!version.isEmpty()) {
262             dependency.addEvidence(EvidenceType.VERSION, file.getName(), "version", version, Confidence.MEDIUM);
263         }
264 
265         final String packagePath = String.format("%s:%s", name, version);
266         dependency.setSha1sum(Checksum.getSHA1Checksum(packagePath));
267         dependency.setSha256sum(Checksum.getSHA256Checksum(packagePath));
268         dependency.setMd5sum(Checksum.getMD5Checksum(packagePath));
269         dependency.setPackagePath(packagePath);
270         dependency.setDisplayFileName(packagePath);
271 
272         return dependency;
273     }
274 
275     private String extractMinimumVersion(String versionRaw) {
276         final String version;
277         if (versionRaw.contains("^")) {
278             version = versionRaw.replace("^", "");
279         } else if (versionRaw.contains("<")) {
280             // this code should parse version definitions like ">=2.10.0 <3.0.0"
281             final String firstPart = versionRaw.split("<")[0].trim();
282             version = firstPart.replace(">=", "").trim();
283         } else if (versionRaw.contains("any") || "null".equals(versionRaw)) {
284             version = "";
285         } else {
286             version = versionRaw;
287         }
288 
289         LOGGER.debug("Extracted minimum version: {} from raw version: {}", version, versionRaw);
290         return version;
291     }
292 }