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) 2023 The OWASP Foundation. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.analyzer;
19  
20  import com.github.packageurl.MalformedPackageURLException;
21  import com.github.packageurl.PackageURL;
22  import com.github.packageurl.PackageURLBuilder;
23  import org.owasp.dependencycheck.Engine;
24  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
25  import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
26  import org.owasp.dependencycheck.dependency.Confidence;
27  import org.owasp.dependencycheck.dependency.Dependency;
28  import org.owasp.dependencycheck.dependency.EvidenceType;
29  import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
30  import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
31  import org.owasp.dependencycheck.utils.Checksum;
32  import org.owasp.dependencycheck.utils.FileFilterBuilder;
33  import org.owasp.dependencycheck.utils.Settings;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  import javax.annotation.concurrent.ThreadSafe;
38  import java.io.File;
39  import java.io.FileFilter;
40  import java.io.IOException;
41  import java.nio.charset.StandardCharsets;
42  import java.nio.file.Files;
43  import java.util.regex.Matcher;
44  import java.util.regex.Pattern;
45  
46  /**
47   * This analyzer is used to analyze SWIFT and Objective-C packages by collecting
48   * information from Cartfile files. Carthage dependency manager see
49   * https://github.com/Carthage/Carthage.
50   *
51   * Based on CocoaPodsAnalyzer by Bianca Jiang.
52   *
53   * @author Alin Radut (https://github.com/alinradut)
54   */
55  @Experimental
56  @ThreadSafe
57  public class CarthageAnalyzer extends AbstractFileTypeAnalyzer {
58  
59      /**
60       * A descriptor for the type of dependencies processed or added by this
61       * analyzer.
62       */
63      public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.IOS;
64  
65      /**
66       * The logger.
67       */
68      private static final Logger LOGGER = LoggerFactory.getLogger(CarthageAnalyzer.class);
69      /**
70       * The name of the analyzer.
71       */
72      private static final String ANALYZER_NAME = "Carthage Package Analyzer";
73  
74      /**
75       * The phase that this analyzer is intended to run in.
76       */
77      private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
78  
79      /**
80       * The file name to scan.
81       */
82      public static final String CARTFILE_RESOLVED = "Cartfile.resolved";
83      /**
84       * Filter that detects files named "Cartfile.resolved".
85       */
86      private static final FileFilter CARTHAGE_FILTER = FileFilterBuilder.newInstance().addFilenames(CARTFILE_RESOLVED).build();
87  
88      /**
89       * The capture group #1 is the dependency type, #2 is the name, #3 is dependency version.
90       * The version can be a commit ref, so we can't assume it's a number
91       *
92       * Example values:
93       * - binary "https://dl.google.com/geosdk/GoogleMaps.json" "7.2.0"
94       * - git "https://gitlab.matrix.org/matrix-org/olm.git" "3.2.16"
95       * - github "alinradut/SwiftEntryKit" "95f4a08f41ddcf2c02e2b22789038774c8c94df5""
96       * - github "CocoaLumberjack/CocoaLumberjack" "3.8.5"
97       * - github "realm/realm-swift" "v10.44.0"
98       */
99      private static final Pattern CARTFILE_RESOLVED_DEPENDENCY_PATTERN = Pattern.compile("(github|git|binary) \"([^\"]+)\" \"([^\"]+)\"");
100 
101     /**
102      * The capture group #1 is the actual numerical version.
103      */
104     private static final Pattern CARTFILE_VERSION_PATTERN = Pattern.compile("^v?(\\d+(\\.\\d+){0,4})$");
105 
106     /**
107      * Capture group #1 is the dependency name.
108      *
109      * Example values:
110      * - robbiehanson/XMPPFramework
111      * - CocoaLumberjack/CocoaLumberjack
112      */
113     private static final Pattern CARTFILE_RESOLVED_GITHUB_DEPENDENCY = Pattern.compile("[a-zA-Z0-9-_]+/([a-zA-Z0-9\\-_\\.]+)");
114 
115     /**
116      * Capture group #1 is the dependency name.
117      */
118     private static final Pattern CARTFILE_RESOLVED_GIT_DEPENDENCY = Pattern.compile(".*?/([a-zA-Z0-9\\-_\\.]+).git");
119 
120     /**
121      * Capture group #1 is the dependency name.
122      *
123      * Example values:
124      * - https://my.domain.com/release/MyFramework.json
125      * - file:///some/Path/MyFramework.json
126      * - relative/path/MyFramework.json
127      * - /absolute/path/MyFramework.json
128      */
129     private static final Pattern CARTFILE_RESOLVED_BINARY_DEPENDENCY = Pattern.compile("([a-zA-Z0-9\\-_\\.]+).json");
130 
131     /**
132      * Returns the FileFilter
133      *
134      * @return the FileFilter
135      */
136     @Override
137     protected FileFilter getFileFilter() {
138         return CARTHAGE_FILTER;
139     }
140 
141     @Override
142     protected void prepareFileTypeAnalyzer(Engine engine) {
143         // NO-OP
144     }
145 
146     /**
147      * Returns the name of the analyzer.
148      *
149      * @return the name of the analyzer.
150      */
151     @Override
152     public String getName() {
153         return ANALYZER_NAME;
154     }
155 
156     /**
157      * Returns the phase that the analyzer is intended to run in.
158      *
159      * @return the phase that the analyzer is intended to run in.
160      */
161     @Override
162     public AnalysisPhase getAnalysisPhase() {
163         return ANALYSIS_PHASE;
164     }
165 
166     /**
167      * Returns the key used in the properties file to reference the analyzer's
168      * enabled property.
169      *
170      * @return the analyzer's enabled property setting key
171      */
172     @Override
173     protected String getAnalyzerEnabledSettingKey() {
174         return Settings.KEYS.ANALYZER_CARTHAGE_ENABLED;
175     }
176 
177     @Override
178     protected void analyzeDependency(Dependency dependency, Engine engine)
179             throws AnalysisException {
180         if (CARTFILE_RESOLVED.equals(dependency.getFileName())) {
181             analyzeCartfileResolvedDependency(dependency, engine);
182         }
183     }
184 
185     /**
186      * Analyzes the Cartfile.resolved and adds the evidence to the dependency.
187      *
188      * @param cartfileResolved the dependency
189      * @throws AnalysisException thrown if there is an error analyzing the
190      * Cartfile
191      */
192     private void analyzeCartfileResolvedDependency(Dependency cartfileResolved, Engine engine)
193             throws AnalysisException {
194         engine.removeDependency(cartfileResolved);
195 
196         final String contents;
197         try {
198             contents = new String(Files.readAllBytes(cartfileResolved.getActualFile().toPath()), StandardCharsets.UTF_8);
199         } catch (IOException e) {
200             throw new AnalysisException(
201                     "Problem occurred while reading dependency file.", e);
202         }
203 
204         final Matcher matcher = CARTFILE_RESOLVED_DEPENDENCY_PATTERN.matcher(contents);
205         while (matcher.find()) {
206             final String type = matcher.group(1);
207             String name = matcher.group(2);
208             String version = matcher.group(3);
209 
210             final Matcher versionMatcher = CARTFILE_VERSION_PATTERN.matcher(version);
211             if (versionMatcher.find()) {
212                 version = versionMatcher.group(1);
213             }
214             else {
215                 // this is probably a git commit reference, so we'll default to 0.0.0.
216                 // this will probably bubble up a ton of CVEs, but serves you right for
217                 // not using semantic versioning.
218                 version = "0.0.0";
219             }
220 
221             if (type.contentEquals("git")) {
222                 final Matcher nameMatcher = CARTFILE_RESOLVED_GIT_DEPENDENCY.matcher(name);
223                 if (!nameMatcher.find()) {
224                     continue;
225                 }
226                 name = nameMatcher.group(1);
227             }
228             else if (type.contentEquals("github")) {
229                 final Matcher nameMatcher = CARTFILE_RESOLVED_GITHUB_DEPENDENCY.matcher(name);
230                 if (!nameMatcher.find()) {
231                     continue;
232                 }
233                 name = nameMatcher.group(1);
234             }
235             else if (type.contentEquals("binary")) {
236                 final Matcher nameMatcher = CARTFILE_RESOLVED_BINARY_DEPENDENCY.matcher(name);
237                 if (!nameMatcher.find()) {
238                     continue;
239                 }
240                 name = nameMatcher.group(1);
241             }
242 
243             final Dependency dependency = new Dependency(cartfileResolved.getActualFile(), true);
244             dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
245             dependency.setName(name);
246             dependency.setVersion(version);
247 
248             try {
249                 final PackageURLBuilder builder = PackageURLBuilder.aPackageURL().withType("carthage").withName(dependency.getName());
250                 if (dependency.getVersion() != null) {
251                     builder.withVersion(dependency.getVersion());
252                 }
253                 final PackageURL purl = builder.build();
254                 dependency.addSoftwareIdentifier(new PurlIdentifier(purl, Confidence.HIGHEST));
255             } catch (MalformedPackageURLException ex) {
256                 LOGGER.debug("Unable to build package url for carthage", ex);
257                 final GenericIdentifier id;
258                 if (dependency.getVersion() != null) {
259                     id = new GenericIdentifier("carthage:" + dependency.getName() + "@" + dependency.getVersion(), Confidence.HIGHEST);
260                 } else {
261                     id = new GenericIdentifier("carthage:" + dependency.getName(), Confidence.HIGHEST);
262                 }
263                 dependency.addSoftwareIdentifier(id);
264             }
265 
266             final String packagePath = String.format("%s:%s", name, version);
267             dependency.setPackagePath(packagePath);
268             dependency.setDisplayFileName(packagePath);
269             dependency.setSha1sum(Checksum.getSHA1Checksum(packagePath));
270             dependency.setSha256sum(Checksum.getSHA256Checksum(packagePath));
271             dependency.setMd5sum(Checksum.getMD5Checksum(packagePath));
272             dependency.addEvidence(EvidenceType.VENDOR, CARTFILE_RESOLVED, "name", name, Confidence.HIGHEST);
273             dependency.addEvidence(EvidenceType.PRODUCT, CARTFILE_RESOLVED, "name", name, Confidence.HIGHEST);
274             dependency.addEvidence(EvidenceType.VERSION, CARTFILE_RESOLVED, "version", version, Confidence.HIGHEST);
275             engine.addDependency(dependency);
276         }
277     }
278 
279     /**
280      * Sets the package path on the given dependency.
281      *
282      * @param dep the dependency to update
283      */
284     private void setPackagePath(Dependency dep) {
285         final File file = new File(dep.getFilePath());
286         final String parent = file.getParent();
287         if (parent != null) {
288             dep.setPackagePath(parent);
289         }
290     }
291 }