CheckMojo.java

/*
 * Copyright © 2025 Christian Grobmeier, Piotr P. Karwasz
 *
 * 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
 *
 *     https://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.
 */
package io.github.sbom.enforcer;

import io.github.sbom.enforcer.internal.Artifacts;
import io.github.sbom.enforcer.support.DefaultBomBuilderRequest;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.PluginParameterExpressionEvaluator;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.codehaus.plexus.component.configurator.ComponentConfigurationException;
import org.codehaus.plexus.component.configurator.ComponentConfigurator;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.configuration.DefaultPlexusConfiguration;
import org.codehaus.plexus.configuration.PlexusConfiguration;
import org.eclipse.aether.AbstractForwardingRepositorySystemSession;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.LocalRepositoryManager;
import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
import org.eclipse.aether.repository.RepositoryPolicy;
import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;

/**
 * Performs a configurable set of checks on the SBOMs attached to the build.
 * <p>
 *     See <a href="https://sbom-enforcer.github.io/maven-plugin/rules.html">Rules</a> for a list of available rules.
 * </p>
 */
@Mojo(name = "check", defaultPhase = LifecyclePhase.VERIFY)
public class CheckMojo extends AbstractMojo {

    /**
     * If set to {@code true}, the contents of the per-user local Maven repository are ignored
     * and a per-Maven module local Maven repository is used instead.
     */
    @Parameter(defaultValue = "false")
    private boolean usePrivateLocalRepo;

    /**
     * Configuration of the rules to execute.
     */
    @Parameter
    private PlexusConfiguration rules = new DefaultPlexusConfiguration("rules");

    /**
     * The current repository/network configuration of Maven.
     */
    @Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
    private RepositorySystemSession repoSession;

    /**
     * Path to a local Maven repository to use if `usePrivateLocalRepo` is true.
     */
    @Parameter(defaultValue = "${project.build.directory}/sbom-enforcer/repository")
    protected Path privateLocalRepoPath;

    /**
     * The current Maven project.
     */
    private final MavenProject project;

    /**
     * The current Maven session
     */
    private final MavenSession session;

    /**
     * The mojoExecution of this mojo
     */
    private final MojoExecution mojoExecution;

    /**
     *
     */
    private final ComponentConfigurator componentConfigurator;

    /**
     * Builders for supported SBOM formats.
     */
    private final Set<BomBuilder> bomBuilders;

    /**
     * Used to retrieve instances of {@link EnforcerRule} by name.
     * <p>
     *     See also <a href="rules.html>collection of built-in rules</a>.
     * </p>
     */
    private final PlexusContainer container;

    /**
     * Used to create a temporary local repository.
     */
    private final LocalRepositoryManagerFactory localRepositoryManagerFactory;

    @Inject
    public CheckMojo(
            MavenProject project,
            MavenSession session,
            MojoExecution mojoExecution,
            @Named("basic") ComponentConfigurator componentConfigurator,
            Set<BomBuilder> bomBuilders,
            PlexusContainer container,
            LocalRepositoryManagerFactory localRepositoryManagerFactory) {
        this.project = project;
        this.session = session;
        this.mojoExecution = mojoExecution;
        this.componentConfigurator = componentConfigurator;
        this.bomBuilders = bomBuilders;
        this.container = container;
        this.localRepositoryManagerFactory = localRepositoryManagerFactory;
    }

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        List<? extends EnforcerRule> rules = createEnforcerRules();

        for (Artifact artifact : project.getAttachedArtifacts()) {
            checkArtifact(artifact, rules);
        }
    }

    private void checkArtifact(Artifact bomArtifact, List<? extends EnforcerRule> rules)
            throws MojoExecutionException, MojoFailureException {
        org.eclipse.aether.artifact.Artifact mainBillOfMaterials = Artifacts.toArtifact(bomArtifact);
        for (BomBuilder bomBuilder : bomBuilders) {
            if (bomBuilder.isSupported(mainBillOfMaterials)) {
                try {
                    RepositorySystemSession effectiveRepoSession = usePrivateLocalRepo
                            ? new CustomLocalRepositorySystemSession(repoSession, createLocalRepositoryManager())
                            : repoSession;

                    // POM projects don't have a resolved artifact
                    org.eclipse.aether.artifact.Artifact artifact = Artifacts.toArtifact(project.getArtifact());
                    if ("pom".equals(project.getPackaging()) && artifact.getFile() == null) {
                        artifact = artifact.setFile(project.getFile());
                    }
                    BomBuilderRequest request = DefaultBomBuilderRequest.newBuilder()
                            .setArtifact(artifact)
                            .setMainBillOfMaterials(mainBillOfMaterials)
                            .get();

                    BillOfMaterials billOfMaterials = bomBuilder.build(effectiveRepoSession, request);
                    for (EnforcerRule rule : rules) {
                        rule.execute(billOfMaterials);
                    }
                } catch (BomBuildingException e) {
                    throw new MojoFailureException("Failed to parse BOM artifact " + mainBillOfMaterials, e);
                }
                break;
            }
        }
    }

    private LocalRepositoryManager createLocalRepositoryManager() throws MojoExecutionException {
        try {
            LocalRepository repository = new LocalRepository(privateLocalRepoPath.toFile());
            return localRepositoryManagerFactory.newInstance(repoSession, repository);
        } catch (NoLocalRepositoryManagerException e) {
            throw new MojoExecutionException(e);
        }
    }

    // package-private for testing
    List<? extends EnforcerRule> createEnforcerRules() throws MojoExecutionException {
        ExpressionEvaluator evaluator = new PluginParameterExpressionEvaluator(session, mojoExecution);

        List<EnforcerRule> enforcerRules = new ArrayList<>();
        ClassRealm realm =
                mojoExecution.getMojoDescriptor().getPluginDescriptor().getClassRealm();
        container.setLookupRealm(realm);
        for (PlexusConfiguration ruleConfig : rules.getChildren()) {
            try {
                EnforcerRule rule = container.lookup(EnforcerRule.class, ruleConfig.getName());
                componentConfigurator.configureComponent(rule, ruleConfig, evaluator, realm);
                enforcerRules.add(rule);
            } catch (ComponentLookupException e) {
                throw new MojoExecutionException(
                        "Failed to instantiate SBOM Enforcer rule `" + ruleConfig.getName() + "`", e);
            } catch (ComponentConfigurationException e) {
                throw new MojoExecutionException(
                        "Failed to configure SBOM Enforcer rule `" + ruleConfig.getName() + "`", e);
            }
        }
        return enforcerRules;
    }

    public void setUsePrivateLocalRepo(boolean usePrivateLocalRepo) {
        this.usePrivateLocalRepo = usePrivateLocalRepo;
    }

    public void setPrivateLocalRepoPath(Path privateLocalRepoPath) {
        this.privateLocalRepoPath = privateLocalRepoPath;
    }

    public void setRules(PlexusConfiguration rules) {
        this.rules = rules;
    }

    public void setRepoSession(RepositorySystemSession repoSession) {
        this.repoSession = repoSession;
    }

    public void addRule(PlexusConfiguration rule) {
        rules.addChild(rule);
    }

    /**
     * Replaces the local repository manager to prevent the usage of artifacts installed locally.
     */
    private static class CustomLocalRepositorySystemSession extends AbstractForwardingRepositorySystemSession {

        private final RepositorySystemSession session;
        private final LocalRepositoryManager localRepositoryManager;

        CustomLocalRepositorySystemSession(
                RepositorySystemSession session, LocalRepositoryManager localRepositoryManager) {
            this.session = session;
            this.localRepositoryManager = localRepositoryManager;
        }

        @Override
        protected RepositorySystemSession getSession() {
            return session;
        }

        @Override
        public String getChecksumPolicy() {
            return RepositoryPolicy.CHECKSUM_POLICY_FAIL;
        }

        @Override
        public LocalRepositoryManager getLocalRepositoryManager() {
            return localRepositoryManager;
        }
    }
}