All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.exasol.projectkeeper.dependencyupdate.ChangesFileUpdater Maven / Gradle / Ivy

package com.exasol.projectkeeper.dependencyupdate;

import static java.util.function.Predicate.not;
import static java.util.stream.Collectors.joining;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.exasol.errorreporting.ExaError;
import com.exasol.projectkeeper.validators.changesfile.*;
import com.exasol.projectkeeper.validators.changesfile.ChangesFileSection.Builder;

/**
 * This class updates the the changesfile (e.g. {@code doc/changes/changes_1.2.0.md} for a given version, adding
 * information about fixed vulnerabilities in dependencies.
 */
// [impl->dsn~dependency-updater.update-changelog~1]
class ChangesFileUpdater {
    private static final Pattern ISSUE_URL_PATTERN = Pattern.compile("^https://github.com/exasol/[^/]+/issues/(\\d+)$");
    private static final String SECURITY_SECTION_HEADER = "## Security";
    private final ChangesFileIO changesFileIO;
    private final Path projectDir;
    private final VulnerabilityInfoProvider vulnerabilityInfoProvider;

    ChangesFileUpdater(final VulnerabilityInfoProvider vulnerabilityInfoProvider, final ChangesFileIO changesFileIO,
            final Path projectDir) {
        this.vulnerabilityInfoProvider = vulnerabilityInfoProvider;
        this.changesFileIO = changesFileIO;
        this.projectDir = projectDir;
    }

    void updateChanges(final String version) {
        final List vulnerabilities = vulnerabilityInfoProvider.getVulnerabilities();
        if (vulnerabilities.isEmpty()) {
            return;
        }
        final Path changesFilePath = getChangesFilePath(version);
        final ChangesFile changesFile = changesFileIO.read(changesFilePath);
        final ChangesFile updatedChanges = new Updater(changesFile, vulnerabilities).update();
        changesFileIO.write(updatedChanges, changesFilePath);
    }

    private Path getChangesFilePath(final String version) {
        return projectDir.resolve(ChangesFile.getPathForVersion(version));
    }

    private static class Updater {
        private final ChangesFile changesFile;
        private final List vulnerabilities;

        private Updater(final ChangesFile changesFile, final List vulnerabilities) {
            this.changesFile = changesFile;
            this.vulnerabilities = vulnerabilities;
        }

        private ChangesFile update() {
            return changesFile.toBuilder() //
                    .codeName(createCodeName()) //
                    .summary(createSummary()) //
                    .sections(createOtherSections()) //
                    .build();
        }

        private String createCodeName() {
            String codeName = changesFile.getCodeName() != null ? changesFile.getCodeName().trim() : "";
            codeName += codeName.isBlank() ? "Fixed " : ", fixed ";
            if (vulnerabilities.size() == 1) {
                final Vulnerability vulnerability = vulnerabilities.get(0);
                codeName += "vulnerability " + vulnerability.cve() + " in " + vulnerability.coordinates();
            } else {
                codeName += "vulnerabilities "
                        + vulnerabilities.stream().map(Vulnerability::cve).collect(joining(", "));
            }
            return codeName;
        }

        private ChangesFileSection createSummary() {
            return changesFile.getSummarySection() //
                    .map(ChangesFileSection::toBuilder) //
                    .orElseGet(() -> ChangesFileSection.builder("## Summary")) //
                    .addLine("") //
                    .addLine("This release fixes the following " + (vulnerabilities.size() == 1 ? "vulnerability"
                            : vulnerabilities.size() + " vulnerabilities") + ":") //
                    .addLine("")
                    .addLines(vulnerabilities.stream().map(this::renderVulnerability).flatMap(List::stream).toList()) //
                    .build();
        }

        private List renderVulnerability(final Vulnerability vulnerability) {
            final List lines = new ArrayList<>();
            lines.add("### " + vulnerability.cve() + " (" + vulnerability.cwe() + ") in dependency `"
                    + vulnerability.coordinates() + "`");
            lines.add(vulnerability.description());
            if (vulnerability.references() != null && !vulnerability.references().isEmpty()) {
                lines.add("#### References");
                lines.addAll(vulnerability.references().stream().map(link -> "* " + link).toList());
            }
            lines.add("");
            return lines;
        }

        private List createOtherSections() {
            final List sections = new ArrayList<>();
            sections.add(buildSecuritySection());
            sections.addAll(changesFile.getSections().stream()
                    .filter(section -> !section.getHeading().equals(SECURITY_SECTION_HEADER)) //
                    .filter(not(this::isDefaultFeaturesSection)).toList());
            return sections;
        }

        private boolean isDefaultFeaturesSection(final ChangesFileSection section) {
            if (!section.getHeading().equals(ChangesFileValidator.FEATURES_SECTION)) {
                return false;
            }
            return section.getContent().stream() //
                    .map(String::trim) //
                    .filter(not(String::isEmpty)) //
                    .filter(line -> !line.equals(ChangesFileValidator.FIXED_ISSUE_TEMPLATE)) //
                    .findAny().isEmpty();
        }

        private ChangesFileSection buildSecuritySection() {
            final Builder securitySectionBuilder = getExistingSecuritySection();
            securitySectionBuilder.addLines(vulnerabilities.stream().map(this::createIssueFixesEntry).toList());
            securitySectionBuilder.addLine("");
            return securitySectionBuilder.build();
        }

        private Builder getExistingSecuritySection() {
            return this.changesFile.getSections().stream() //
                    .filter(section -> section.getHeading().equals(SECURITY_SECTION_HEADER)) //
                    .map(ChangesFileSection::toBuilder) //
                    .findFirst() //
                    .orElseGet(() -> ChangesFileSection.builder(SECURITY_SECTION_HEADER).addLine(""));
        }

        private String createIssueFixesEntry(final Vulnerability vulnerability) {
            return "* #" + getIssueNumber(vulnerability.issueUrl()) + ": Fixed vulnerability " + vulnerability.cve()
                    + " in dependency `" + vulnerability.coordinates() + "`";
        }

        private String getIssueNumber(final String issueUrl) {
            final Matcher matcher = ISSUE_URL_PATTERN.matcher(issueUrl);
            if (!matcher.matches()) {
                throw new IllegalArgumentException(ExaError.messageBuilder("E-PK-CORE-181")
                        .message("Issues URL {{url}} does not match expected pattern {{pattern}}.", issueUrl,
                                ISSUE_URL_PATTERN)
                        .ticketMitigation().toString());
            }
            return matcher.group(1);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy