CocoaPodsAnalyzer.java

/*
 * This file is part of dependency-check-core.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Copyright (c) 2016 IBM Corporation. All Rights Reserved.
 */
package org.owasp.dependencycheck.analyzer;

import com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURL;
import com.github.packageurl.PackageURLBuilder;
import org.owasp.dependencycheck.Engine;
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
import org.owasp.dependencycheck.dependency.Confidence;
import org.owasp.dependencycheck.dependency.Dependency;
import org.owasp.dependencycheck.dependency.EvidenceType;
import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
import org.owasp.dependencycheck.utils.Checksum;
import org.owasp.dependencycheck.utils.FileFilterBuilder;
import org.owasp.dependencycheck.utils.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.concurrent.ThreadSafe;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This analyzer is used to analyze SWIFT and Objective-C packages by collecting
 * information from .podspec files. CocoaPods dependency manager see
 * https://cocoapods.org/.
 *
 * @author Bianca Jiang (https://twitter.com/biancajiang)
 */
@Experimental
@ThreadSafe
public class CocoaPodsAnalyzer extends AbstractFileTypeAnalyzer {

    /**
     * A descriptor for the type of dependencies processed or added by this
     * analyzer.
     */
    public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.IOS;

    /**
     * The logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(CocoaPodsAnalyzer.class);
    /**
     * The name of the analyzer.
     */
    private static final String ANALYZER_NAME = "CocoaPods Package Analyzer";

    /**
     * The phase that this analyzer is intended to run in.
     */
    private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;

    /**
     * The file name to scan.
     */
    public static final String PODSPEC = "podspec";
    /**
     * The file name to scan.
     */
    public static final String PODFILE_LOCK = "Podfile.lock";
    /**
     * Filter that detects files named "*.podspec" and "Podfile.lock".
     */
    private static final FileFilter PODS_FILTER = FileFilterBuilder.newInstance().addExtensions(PODSPEC).addFilenames(PODFILE_LOCK).build();

    /**
     * The capture group #1 is the block variable. e.g. "Pod::Spec.new do
     * |spec|"
     */
    private static final Pattern PODSPEC_BLOCK_PATTERN = Pattern.compile("Pod::Spec\\.new\\s+?do\\s+?\\|(.+?)\\|");

    /**
     * The capture group #1 is the dependency name, #2 is dependency version
     */
    private static final Pattern PODFILE_LOCK_DEPENDENCY_PATTERN = Pattern.compile("  - \"?(.*) \\((\\d+(\\.\\d+){0,4})\\)\"?");

    /**
     * Returns the FileFilter
     *
     * @return the FileFilter
     */
    @Override
    protected FileFilter getFileFilter() {
        return PODS_FILTER;
    }

    @Override
    protected void prepareFileTypeAnalyzer(Engine engine) {
        // NO-OP
    }

    /**
     * Returns the name of the analyzer.
     *
     * @return the name of the analyzer.
     */
    @Override
    public String getName() {
        return ANALYZER_NAME;
    }

    /**
     * Returns the phase that the analyzer is intended to run in.
     *
     * @return the phase that the analyzer is intended to run in.
     */
    @Override
    public AnalysisPhase getAnalysisPhase() {
        return ANALYSIS_PHASE;
    }

    /**
     * Returns the key used in the properties file to reference the analyzer's
     * enabled property.
     *
     * @return the analyzer's enabled property setting key
     */
    @Override
    protected String getAnalyzerEnabledSettingKey() {
        return Settings.KEYS.ANALYZER_COCOAPODS_ENABLED;
    }

    @Override
    protected void analyzeDependency(Dependency dependency, Engine engine)
            throws AnalysisException {
        if (PODFILE_LOCK.equals(dependency.getFileName())) {
            analyzePodfileLockDependencies(dependency, engine);
        }

        if (dependency.getFileName().endsWith(PODSPEC)) {
            analyzePodspecDependency(dependency);
        }
    }

    /**
     * Analyzes the podfile.lock file to extract evidence for the dependency.
     *
     * @param podfileLock the dependency to analyze
     * @param engine the analysis engine
     * @throws AnalysisException thrown if there is an error analyzing the
     * dependency
     */
    private void analyzePodfileLockDependencies(Dependency podfileLock, Engine engine)
            throws AnalysisException {
        engine.removeDependency(podfileLock);

        final String contents;
        try {
            contents = new String(Files.readAllBytes(podfileLock.getActualFile().toPath()), StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new AnalysisException(
                    "Problem occurred while reading dependency file.", e);
        }

        final Matcher matcher = PODFILE_LOCK_DEPENDENCY_PATTERN.matcher(contents);
        while (matcher.find()) {
            final String name = matcher.group(1);
            final String version = matcher.group(2);

            final Dependency dependency = new Dependency(podfileLock.getActualFile(), true);
            dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
            dependency.setName(name);
            dependency.setVersion(version);

            try {
                final PackageURLBuilder builder = PackageURLBuilder.aPackageURL().withType("cocoapods").withName(dependency.getName());
                if (dependency.getVersion() != null) {
                    builder.withVersion(dependency.getVersion());
                }
                final PackageURL purl = builder.build();
                dependency.addSoftwareIdentifier(new PurlIdentifier(purl, Confidence.HIGHEST));
            } catch (MalformedPackageURLException ex) {
                LOGGER.debug("Unable to build package url for cocoapods", ex);
                final GenericIdentifier id;
                if (dependency.getVersion() != null) {
                    id = new GenericIdentifier("cocoapods:" + dependency.getName() + "@" + dependency.getVersion(), Confidence.HIGHEST);
                } else {
                    id = new GenericIdentifier("cocoapods:" + dependency.getName(), Confidence.HIGHEST);
                }
                dependency.addSoftwareIdentifier(id);
            }

            final String packagePath = String.format("%s:%s", name, version);
            dependency.setPackagePath(packagePath);
            dependency.setDisplayFileName(packagePath);
            dependency.setSha1sum(Checksum.getSHA1Checksum(packagePath));
            dependency.setSha256sum(Checksum.getSHA256Checksum(packagePath));
            dependency.setMd5sum(Checksum.getMD5Checksum(packagePath));
            dependency.addEvidence(EvidenceType.VENDOR, PODFILE_LOCK, "name", name, Confidence.HIGHEST);
            dependency.addEvidence(EvidenceType.PRODUCT, PODFILE_LOCK, "name", name, Confidence.HIGHEST);
            dependency.addEvidence(EvidenceType.VERSION, PODFILE_LOCK, "version", version, Confidence.HIGHEST);
            engine.addDependency(dependency);
        }
    }

    /**
     * Analyzes the podspec and adds the evidence to the dependency.
     *
     * @param dependency the dependency
     * @throws AnalysisException thrown if there is an error analyzing the
     * podspec
     */
    private void analyzePodspecDependency(Dependency dependency)
            throws AnalysisException {
        dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
        String contents;
        try {
            contents = new String(Files.readAllBytes(dependency.getActualFile().toPath()), StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new AnalysisException(
                    "Problem occurred while reading dependency file.", e);
        }
        final Matcher matcher = PODSPEC_BLOCK_PATTERN.matcher(contents);
        if (matcher.find()) {
            contents = contents.substring(matcher.end());
            final String blockVariable = matcher.group(1);
            PackageURLBuilder builder = null;
            final String name = determineEvidence(contents, blockVariable, "name");
            if (!name.isEmpty()) {
                dependency.addEvidence(EvidenceType.PRODUCT, PODSPEC, "name_project", name, Confidence.HIGHEST);
                dependency.addEvidence(EvidenceType.VENDOR, PODSPEC, "name_project", name, Confidence.HIGHEST);
                dependency.setName(name);

                builder = PackageURLBuilder.aPackageURL();
                builder.withType("cocoapods").withName(name);
            }
            final String version = determineEvidence(contents, blockVariable, "version");
            if (!version.isEmpty()) {
                dependency.addEvidence(EvidenceType.VERSION, PODSPEC, "version", version, Confidence.HIGHEST);
                dependency.setVersion(version);
                if (builder != null) {
                    builder.withVersion(version);
                }
            }

            final String summary = determineEvidence(contents, blockVariable, "summary");
            if (!summary.isEmpty()) {
                dependency.addEvidence(EvidenceType.PRODUCT, PODSPEC, "summary", summary, Confidence.HIGHEST);
            }

            final String author = determineEvidence(contents, blockVariable, "authors?");
            if (!author.isEmpty()) {
                dependency.addEvidence(EvidenceType.VENDOR, PODSPEC, "author", author, Confidence.HIGHEST);
            }
            final String homepage = determineEvidence(contents, blockVariable, "homepage");
            if (!homepage.isEmpty()) {
                dependency.addEvidence(EvidenceType.VENDOR, PODSPEC, "homepage", homepage, Confidence.HIGHEST);
            }
            final String license = determineEvidence(contents, blockVariable, "licen[cs]es?");
            if (!license.isEmpty()) {
                dependency.setLicense(license);
            }

            if (builder != null) {
                try {
                    final PurlIdentifier purl = new PurlIdentifier(builder.build(), homepage, Confidence.HIGHEST);
                    dependency.addSoftwareIdentifier(purl);
                } catch (MalformedPackageURLException ex) {
                    LOGGER.debug("Unable to generate purl for cocoapod", ex);
                    final StringBuilder sb = new StringBuilder("pkg:cocoapods/");
                    sb.append(name);
                    if (!version.isEmpty()) {
                        sb.append("@").append(version);
                    }
                    final GenericIdentifier id = new GenericIdentifier(sb.toString(), Confidence.HIGHEST);
                    dependency.addSoftwareIdentifier(id);
                }
            }
        }
        if (dependency.getVersion() != null && !dependency.getVersion().isEmpty()) {
            dependency.setDisplayFileName(String.format("%s:%s", dependency.getName(), dependency.getVersion()));
        } else {
            dependency.setDisplayFileName(dependency.getName());
        }
        setPackagePath(dependency);
    }

    /**
     * Extracts evidence from the contents and adds it to the given evidence
     * collection.
     *
     * @param contents the text to extract evidence from
     * @param blockVariable the block variable within the content to search for
     * @param fieldPattern the field pattern within the contents to search for
     * @return the evidence
     */
    private String determineEvidence(String contents, String blockVariable, String fieldPattern) {
        String value = "";

        //capture array value between [ ]
        final Matcher arrayMatcher = Pattern.compile(
                String.format("\\s*?%s\\.%s\\s*?=\\s*?\\{\\s*?(.*?)\\s*?\\}", blockVariable, fieldPattern),
                Pattern.CASE_INSENSITIVE).matcher(contents);
        if (arrayMatcher.find()) {
            value = arrayMatcher.group(1);
        } else { //capture single value between quotes
            final Matcher matcher = Pattern.compile(
                    String.format("\\s*?%s\\.%s\\s*?=\\s*?(['\"])(.*?)\\1", blockVariable, fieldPattern),
                    Pattern.CASE_INSENSITIVE).matcher(contents);
            if (matcher.find()) {
                value = matcher.group(2);
            }
        }
        return value;
    }

    /**
     * Sets the package path on the given dependency.
     *
     * @param dep the dependency to update
     */
    private void setPackagePath(Dependency dep) {
        final File file = new File(dep.getFilePath());
        final String parent = file.getParent();
        if (parent != null) {
            dep.setPackagePath(parent);
        }
    }
}