PoetryAnalyzer.java

  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) 2019 Nima Yahyazadeh. All Rights Reserved.
  17.  */
  18. package org.owasp.dependencycheck.analyzer;

  19. import com.github.packageurl.MalformedPackageURLException;
  20. import com.github.packageurl.PackageURL;
  21. import com.github.packageurl.PackageURLBuilder;
  22. import java.io.FileFilter;
  23. import java.util.List;

  24. import javax.annotation.concurrent.ThreadSafe;

  25. import org.owasp.dependencycheck.Engine;
  26. import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
  27. import org.owasp.dependencycheck.dependency.Confidence;
  28. import org.owasp.dependencycheck.dependency.Dependency;
  29. import org.owasp.dependencycheck.dependency.EvidenceType;
  30. import org.owasp.dependencycheck.exception.InitializationException;
  31. import org.owasp.dependencycheck.utils.FileFilterBuilder;
  32. import org.owasp.dependencycheck.utils.Settings;

  33. import com.moandjiezana.toml.Toml;
  34. import java.io.File;
  35. import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
  36. import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
  37. import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
  38. import org.owasp.dependencycheck.utils.Checksum;
  39. import org.slf4j.Logger;
  40. import org.slf4j.LoggerFactory;

  41. /**
  42.  * Poetry dependency analyzer.
  43.  *
  44.  * @author Ferdinand Niedermann
  45.  * @author Jeremy Long
  46.  */
  47. @ThreadSafe
  48. @Experimental
  49. public class PoetryAnalyzer extends AbstractFileTypeAnalyzer {

  50.     /**
  51.      * The logger.
  52.      */
  53.     private static final Logger LOGGER = LoggerFactory.getLogger(PoetryAnalyzer.class);

  54.     /**
  55.      * A descriptor for the type of dependencies processed or added by this
  56.      * analyzer.
  57.      */
  58.     public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.PYTHON;

  59.     /**
  60.      * Lock file name.
  61.      */
  62.     private static final String POETRY_LOCK = "poetry.lock";
  63.     /**
  64.      * Poetry project file.
  65.      */
  66.     private static final String PYPROJECT_TOML = "pyproject.toml";
  67.     /**
  68.      * The file filter for poetry.lock
  69.      */
  70.     private static final FileFilter POETRY_LOCK_FILTER = FileFilterBuilder.newInstance()
  71.             .addFilenames(POETRY_LOCK, PYPROJECT_TOML)
  72.             .build();

  73.     /**
  74.      * Returns the name of the Poetry Analyzer.
  75.      *
  76.      * @return the name of the analyzer
  77.      */
  78.     @Override
  79.     public String getName() {
  80.         return "Poetry Analyzer";
  81.     }

  82.     /**
  83.      * Tell that we are used for information collection.
  84.      *
  85.      * @return INFORMATION_COLLECTION
  86.      */
  87.     @Override
  88.     public AnalysisPhase getAnalysisPhase() {
  89.         return AnalysisPhase.INFORMATION_COLLECTION;
  90.     }

  91.     /**
  92.      * Returns the key name for the analyzers enabled setting.
  93.      *
  94.      * @return the key name for the analyzers enabled setting
  95.      */
  96.     @Override
  97.     protected String getAnalyzerEnabledSettingKey() {
  98.         return Settings.KEYS.ANALYZER_POETRY_ENABLED;
  99.     }

  100.     /**
  101.      * Returns the FileFilter
  102.      *
  103.      * @return the FileFilter
  104.      */
  105.     @Override
  106.     protected FileFilter getFileFilter() {
  107.         return POETRY_LOCK_FILTER;
  108.     }

  109.     /**
  110.      * No-op initializer implementation.
  111.      *
  112.      * @param engine a reference to the dependency-check engine
  113.      *
  114.      * @throws InitializationException never thrown
  115.      */
  116.     @Override
  117.     protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
  118.         // Nothing to do here.
  119.     }

  120.     /**
  121.      * Analyzes poetry packages and adds evidence to the dependency.
  122.      *
  123.      * @param dependency the dependency being analyzed
  124.      * @param engine the engine being used to perform the scan
  125.      *
  126.      * @throws AnalysisException thrown if there is an unrecoverable error
  127.      * analyzing the dependency
  128.      */
  129.     @Override
  130.     protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
  131.         LOGGER.debug("Checking file {}", dependency.getActualFilePath());

  132.         //do not report on the build file itself
  133.         engine.removeDependency(dependency);

  134.         final Toml result = new Toml().read(dependency.getActualFile());
  135.         if (PYPROJECT_TOML.equals(dependency.getActualFile().getName())) {
  136.             if (result.getTable("tool.poetry") == null) {
  137.                 LOGGER.debug("skipping {} as it does not contain `tool.poetry`", dependency.getDisplayFileName());
  138.                 return;
  139.             }

  140.             final File parentPath = dependency.getActualFile().getParentFile();
  141.             ensureLock(parentPath);
  142.             //exit as we can't analyze pyproject.toml - insufficient version information
  143.             return;
  144.         }

  145.         final List<Toml> projectsLocks = result.getTables("package");
  146.         if (projectsLocks == null) {
  147.             return;
  148.         }
  149.         projectsLocks.forEach((project) -> {
  150.             final String name = project.getString("name");
  151.             final String version = project.getString("version");

  152.             LOGGER.debug(String.format("package, version: %s %s", name, version));

  153.             final Dependency d = new Dependency(dependency.getActualFile(), true);
  154.             d.setName(name);
  155.             d.setVersion(version);

  156.             try {
  157.                 final PackageURL purl = PackageURLBuilder.aPackageURL()
  158.                         .withType("pypi")
  159.                         .withName(name)
  160.                         .withVersion(version)
  161.                         .build();
  162.                 d.addSoftwareIdentifier(new PurlIdentifier(purl, Confidence.HIGHEST));
  163.             } catch (MalformedPackageURLException ex) {
  164.                 LOGGER.debug("Unable to build package url for pypi", ex);
  165.                 d.addSoftwareIdentifier(new GenericIdentifier("pypi:" + name + "@" + version, Confidence.HIGH));
  166.             }

  167.             d.setPackagePath(String.format("%s:%s", name, version));
  168.             d.setEcosystem(PythonDistributionAnalyzer.DEPENDENCY_ECOSYSTEM);
  169.             final String filePath = String.format("%s:%s/%s", dependency.getFilePath(), name, version);
  170.             d.setFilePath(filePath);
  171.             d.setSha1sum(Checksum.getSHA1Checksum(filePath));
  172.             d.setSha256sum(Checksum.getSHA256Checksum(filePath));
  173.             d.setMd5sum(Checksum.getMD5Checksum(filePath));
  174.             d.addEvidence(EvidenceType.PRODUCT, POETRY_LOCK, "product", name, Confidence.HIGHEST);
  175.             d.addEvidence(EvidenceType.VERSION, POETRY_LOCK, "version", version, Confidence.HIGHEST);
  176.             d.addEvidence(EvidenceType.VENDOR, POETRY_LOCK, "vendor", name, Confidence.HIGHEST);
  177.             engine.addDependency(d);
  178.         });
  179.     }

  180.     private void ensureLock(File parent) throws AnalysisException {
  181.         final File lock = new File(parent, POETRY_LOCK);
  182.         final File requirements = new File(parent, "requirements.txt");
  183.         final boolean found = lock.isFile() || requirements.isFile();
  184.         //do not throw an error if the pyproject.toml is in the node_modules (#5464).
  185.         if (!found && !parent.toString().contains("node_modules")) {
  186.             throw new AnalysisException("Python `pyproject.toml` found and there "
  187.                     + "is not a `poetry.lock` or `requirements.txt` - analysis will be incomplete");
  188.         }
  189.     }
  190. }