DartAnalyzer.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) 2022 Marc Rödder. All Rights Reserved.
 */
package org.owasp.dependencycheck.analyzer;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
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.dependency.Confidence;
import org.owasp.dependencycheck.dependency.Dependency;
import org.owasp.dependencycheck.dependency.EvidenceType;
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.util.Iterator;
import java.util.Map;

/**
 * This analyzer is used to analyze Dart packages by collecting information from
 * pubspec lock and yaml files. See https://dart.dev/tools/pub/pubspec
 *
 * @author Marc Rödder (http://github.com/sticksen)
 */
@Experimental
@ThreadSafe
public class DartAnalyzer extends AbstractFileTypeAnalyzer {

//    /**
//     * A descriptor for the type of dependencies processed or added by this
//     * analyzer.
//     */
//    // currently not supported by Sonatype OSS
//    public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.DART;
    /**
     * The logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(DartAnalyzer.class);

    /**
     * The lock file name.
     */
    private static final String LOCK_FILE = "pubspec.lock";
    /**
     * The YAML file name.
     */
    private static final String YAML_FILE = "pubspec.yaml";

    @Override
    protected FileFilter getFileFilter() {
        return FileFilterBuilder.newInstance().addFilenames(LOCK_FILE, YAML_FILE).build();
    }

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

    @Override
    public String getName() {
        return "Dart Package Analyzer";
    }

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

