FacepalmMojo.java

/*
 * SPDX-License-Identifier: Apache-2.0
 * Copyright 2026 Nikolas Charalambidis
 */

package dev.nichar.facepalm;

import static dev.nichar.facepalm.configurator.CommaSeparatedConfigurator.COMMA_SEPARATED_CONFIGURATOR;

import com.google.inject.Guice;
import dev.nichar.facepalm.config.EngineConfig;
import dev.nichar.facepalm.config.EvaluatorConfig;
import dev.nichar.facepalm.config.PatternConfig;
import dev.nichar.facepalm.config.PostProcessorConfig;
import dev.nichar.facepalm.config.ScoringConfig;
import dev.nichar.facepalm.engine.FacepalmRunner;
import dev.nichar.facepalm.engine.ScoringStrategy;
import dev.nichar.facepalm.module.FacepalmConfigModule;
import dev.nichar.facepalm.module.FacepalmLogModule;
import jakarta.annotation.Nonnull;
import java.io.File;
import java.util.Set;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.eclipse.sisu.space.BeanScanning;
import org.eclipse.sisu.space.SpaceModule;
import org.eclipse.sisu.space.URLClassSpace;
import org.eclipse.sisu.wire.WireModule;

/**
 * Entry point for security scans during the {@code verify} phase.
 * Bootstraps the Guice engine and executes the scanning pipeline.
 */
@Mojo(name = "scan", defaultPhase = LifecyclePhase.VERIFY, threadSafe = true, configurator = COMMA_SEPARATED_CONFIGURATOR)
public class FacepalmMojo extends AbstractMojo {

    /**
     * Metadata about the current plugin execution.
     */
    @Parameter(defaultValue = "${plugin}", readonly = true)
    private PluginDescriptor pluginDescriptor;

    /**
     * Maven project base directory.
     */
    @Parameter(defaultValue = "${project.basedir}", readonly = true)
    private File baseDir;

    /**
     * Build output directory for reports and artifacts.
     */
    @Parameter(defaultValue = "${project.build.directory}", readonly = true)
    private File outputDirectory;

    /**
     * Target directory for the security scan.
     */
    @Parameter(property = "root")
    private File root;

    /**
     * Thread pool size for parallel scanning. Defaults to CPU count.
     */
    @Parameter(property = "threads")
    private Integer threads;

    /**
     * Fail-safe limit for file sizes. Prevents memory exhaustion.
     */
    @Parameter(property = "maxFileSizeBytes", defaultValue = "5242880")
    private long maxFileSizeBytes;

    /**
     * Filter for binary files and non-text assets.
     */
    @Parameter(property = "skipBinaryRegex", defaultValue = ".*\\.(png|jpg|jpeg|gif|pdf|zip|jar|class|tar|gz|exe|dll)$")
    private String skipBinaryRegex;

    /**
     * User-defined exclusions for the scanner.
     */
    @Parameter(property = "skipDirs")
    private Set<String> skipDirs;

    /**
     * Log successfully analyzed files.
     */
    @Parameter(property = "showProcessed", defaultValue = "false")
    private boolean showProcessed;

    /**
     * Log files skipped by filters.
     */
    @Parameter(property = "showSkipped", defaultValue = "false")
    private boolean showSkipped;

    /**
     * Strategy used to combine risk and confidence into a final score.
     */
    @Parameter(property = "scoring.strategy", defaultValue = "WEIGHTED_QUADRATIC")
    private ScoringStrategy scoringStrategy;

    /**
     * Minimum score to trigger a critical failure.
     */
    @Parameter(property = "errorThreshold", defaultValue = "80")
    private int errorThreshold;

    /**
     * Minimum score to trigger a warning.
     */
    @Parameter(property = "warningThreshold", defaultValue = "40")
    private int warningThreshold;

    /**
     * Verbose logging for scoring decisions.
     */
    @Parameter(property = "showScoring", defaultValue = "false")
    private boolean showScoring;

    /**
     * Statistics for scan coverage and exclusions.
     */
    @Parameter(property = "showDetails", defaultValue = "false")
    private boolean showDetails;

    /**
     * Stop the build on high-risk findings.
     */
    @Parameter(property = "failOnError", defaultValue = "true")
    private boolean failOnError;

    /**
     * Stop the build on moderate-risk findings.
     */
    @Parameter(property = "failOnWarnings", defaultValue = "false")
    private boolean failOnWarnings;

    /**
     * Logic for evaluating leak legitimacy.
     */
    @Parameter
    private EvaluatorConfig evaluators = new EvaluatorConfig();

    /**
     * Definitions for secret detection.
     */
    @Parameter
    private PatternConfig patterns = new PatternConfig();

    /**
     * Logic for noise reduction and deduplication.
     */
    @Parameter
    private PostProcessorConfig postProcessing = new PostProcessorConfig();

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {

        // Map Maven parameters to internal configuration.
        final var engine = new EngineConfig(threads, maxFileSizeBytes, skipBinaryRegex, skipDirs, showProcessed,
            showSkipped);
        final var scoring = new ScoringConfig(scoringStrategy, errorThreshold, warningThreshold, showScoring,
            showDetails, failOnError, failOnWarnings);
        final var effectiveConfig = new FacepalmConfig(engine, scoring, evaluators, postProcessing, patterns);

        // Discover components using Sisu indexing.
        final var space = new URLClassSpace(getClass().getClassLoader());

        // Initialize the Guice container.
        final var injector = Guice.createInjector(
            new WireModule(
                // Use pre-compiled JSR330 metadata for rapid startup.
                new SpaceModule(space, BeanScanning.INDEX),
                // Bridge Maven Log and state into the container.
                new FacepalmLogModule(getLog()),
                new FacepalmConfigModule(effectiveConfig)));

        // Inject components manually since Maven handles Mojo instantiation.
        final var runner = injector.getInstance(FacepalmRunner.class);
        final var config = injector.getInstance(FacepalmConfig.class);
        final var log = injector.getInstance(Log.class);

        if (runner == null || config == null || log == null) {
            throw new MojoExecutionException("Facepalm Mojo initialization failed: " +
                "runner=" + runner + ", config=" + config + ", log=" + log);
        }

        log.info("Starting facepalm-maven-plugin " + pluginDescriptor.getVersion());

        // Normalize the target scan path.
        final var rootFile = root != null ? root : baseDir;
        final var rootPath = rootFile.toPath().toAbsolutePath().normalize();

        try {
            runner.run(rootPath, outputDirectory.toPath(), pluginDescriptor.getVersion());
        } catch (MojoFailureException e) {
            throw e;
        } catch (Exception e) {
            throw executionFailure(e);
        }
    }

    /**
     * Wraps an unexpected scan error so that the build error states its cause (e.g. an invalid
     * {@code skipBinaryRegex} or an unwritable output directory) without rerunning Maven with {@code -e}.
     */
    @Nonnull
    static MojoExecutionException executionFailure(@Nonnull final Exception e) {
        final var reason = e.getMessage() != null ? e.getMessage() : e.getClass().getName();
        return new MojoExecutionException("Facepalm scan failed: " + reason, e);
    }
}