ElixirMixAuditAnalyzer.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) 2020 The OWASP Foundation. All Rights Reserved.
 */
package org.owasp.dependencycheck.analyzer;

import org.owasp.dependencycheck.Engine;
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
import org.owasp.dependencycheck.dependency.Dependency;
import org.owasp.dependencycheck.exception.InitializationException;
import org.owasp.dependencycheck.utils.FileFilterBuilder;
import org.owasp.dependencycheck.utils.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import us.springett.parsers.cpe.exceptions.CpeValidationException;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
import org.owasp.dependencycheck.processing.MixAuditProcessor;
import org.owasp.dependencycheck.utils.processing.ProcessReader;

@Experimental
public class ElixirMixAuditAnalyzer extends AbstractFileTypeAnalyzer {

    /**
     * The logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(ElixirMixAuditAnalyzer.class);

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

    /**
     * The name of the analyzer.
     */
    private static final String ANALYZER_NAME = "Elixir Mix Audit Analyzer";

    /**
     * The phase that this analyzer is intended to run in.
     */
    private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.PRE_INFORMATION_COLLECTION;
    /**
     * The filter defining which files will be analyzed.
     */
    private static final FileFilter FILTER = FileFilterBuilder.newInstance().addFilenames("mix.lock").build();
    /**
     * Name.
     */
    public static final String NAME = "Name: ";
    /**
     * Version.
     */
    public static final String VERSION = "Version: ";
    /**
     * Advisory.
     */
    public static final String ADVISORY = "Advisory: ";
    /**
     * Criticality.
     */
    public static final String CRITICALITY = "Criticality: ";

    /**
     * @return a filter that accepts files named mix.lock
     */
    @Override
    protected FileFilter getFileFilter() {
        return FILTER;
    }

    @Override
    protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
        // Here we check if mix_audit actually runs from this location. We do this by running the
        // `mix_audit --version` command and seeing whether or not it succeeds (if it returns with an exit value of 0)
        final Process process;
        try {
            final List<String> mixAuditArgs = Collections.singletonList("--version");
            process = launchMixAudit(getSettings().getTempDirectory(), mixAuditArgs);
        } catch (AnalysisException ae) {
            setEnabled(false);
            final String msg = String.format("Exception from mix_audit process: %s. Disabling %s", ae.getCause(), ANALYZER_NAME);
            throw new InitializationException(msg, ae);
        } catch (IOException ex) {
            setEnabled(false);
            throw new InitializationException("Unable to create temporary file, the Mix Audit Analyzer will be disabled", ex);
        }

        final int exitValue;
        final String mixAuditVersionDetails;
        try (ProcessReader processReader = new ProcessReader(process)) {
            processReader.readAll();
            exitValue = process.exitValue();

            if (exitValue != 0) {
                if (StringUtils.isBlank(processReader.getError())) {
                    LOGGER.warn("Unexpected exit value from mix_audit process and error stream unexpectedly not ready to capture error details. "
                            + "Disabling {}. Exit value was: {}", ANALYZER_NAME, exitValue);
                    setEnabled(false);
                    throw new InitializationException("mix_audit error stream unexpectedly not ready.");
                } else {
                    setEnabled(false);
                    LOGGER.warn("Unexpected exit value from mix_audit process. Disabling {}. Exit value was: {}. "
                            + "error stream output from mix_audit process was: {}", ANALYZER_NAME, exitValue, processReader.getError());
                    throw new InitializationException("Unexpected exit value from bundle-audit process.");
                }
            } else {
                if (StringUtils.isBlank(processReader.getOutput())) {
                    LOGGER.warn("mix_audit input stream unexpectedly not ready to capture version details. Disabling {}", ANALYZER_NAME);
                    setEnabled(false);
                    throw new InitializationException("mix_audit input stream unexpectedly not ready to capture version details.");
                } else {
                    mixAuditVersionDetails = processReader.getOutput();
                }
            }
        } catch (InterruptedException ex) {
            setEnabled(false);
            final String msg = String.format("mix_audit process was interrupted. Disabling %s", ANALYZER_NAME);
            Thread.currentThread().interrupt();
            throw new InitializationException(msg);
        } catch (IOException ex) {
            setEnabled(false);
            final String msg = String.format("IOException '%s' during mix_audit process was interrupted. Disabling %s",
                    ex.getMessage(), ANALYZER_NAME);
            throw new InitializationException(msg);
        }

        if (isEnabled()) {
            LOGGER.debug("{} is enabled and is using mix_audit with version: {}.", ANALYZER_NAME, mixAuditVersionDetails);
        }
    }

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

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

    /**
     * Launch mix audit.
     *
     * @param folder directory that contains the mix.lock file
     * @param mixAuditArgs the arguments to pass to mix audit
     * @return a handle to the process
     * @throws AnalysisException thrown when there is an issue launching mix
     * audit
     */
    private Process launchMixAudit(File folder, List<String> mixAuditArgs) throws AnalysisException {
        if (!folder.isDirectory()) {
            throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath()));
        }

        final List<String> args = new ArrayList<>();
        final String mixAuditPath = getSettings().getString(Settings.KEYS.ANALYZER_MIX_AUDIT_PATH);
        File mixAudit = null;

        if (mixAuditPath != null) {
            mixAudit = new File(mixAuditPath);
            if (!mixAudit.isFile()) {
                LOGGER.warn("Supplied `mixAudit` path is incorrect: {}", mixAuditPath);
                mixAudit = null;
            }
        } else {
            final Path homePath = Paths.get(System.getProperty("user.home"));
            final Path escriptPath = Paths.get(homePath.toString(), ".mix", "escripts", "mix_audit");
            mixAudit = escriptPath.toFile();
        }

        args.add(mixAudit != null ? mixAudit.getAbsolutePath() : "mix_audit");
        args.addAll(mixAuditArgs);
        final ProcessBuilder builder = new ProcessBuilder(args);

        builder.directory(folder);
        try {
            LOGGER.info("Launching: {} from {}", args, folder);
            return builder.start();
        } catch (IOException ioe) {
            throw new AnalysisException("mix_audit initialization failure; this error can be ignored if you are not analyzing Elixir. "
                    + "Otherwise ensure that mix_audit is installed and the path to mix_audit is correctly specified", ioe);
        }
    }

    /**
     * Determines if the analyzer can analyze the given file type.
     *
     * @param dependency the dependency to determine if it can analyze
     * @param engine the dependency-check engine
     * @throws AnalysisException thrown if there is an analysis exception.
     */
    @Override
    protected void analyzeDependency(Dependency dependency, Engine engine)
            throws AnalysisException {
        final File parentFile = dependency.getActualFile().getParentFile();
        final List<String> mixAuditArgs = Arrays.asList("--format", "json");

        final Process process = launchMixAudit(parentFile, mixAuditArgs);
        final int exitValue;
        try (MixAuditProcessor processor = new MixAuditProcessor(dependency, engine);
                ProcessReader processReader = new ProcessReader(process, processor)) {
            processReader.readAll();
            exitValue = process.exitValue();
            if (exitValue < 0 || exitValue > 1) {
                final String msg = String.format("Unexpected exit code from mix_audit process; exit code: %s", exitValue);
                throw new AnalysisException(msg);
            }
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            throw new AnalysisException("mix_audit process interrupted", ie);
        } catch (IOException | CpeValidationException ioe) {
            LOGGER.warn("mix_audit failure", ioe);
            throw new AnalysisException("mix_audit failure", ioe);
        }
    }
}