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  package org.owasp.dependencycheck.analyzer;
17  
18  import com.github.packageurl.MalformedPackageURLException;
19  import com.github.packageurl.PackageURL;
20  import com.github.packageurl.PackageURLBuilder;
21  import java.io.FileFilter;
22  import java.util.List;
23  
24  import javax.annotation.concurrent.ThreadSafe;
25  
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.exception.InitializationException;
32  import org.owasp.dependencycheck.utils.FileFilterBuilder;
33  import org.owasp.dependencycheck.utils.Settings;
34  
35  import com.moandjiezana.toml.Toml;
36  import java.io.File;
37  import java.util.Optional;
38  
39  import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
40  import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
41  import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
42  import org.owasp.dependencycheck.utils.Checksum;
43  import org.slf4j.Logger;
44  import org.slf4j.LoggerFactory;
45  
46  /**
47   * Poetry dependency analyzer.
48   *
49   * @author Ferdinand Niedermann
50   * @author Jeremy Long
51   */
52  @ThreadSafe
53  @Experimental
54  public class PoetryAnalyzer extends AbstractFileTypeAnalyzer {
55  
56      /**
57       * The logger.
58       */
59      private static final Logger LOGGER = LoggerFactory.getLogger(PoetryAnalyzer.class);
60  
61      /**
62       * A descriptor for the type of dependencies processed or added by this
63       * analyzer.
64       */
65      public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.PYTHON;
66  
67      /**
68       * Lock file name.
69       */
70      private static final String POETRY_LOCK = "poetry.lock";
71      /**
72       * Poetry project file.
73       */
74      private static final String PYPROJECT_TOML = "pyproject.toml";
75      /**
76       * The file filter for poetry.lock
77       */
78      private static final FileFilter POETRY_LOCK_FILTER = FileFilterBuilder.newInstance()
79              .addFilenames(POETRY_LOCK, PYPROJECT_TOML)
80              .build();
81  
82      /**
83       * Returns the name of the Poetry Analyzer.
84       *
85       * @return the name of the analyzer
86       */
87      @Override
88      public String getName() {
89          return "Poetry Analyzer";
90      }
91  
92      /**
93       * Tell that we are used for information collection.
94       *
95       * @return INFORMATION_COLLECTION
96       */
97      @Override
98      public AnalysisPhase getAnalysisPhase() {
99          return AnalysisPhase.INFORMATION_COLLECTION;
100     }
101 
102     /**
103      * Returns the key name for the analyzers enabled setting.
104      *
105      * @return the key name for the analyzers enabled setting
106      */
107     @Override
108     protected String getAnalyzerEnabledSettingKey() {
109         return Settings.KEYS.ANALYZER_POETRY_ENABLED;
110     }
111 
112     /**
113      * Returns the FileFilter
114      *
115      * @return the FileFilter
116      */
117     @Override
118     protected FileFilter getFileFilter() {
119         return POETRY_LOCK_FILTER;
120     }
121 
122     /**
123      * No-op initializer implementation.
124      *
125      * @param engine a reference to the dependency-check engine
126      *
127      * @throws InitializationException never thrown
128      */
129     @Override
130     protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
131         // Nothing to do here.
132     }
133 
134     /**
135      * Analyzes poetry packages and adds evidence to the dependency.
136      *
137      * @param dependency the dependency being analyzed
138      * @param engine the engine being used to perform the scan
139      *
140      * @throws AnalysisException thrown if there is an unrecoverable error
141      * analyzing the dependency
142      */
143     @Override
144     protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
145         LOGGER.debug("Checking file {}", dependency.getActualFilePath());
146 
147         //do not report on the build file itself
148         engine.removeDependency(dependency);
149 
150         final Optional<Toml> potentiallyParsedToml = parseDependencyFile(dependency);
151         if (potentiallyParsedToml.isEmpty()) {
152             LOGGER.warn("toml file skipped: {} could not be parsed", dependency.getActualFilePath());
153             return;
154         }
155         final Toml result = potentiallyParsedToml.get();
156         if (PYPROJECT_TOML.equals(dependency.getActualFile().getName())) {
157             if (result.getTable("tool.poetry") == null) {
158                 LOGGER.debug("skipping {} as it does not contain `tool.poetry`", dependency.getDisplayFileName());
159                 return;
160             }
161 
162             final File parentPath = dependency.getActualFile().getParentFile();
163             ensureLock(parentPath);
164             //exit as we can't analyze pyproject.toml - insufficient version information
165             return;
166         }
167 
168         final List<Toml> projectsLocks = result.getTables("package");
169         if (projectsLocks == null) {
170             return;
171         }
172         projectsLocks.forEach((project) -> {
173             final String name = project.getString("name");
174             final String version = project.getString("version");
175 
176             LOGGER.debug(String.format("package, version: %s %s", name, version));
177 
178             final Dependency d = new Dependency(dependency.getActualFile(), true);
179             d.setName(name);
180             d.setVersion(version);
181 
182             try {
183                 final PackageURL purl = PackageURLBuilder.aPackageURL()
184                         .withType("pypi")
185                         .withName(name)
186                         .withVersion(version)
187                         .build();
188                 d.addSoftwareIdentifier(new PurlIdentifier(purl, Confidence.HIGHEST));
189             } catch (MalformedPackageURLException ex) {
190                 LOGGER.debug("Unable to build package url for pypi", ex);
191                 d.addSoftwareIdentifier(new GenericIdentifier("pypi:" + name + "@" + version, Confidence.HIGH));
192             }
193 
194             d.setPackagePath(String.format("%s:%s", name, version));
195             d.setEcosystem(PythonDistributionAnalyzer.DEPENDENCY_ECOSYSTEM);
196             final String filePath = String.format("%s:%s/%s", dependency.getFilePath(), name, version);
197             d.setFilePath(filePath);
198             d.setSha1sum(Checksum.getSHA1Checksum(filePath));
199             d.setSha256sum(Checksum.getSHA256Checksum(filePath));
200             d.setMd5sum(Checksum.getMD5Checksum(filePath));
201             d.addEvidence(EvidenceType.PRODUCT, POETRY_LOCK, "product", name, Confidence.HIGHEST);
202             d.addEvidence(EvidenceType.VERSION, POETRY_LOCK, "version", version, Confidence.HIGHEST);
203             d.addEvidence(EvidenceType.VENDOR, POETRY_LOCK, "vendor", name, Confidence.HIGHEST);
204             engine.addDependency(d);
205         });
206     }
207 
208     private Optional<Toml> parseDependencyFile(Dependency dependency) {
209         try {
210             final Toml toml = new Toml().read(dependency.getActualFile());
211             return Optional.of(toml);
212         } catch (RuntimeException e) {
213             final Optional<String> unparsableFileErrorMessage = Optional.ofNullable(e.getCause())
214                     .filter(c -> c instanceof IllegalStateException)
215                     .map(Throwable::getMessage)
216                     .filter(PoetryAnalyzer::isInvalidKeyErrorMessage);
217 
218             if (unparsableFileErrorMessage.isPresent()) {
219                 final String message = String.format("Invalid toml file, cannot parse '%s'", dependency.getActualFile());
220                 LOGGER.debug(message, e);
221                 return Optional.empty();
222             }
223 
224             throw e;
225         }
226     }
227 
228     private static boolean isInvalidKeyErrorMessage(String m) {
229         return m.startsWith("Invalid key");
230     }
231 
232     private void ensureLock(File parent) throws AnalysisException {
233         final File lock = new File(parent, POETRY_LOCK);
234         final File requirements = new File(parent, "requirements.txt");
235         final boolean found = lock.isFile() || requirements.isFile();
236         //do not throw an error if the pyproject.toml is in the node_modules (#5464).
237         if (!found && !parent.toString().contains("node_modules")) {
238             throw new AnalysisException("Python `pyproject.toml` found and there "
239                     + "is not a `poetry.lock` or `requirements.txt` - analysis will be incomplete");
240         }
241     }
242 }