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
90       * dependency version. The version can be a commit ref, so we can't assume
91       * it's a number
92       *
93       * Example values:
94       * <ul>
95       * <li>binary "https://dl.google.com/geosdk/GoogleMaps.json" "7.2.0"</li>
96       * <li>git "https://gitlab.matrix.org/matrix-org/olm.git" "3.2.16"</li>
97       * <li>github "alinradut/SwiftEntryKit"
98       * "95f4a08f41ddcf2c02e2b22789038774c8c94df5"</li>
99       * <li>github "CocoaLumberjack/CocoaLumberjack" "3.8.5"</li>
100      * <li>github "realm/realm-swift" "v10.44.0"</li>
101      * </ul>
102      */
103     private static final Pattern CARTFILE_RESOLVED_DEPENDENCY_PATTERN = Pattern.compile("(github|git|binary) \"([^\"]+)\" \"([^\"]+)\"");
104 
105     /**
106      * The capture group #1 is the actual numerical version.
107      */
108     private static final Pattern CARTFILE_VERSION_PATTERN = Pattern.compile("^v?(\\d+(\\.\\d+){0,4})$");
109 
110     /**
111      * Capture group #1 is the dependency name.
112      *
113      * Example values:
114      * <ul>
115      * <li>robbiehanson/XMPPFramework</li>
116      * <li>CocoaLumberjack/CocoaLumberjack</li>
117      * </ul>
118      */
119     private static final Pattern CARTFILE_RESOLVED_GITHUB_DEPENDENCY = Pattern.compile("[a-zA-Z0-9-_]+/([a-zA-Z0-9\\-_\\.]+)");
120 
121     /**
122      * Capture group #1 is the dependency name.
123      */
124     private static final Pattern CARTFILE_RESOLVED_GIT_DEPENDENCY = Pattern.compile(".*?/([a-zA-Z0-9\\-_\\.]+).git");
125 
126     /**
127      * Capture group #1 is the dependency name.
128      *
129      * Example values:
130      * <ul>
131      * <li>https://my.domain.com/release/MyFramework.json</li>
132      * <li>file:///some/Path/MyFramework.json -
133      * relative/path/MyFramework.json</li>
134      * <li>/absolute/path/MyFramework.json</li>
135      * </ul>
136      */
137     private static final Pattern CARTFILE_RESOLVED_BINARY_DEPENDENCY = Pattern.compile("([a-zA-Z0-9\\-_\\.]+).json");
138 
139     /**
140      * Returns the FileFilter
141      *
142      * @return the FileFilter
143      */
144     @Override
145     protected FileFilter getFileFilter() {
146         return CARTHAGE_FILTER;
147     }
148 
149     @Override
150     protected void prepareFileTypeAnalyzer(Engine engine) {
151         // NO-OP
152     }
153 
154     /**
155      * Returns the name of the analyzer.
156      *
157      * @return the name of the analyzer.
158      */
159     @Override
160     public String getName() {
161         return ANALYZER_NAME;
162     }
163 
164     /**
165      * Returns the phase that the analyzer is intended to run in.
166      *
167      * @return the phase that the analyzer is intended to run in.
168      */
169     @Override
170     public AnalysisPhase getAnalysisPhase() {
171         return ANALYSIS_PHASE;
172     }
173 
174     /**
175      * Returns the key used in the properties file to reference the analyzer's
176      * enabled property.
177      *
178      * @return the analyzer's enabled property setting key
179      */
180     @Override
181     protected String getAnalyzerEnabledSettingKey() {
182         return Settings.KEYS.ANALYZER_CARTHAGE_ENABLED;
183     }
184 
185     @Override
186     protected void analyzeDependency(Dependency dependency, Engine engine)
187             throws AnalysisException {
188         if (CARTFILE_RESOLVED.equals(dependency.getFileName())) {
189             analyzeCartfileResolvedDependency(dependency, engine);
190         }
191     }
192 
193     /**
194      * Analyzes the Cartfile.resolved and adds the evidence to the dependency.
195      *
196      * @param cartfileResolved the dependency
197      * @param engine a reference to the dependency-check engine
198      * @throws AnalysisException thrown if there is an error analyzing the
199      * Cartfile
200      */
201     private void analyzeCartfileResolvedDependency(Dependency cartfileResolved, Engine engine)
202             throws AnalysisException {
203         engine.removeDependency(cartfileResolved);
204 
205         final String contents;
206         try {
207             contents = new String(Files.readAllBytes(cartfileResolved.getActualFile().toPath()), StandardCharsets.UTF_8);
208         } catch (IOException e) {
209             throw new AnalysisException(
210                     "Problem occurred while reading dependency file.", e);
211         }
212 
213         final Matcher matcher = CARTFILE_RESOLVED_DEPENDENCY_PATTERN.matcher(contents);
214         while (matcher.find()) {
215             final String type = matcher.group(1);
216             String name = matcher.group(2);
217             String version = matcher.group(3);
218 
219             final Matcher versionMatcher = CARTFILE_VERSION_PATTERN.matcher(version);
220             if (versionMatcher.find()) {
221                 version = versionMatcher.group(1);
222             } else {
223                 // this is probably a git commit reference, so we'll default to 0.0.0.
224                 // this will probably bubble up a ton of CVEs, but serves you right for
225                 // not using semantic versioning.
226                 version = "0.0.0";
227             }
228 
229             if (type.contentEquals("git")) {
230                 final Matcher nameMatcher = CARTFILE_RESOLVED_GIT_DEPENDENCY.matcher(name);
231                 if (!nameMatcher.find()) {
232                     continue;
233                 }
234                 name = nameMatcher.group(1);
235             } else if (type.contentEquals("github")) {
236                 final Matcher nameMatcher = CARTFILE_RESOLVED_GITHUB_DEPENDENCY.matcher(name);
237                 if (!nameMatcher.find()) {
238                     continue;
239                 }
240                 name = nameMatcher.group(1);
241             } else if (type.contentEquals("binary")) {
242                 final Matcher nameMatcher = CARTFILE_RESOLVED_BINARY_DEPENDENCY.matcher(name);
243                 if (!nameMatcher.find()) {
244                     continue;
245                 }
246                 name = nameMatcher.group(1);
247             }
248 
249             final Dependency dependency = new Dependency(cartfileResolved.getActualFile(), true);
250             dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
251             dependency.setName(name);
252             dependency.setVersion(version);
253 
254             try {
255                 final PackageURLBuilder builder = PackageURLBuilder.aPackageURL().withType("carthage").withName(dependency.getName());
256                 if (dependency.getVersion() != null) {
257                     builder.withVersion(dependency.getVersion());
258                 }
259                 final PackageURL purl = builder.build();
260                 dependency.addSoftwareIdentifier(new PurlIdentifier(purl, Confidence.HIGHEST));
261             } catch (MalformedPackageURLException ex) {
262                 LOGGER.debug("Unable to build package url for carthage", ex);
263                 final GenericIdentifier id;
264                 if (dependency.getVersion() != null) {
265                     id = new GenericIdentifier("carthage:" + dependency.getName() + "@" + dependency.getVersion(), Confidence.HIGHEST);
266                 } else {
267                     id = new GenericIdentifier("carthage:" + dependency.getName(), Confidence.HIGHEST);
268                 }
269                 dependency.addSoftwareIdentifier(id);
270             }
271 
272             final String packagePath = String.format("%s:%s", name, version);
273             dependency.setPackagePath(packagePath);
274             dependency.setDisplayFileName(packagePath);
275             dependency.setSha1sum(Checksum.getSHA1Checksum(packagePath));
276             dependency.setSha256sum(Checksum.getSHA256Checksum(packagePath));
277             dependency.setMd5sum(Checksum.getMD5Checksum(packagePath));
278             dependency.addEvidence(EvidenceType.VENDOR, CARTFILE_RESOLVED, "name", name, Confidence.HIGHEST);
279             dependency.addEvidence(EvidenceType.PRODUCT, CARTFILE_RESOLVED, "name", name, Confidence.HIGHEST);
280             dependency.addEvidence(EvidenceType.VERSION, CARTFILE_RESOLVED, "version", version, Confidence.HIGHEST);
281             engine.addDependency(dependency);
282         }
283     }
284 
285     /**
286      * Sets the package path on the given dependency.
287      *
288      * @param dep the dependency to update
289      */
290     private void setPackagePath(Dependency dep) {
291         final File file = new File(dep.getFilePath());
292         final String parent = file.getParent();
293         if (parent != null) {
294             dep.setPackagePath(parent);
295         }
296     }
297 }