diff --git a/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/FortifyCLI.java b/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/FortifyCLI.java index f2ab435cb7..34eaf65c90 100644 --- a/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/FortifyCLI.java +++ b/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/FortifyCLI.java @@ -17,9 +17,9 @@ /** *

This class provides the {@link #main(String[])} entrypoint into the application, - * and also registers some GraalVM features, allowing the application to run properly + * and also registers some GraalVM features, allowing the application to run properly * as GraalVM native images.

- * + * * @author Ruud Senden */ public class FortifyCLI { diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java index 2f717dd66b..64fa2cf8c5 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java @@ -30,6 +30,8 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.zip.ZipFile; @@ -37,6 +39,7 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import com.fortify.cli.aviator.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; @@ -53,8 +56,6 @@ import com.fortify.cli.aviator.fpr.utils.SourceDecoders; import com.fortify.cli.aviator.fpr.utils.SourceEncoder; import com.fortify.cli.aviator.fpr.utils.SourceEncoder.SourceEncodeException; -import com.fortify.cli.aviator.util.FprHandle; -import com.fortify.cli.aviator.util.FuzzyContextSearcher; public class RemediationProcessor { private static final Logger LOG = LoggerFactory.getLogger(RemediationProcessor.class); @@ -78,6 +79,8 @@ private record PendingFileWrite(String filename, Path filePath, String content, private record RollbackFileWrite(String filename, Path filePath, byte[] originalBytes) {} + private record RemediationKey(String fileName, Path filePath,int lineFrom,int lineTo,String comparisonCode){} + private enum SkipReason { SOURCE_FILE_MISSING("Source file missing"), SOURCE_FILE_OUTSIDE_SOURCE_DIR("Source file outside source directory"), @@ -154,8 +157,10 @@ public RemediationMetric processRemediationXML() { Document remediationDoc; int totalRemediations; int appliedRemediations; + int identicalRemediations = 0; Set modifiedFiles = new LinkedHashSet<>(); Map skippedByReason = new LinkedHashMap<>(); + Map remediationLookup = new LinkedHashMap<>(); // Sanitize and normalize the base source directory path once. String trimmedSourceDir = sourceCodeDirectory.trim(); @@ -185,8 +190,54 @@ public RemediationMetric processRemediationXML() { appliedRemediations = 0; for (int i = 0; i < remediationNodes.getLength(); i++) { Element remediation = (Element) remediationNodes.item(i); - if (processRemediation(remediation, sourceBasePath, fvdlMetadata, modifiedFiles, skippedByReason)) { + + String instanceId = + remediation.getAttribute("instanceId"); + + List remediationKeys = createRemediationKeys( + remediation, + sourceBasePath); + + String identicalInstanceId = null; + + for (RemediationKey key : remediationKeys) { + String existingInstanceId = + remediationLookup.get(key); + + if (existingInstanceId != null) { + identicalInstanceId = existingInstanceId; + break; + } + } + + if (identicalInstanceId != null) { + identicalRemediations++; + appliedRemediations++; + + LOG.info( + "Identical found: {}", + identicalInstanceId); + + LOG.info( + "Identical Remediation Applied: {} is identical to {}", + instanceId, + identicalInstanceId); + + continue; + } + + if (processRemediation( + remediation, + sourceBasePath, + fvdlMetadata, + modifiedFiles, + skippedByReason)) { + appliedRemediations++; + + for (RemediationKey key : remediationKeys) { + remediationLookup.put(key, instanceId); + } } } @@ -195,18 +246,23 @@ public RemediationMetric processRemediationXML() { throw new AviatorTechnicalException("Error processing remediation.xml file.", e); } catch (AviatorTechnicalException e) { throw e; + } catch (Exception e) { LOG.error("Unexpected error processing remediation.xml: {}", remediationPath, e); throw new AviatorTechnicalException("Unexpected error processing remediations.xml.", e); } + int skippedRemediations = totalRemediations - appliedRemediations; - LOG.info("Auto-remediation summary: total={}, applied={}, skipped={}", totalRemediations, appliedRemediations, skippedRemediations); + LOG.info("Auto-remediation summary: total={}, applied={},indentical={},skipped={}", totalRemediations, appliedRemediations, identicalRemediations,skippedRemediations); + if (!skippedByReason.isEmpty()) { - LOG.info("Skipped remediations by reason: {}", formatSkippedReasons(skippedByReason)); + LOG.info( + "Skipped remediations by reason: {}",formatSkippedReasons(skippedByReason)); } return new RemediationMetric(totalRemediations, appliedRemediations, skippedRemediations, modifiedFiles, skippedByReason); } + private boolean processRemediation(Element remediation, Path sourceBasePath, FVDLMetadata fvdlMetadata, Set modifiedFiles, Map skippedByReason) { String instanceId = remediation.getAttribute("instanceId"); @@ -577,4 +633,179 @@ private String formatSkippedReasons(Map skippedByReason) { skippedByReason.forEach((reason, count) -> parts.add(reason + "=" + count)); return String.join(", ", parts); } + + private RemediationKey createRemediationKey( + Element fileChanges, + Element change, + Path sourceBasePath, + String comparisonCode) { + + String fileName = getRequiredElementText(fileChanges, "Filename"); + Path filePath = sourceBasePath.resolve(fileName).normalize(); + + int lineFrom = parseRequiredInt(change, "LineFrom"); + int lineTo = parseRequiredInt(change, "LineTo"); + + return new RemediationKey( + fileName, + filePath, + lineFrom, + lineTo, + comparisonCode + ); + } + + + private String trimBlankLines(String content) { + String[] lines = content.split("\\R", -1); + + int start = 0; + int end = lines.length - 1; + + while (start <= end && lines[start].isBlank()) { + start++; + } + + while (end >= start && lines[end].isBlank()) { + end--; + } + + if (start > end) { + return ""; + } + + return String.join( + System.lineSeparator(), + Arrays.copyOfRange(lines, start, end + 1)); + } + + private String normalizeProposedCode(String content, String fileName) { + if (content == null) { + return null; + } + + String language = FileTypeLanguageMapperUtil.getProgrammingLanguage( + FileUtil.getFileExtension(fileName)); + + String commentSymbol = + LanguageCommentMapperUtil.getProgrammingLanguageComment(language); + + if ("Unknown".equals(commentSymbol)) { + return trimBlankLines(content); + } + + String closingToken = commentSymbol.equals("" + : commentSymbol.equals("<%--") ? "--%>" + : null; + + Pattern markerPattern = Pattern.compile( + "[ \\t]*" + Pattern.quote(commentSymbol) + " L\\d+" + + (closingToken != null + ? "[ \\t]*" + Pattern.quote(closingToken) + : "") + + "[ \\t]*$"); + + String[] lines = content.split("\\R", -1); + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < lines.length; i++) { + Matcher matcher = markerPattern.matcher(lines[i]); + + result.append( + matcher.find() + ? lines[i].substring(0, matcher.start()) + : lines[i]); + + if (i < lines.length - 1) { + result.append(System.lineSeparator()); + } + } + + return trimBlankLines(result.toString()); + } + + private String createComparisonCode(String normalizedCode, String fileName) { + if (normalizedCode == null) { + return null; + } + + String language = FileTypeLanguageMapperUtil.getProgrammingLanguage( + FileUtil.getFileExtension(fileName)); + + String commentSymbol = + LanguageCommentMapperUtil.getProgrammingLanguageComment(language); + + if ("Unknown".equals(commentSymbol)) { + return normalizedCode.replaceAll("\\s+", ""); + } + + String comparisonCode = normalizedCode; + + // Remove block comments + String closingToken = commentSymbol.equals("" + : commentSymbol.equals("<%--") ? "--%>" + : null; + + if (closingToken != null) { + comparisonCode = comparisonCode.replaceAll( + "(?s)" + Pattern.quote(commentSymbol) + + ".*?" + Pattern.quote(closingToken), + ""); + } else if ("//".equals(commentSymbol)) { + comparisonCode = comparisonCode.replaceAll( + "(?m)" + Pattern.quote(commentSymbol) + ".*$", + ""); + comparisonCode = comparisonCode.replaceAll( + "(?s)/\\*.*?\\*/", + ""); + } else if ("#".equals(commentSymbol)) { + comparisonCode = comparisonCode.replaceAll( + "(?m)" + Pattern.quote(commentSymbol) + ".*$", + ""); + } + + // Normalize whitespace + return comparisonCode.replaceAll("\\s+", ""); + } + + private List createRemediationKeys( + Element remediation, + Path sourceBasePath) { + + List keys = new ArrayList<>(); + + NodeList fileChangesNodes = + remediation.getElementsByTagNameNS(NAMESPACE_URI, "FileChanges"); + + for (int i = 0; i < fileChangesNodes.getLength(); i++) { + Element fileChanges = (Element) fileChangesNodes.item(i); + + NodeList changeNodes = + fileChanges.getElementsByTagNameNS(NAMESPACE_URI, "Change"); + + for (int j = 0; j < changeNodes.getLength(); j++) { + Element change = (Element) changeNodes.item(j); + + String fileName = + getRequiredElementText(fileChanges, "Filename"); + + String newCode = + getRequiredElementText(change, "NewCode"); + + String normalizedCode = + normalizeProposedCode(newCode, fileName); + + String comparisonCode = + createComparisonCode(normalizedCode, fileName); + + keys.add(createRemediationKey( + fileChanges, + change, + sourceBasePath, + comparisonCode)); + } + } + + return keys; + } }