ChecksumRule.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.rules;

import io.github.sbom.enforcer.BillOfMaterials;
import io.github.sbom.enforcer.Component;
import io.github.sbom.enforcer.Component.ChecksumAlgorithm;
import io.github.sbom.enforcer.EnforcerRule;
import java.io.File;
import java.io.IOException;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Named;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.maven.plugin.MojoFailureException;
import org.jspecify.annotations.Nullable;

/**
 * Rules to check if the checksums present in the SBOM are correct.
 */
@Named("checksum")
public class ChecksumRule implements EnforcerRule {
    @Override
    public void execute(BillOfMaterials bom) throws MojoFailureException {
        List<String> errors = new ArrayList<>(validateChecksums(bom.getComponent()));
        for (Component dependency : bom.getDependencies()) {
            errors.addAll(validateChecksums(dependency));
        }

        if (!errors.isEmpty()) {
            String message = errors.stream()
                    .sorted()
                    .collect(Collectors.joining(
                            "\n* ",
                            "\nSBOM " + bom.getBillOfMaterials().getFile() + " contains invalid checksums:\n* ",
                            ""));
            throw new MojoFailureException(message);
        }
    }

    private static List<String> validateChecksums(Component component) {
        // If there are no checksums, there is nothing to validate
        if (component.getChecksums().isEmpty()) {
            return List.of();
        }
        File file = component.getArtifact().getFile();
        if (file == null || !file.exists()) {
            return List.of("Missing file for artifact: " + component.getArtifact());
        }
        return component.getChecksums().entrySet().stream()
                .<String>mapMulti((entry, consumer) -> {
                    String error = validateChecksum(entry.getKey(), entry.getValue(), file);
                    if (error != null) {
                        consumer.accept(error);
                    }
                })
                .toList();
    }

    static @Nullable String validateChecksum(ChecksumAlgorithm algorithm, String expectedValue, File file) {
        try {
            MessageDigest digest = DigestUtils.getDigest(algorithm.toJce());
            String computedValue = Hex.encodeHexString(DigestUtils.digest(digest, file));
            if (!expectedValue.equals(computedValue)) {
                return "Invalid " + algorithm + " checksum for file " + file.getName() + ": expecting `" + expectedValue
                        + "` but got `" + computedValue + "`";
            }
        } catch (IllegalArgumentException e) {
            return "Failed to calculate checksum for file " + file.getName() + ": algorithm " + algorithm.toJce()
                    + " is not supported.";
        } catch (IOException e) {
            return "Failed to calculate checksum for file " + file.getName() + ": " + e;
        }
        return null;
    }
}