    /**
     * 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_DART_ENABLED;
    }

    @Override
    protected void analyzeDependency(Dependency dependency, Engine engine)
            throws AnalysisException {
        final String fileName = dependency.getFileName();
        LOGGER.debug("Checking file {}", fileName);

        switch (fileName) {
            case LOCK_FILE:
                analyzeLockFileDependencies(dependency, engine);
                break;
            case YAML_FILE:
                analyzeYamlFileDependencies(dependency, engine);
                break;
            default:
            //do nothing
        }
    }

    private void analyzeYamlFileDependencies(Dependency yamlFileDependency, Engine engine) throws AnalysisException {
        engine.removeDependency(yamlFileDependency);
        final File yamlFile = yamlFileDependency.getActualFile();
        if (YAML_FILE.equals(yamlFile.getName())) {
            final File lock = new File(yamlFile.getParentFile(), LOCK_FILE);
            if (lock.isFile()) {
                LOGGER.debug("Skipping {} because {} exists", yamlFile, lock);
                return;
            }
        }

        final JsonNode rootNode;
        final ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
        try {
            rootNode = objectMapper.readTree(yamlFile);
        } catch (IOException e) {
            throw new AnalysisException("Problem occurred while reading dependency file.", e);
        }

        if (rootNode.hasNonNull("dependencies")) {
            final Iterator<Map.Entry<String, JsonNode>> dependencies = rootNode.get("dependencies").fields();
            addYamlDependenciesToEngine(dependencies, yamlFile, engine);
        }
        //TODO - add configuration to allow skipping dev dependencies.
        if (rootNode.hasNonNull("dev_dependencies")) {
            final Iterator<Map.Entry<String, JsonNode>> devDependencies = rootNode.get("dev_dependencies").fields();
            addYamlDependenciesToEngine(devDependencies, yamlFile, engine);
        }

        addYamlDartDependencyToEngine(rootNode, yamlFile, engine);
    }

    private void analyzeLockFileDependencies(Dependency lockFileDependency, Engine engine) throws AnalysisException {
        engine.removeDependency(lockFileDependency);

        final JsonNode rootNode;
        final File lockFile = lockFileDependency.getActualFile();
        final ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
        try {
            rootNode = objectMapper.readTree(lockFile);
        } catch (IOException e) {
            throw new AnalysisException("Problem occurred while reading dependency lockFile.", e);
        }

        addLockFileDependenciesToEngine(lockFile, engine, rootNode);
        addLockFileDartVersionToEngine(lockFile, engine, rootNode);
    }

    private void addLockFileDartVersionToEngine(File file, Engine engine, JsonNode rootNode) throws AnalysisException {
        final String dartVersion = rootNode.get("sdks").get("dart").textValue();
        final String minimumVersion = extractMinimumVersion(dartVersion);

        engine.addDependency(
                createDependencyFromNameAndVersion(file, "dart_software_development_kit", minimumVersion));
    }

    private void addLockFileDependenciesToEngine(File file, Engine engine, JsonNode rootNode) throws AnalysisException {
        for (JsonNode nextPackage : rootNode.get("packages")) {
            final JsonNode description = nextPackage.get("description");
            if (description == null) {
                continue;
            }

            final JsonNode nameNode = description.get("name");
            if (nameNode == null) {
                continue;
            }

            final JsonNode versionNode = nextPackage.get("version");
            if (versionNode == null) {
                continue;
            }

            final String name = nameNode.asText();
            final String version = versionNode.asText();
            LOGGER.debug("Found dependency in {} file, name: {}, version: {}", LOCK_FILE, name, version);

            engine.addDependency(
                    createDependencyFromNameAndVersion(file, name, version)
            );
        }
    }

    private void addYamlDependenciesToEngine(Iterator<Map.Entry<String, JsonNode>> dependencies, File file, Engine engine) throws AnalysisException {
        while (dependencies.hasNext()) {
            final Map.Entry<String, JsonNode> entry = dependencies.next();

            final String name = entry.getKey();
            final String versionRaw = entry.getValue().asText();
            final String version = extractMinimumVersion(versionRaw);
            LOGGER.debug("Found dependency in {} file, name: {}, version: {}", YAML_FILE, name, version);

            engine.addDependency(
                    createDependencyFromNameAndVersion(file, name, version)
            );
        }
    }

    private void addYamlDartDependencyToEngine(JsonNode rootNode, File file, Engine engine) throws AnalysisException {
        if (rootNode.hasNonNull("environment") && rootNode.get("environment").hasNonNull("sdk")) {
            final String dartVersion = rootNode.get("environment").get("sdk").textValue();
            final String minimumVersion = extractMinimumVersion(dartVersion);

            engine.addDependency(
                    createDependencyFromNameAndVersion(file, "dart_software_development_kit", minimumVersion));
        }
    }

    private Dependency createDependencyFromNameAndVersion(File file, String name, String version) throws AnalysisException {
        final Dependency dependency = new Dependency(file, true);

        // currently not supported by Sonatype OSS
//            // dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
        dependency.setName(name);
        dependency.setVersion(version);

        final PackageURL packageURL;
        try {
            packageURL = PackageURLBuilder
                    .aPackageURL()
                    .withType("pub")
                    .withName(dependency.getName())
                    .withVersion(version.isEmpty() ? null : version)
                    .build();

        } catch (MalformedPackageURLException e) {
            throw new AnalysisException("Problem occurred while reading dependency file.", e);
        }

        dependency.addSoftwareIdentifier(new PurlIdentifier(packageURL, Confidence.HIGHEST));

        dependency.addEvidence(EvidenceType.PRODUCT, file.getName(), "name", name, Confidence.HIGHEST);
        dependency.addEvidence(EvidenceType.VENDOR, file.getName(), "name", name, Confidence.HIGHEST);
        dependency.addEvidence(EvidenceType.VENDOR, file.getName(), "name", "dart", Confidence.HIGHEST);
        if (!version.isEmpty()) {
            dependency.addEvidence(EvidenceType.VERSION, file.getName(), "version", version, Confidence.MEDIUM);
        }

        final String packagePath = String.format("%s:%s", name, version);
        dependency.setSha1sum(Checksum.getSHA1Checksum(packagePath));
        dependency.setSha256sum(Checksum.getSHA256Checksum(packagePath));
        dependency.setMd5sum(Checksum.getMD5Checksum(packagePath));
        dependency.setPackagePath(packagePath);
        dependency.setDisplayFileName(packagePath);

        return dependency;
    }

    private String extractMinimumVersion(String versionRaw) {
        final String version;
        if (versionRaw.contains("^")) {
            version = versionRaw.replace("^", "");
        } else if (versionRaw.contains("<")) {
            // this code should parse version definitions like ">=2.10.0 <3.0.0"
            final String firstPart = versionRaw.split("<")[0].trim();
            version = firstPart.replace(">=", "").trim();
        } else if (versionRaw.contains("any") || "null".equals(versionRaw)) {
            version = "";
        } else {
            version = versionRaw;
        }

        LOGGER.debug("Extracted minimum version: {} from raw version: {}", version, versionRaw);
        return version;
    }
}