diff --git a/backend/Dockerfile b/backend/Dockerfile index 37b89ea6..81312732 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -105,23 +105,25 @@ RUN wget https://github.com/zaproxy/zaproxy/releases/download/v2.16.1/ZAP_2.16.1 # Rest of the Dockerfile remains unchanged RUN mkdir /opt/kics -RUN mkdir /opt/dtrack +#RUN mkdir /opt/dtrack COPY --from=kics /app/bin/kics /usr/local/bin/kics COPY --from=kics /app/bin/assets /opt/tools/kics/assets RUN rm -rf /opt/tools/kics/assets/queries/openAPI RUN rm -rf /opt/tools/kics/assets/queries/common/passwords_and_secrets -# Setup Dependency-Track -RUN wget https://github.com/DependencyTrack/dependency-track/releases/download/4.13.6/dependency-track-bundled.jar -P /opt/dtrack/ +## Setup Dependency-Track +#RUN wget https://github.com/DependencyTrack/dependency-track/releases/download/4.13.6/dependency-track-bundled.jar -P /opt/dtrack/ # Download and install the appropriate gitleaks for the architecture RUN ARCH=$(uname -m) && \ if [ "$ARCH" = "x86_64" ]; then \ wget -O gitleaks.tar.gz https://github.com/gitleaks/gitleaks/releases/download/v8.12.0/gitleaks_8.12.0_linux_x64.tar.gz; \ wget -O bearer.tar.gz https://github.com/Bearer/bearer/releases/download/v1.50.2/bearer_1.50.2_linux_amd64.tar.gz; \ + wget -O grype.tar.gz https://github.com/anchore/grype/releases/download/v0.104.2/grype_0.104.2_linux_amd64.tar.gz; \ elif [ "$ARCH" = "aarch64" ]; then \ wget -O gitleaks.tar.gz https://github.com/gitleaks/gitleaks/releases/download/v8.12.0/gitleaks_8.12.0_linux_arm64.tar.gz; \ wget -O bearer.tar.gz https://github.com/Bearer/bearer/releases/download/v1.50.2/bearer_1.50.2_linux_arm64.tar.gz; \ + wget -O grype.tar.gz https://github.com/anchore/grype/releases/download/v0.104.2/grype_0.104.2_linux_arm64.tar.gz; \ else \ echo "Unsupported architecture: $ARCH"; \ exit 1; \ @@ -131,7 +133,10 @@ RUN ARCH=$(uname -m) && \ rm gitleaks.tar.gz && \ tar -xzf bearer.tar.gz && \ mv bearer /usr/local/bin/bearer && \ - rm bearer.tar.gz + rm bearer.tar.gz && \ + tar -xzf grype.tar.gz && \ + mv grype /usr/local/bin/grype && \ + rm grype.tar.gz # Create directory for Bearer rules RUN mkdir -p /opt/bearer diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index 7bd3becf..ba70bc17 100644 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -30,16 +30,16 @@ else SPRING_PROFILE="prod" fi -# Start Dependency-Track in the background with 4GB of memory and log output to a file -LOG_FILE="/var/log/dtrack.log" -echo "Starting Dependency-Track..." -if [ -n "$PROXY_HOST" ] && [ -n "$PROXY_PORT" ]; then - java -Xmx4g -Dhttp.proxyHost=$PROXY_HOST -Dhttp.proxyPort=$PROXY_PORT -Dhttps.proxyHost=$PROXY_HOST -Dhttps.proxyPort=$PROXY_PORT -Dcom.sun.net.ssl.checkRevocation=false -Djavax.net.ssl.trustAll=true -Djavax.net.ssl.trustStore=/dev/null -Djavax.net.ssl.trustAll=true -Djavax.net.ssl.verifyHostname=false -jar /opt/dtrack/dependency-track-bundled.jar >> $LOG_FILE 2>&1 & -else - java -Xmx4g -jar /opt/dtrack/dependency-track-bundled.jar >> $LOG_FILE 2>&1 & -fi - -sleep 30 +## Start Dependency-Track in the background with 4GB of memory and log output to a file +#LOG_FILE="/var/log/dtrack.log" +#echo "Starting Dependency-Track..." +#if [ -n "$PROXY_HOST" ] && [ -n "$PROXY_PORT" ]; then +# java -Xmx4g -Dhttp.proxyHost=$PROXY_HOST -Dhttp.proxyPort=$PROXY_PORT -Dhttps.proxyHost=$PROXY_HOST -Dhttps.proxyPort=$PROXY_PORT -Dcom.sun.net.ssl.checkRevocation=false -Djavax.net.ssl.trustAll=true -Djavax.net.ssl.trustStore=/dev/null -Djavax.net.ssl.trustAll=true -Djavax.net.ssl.verifyHostname=false -jar /opt/dtrack/dependency-track-bundled.jar >> $LOG_FILE 2>&1 & +#else +# java -Xmx4g -jar /opt/dtrack/dependency-track-bundled.jar >> $LOG_FILE 2>&1 & +#fi +# +#sleep 30 # Start ZAP daemon ZAP_LOG_FILE="/var/log/zap.log" diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/api/auth/controller/AuthController.java b/backend/src/main/java/io/mixeway/mixewayflowapi/api/auth/controller/AuthController.java index b697f423..42003af7 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/api/auth/controller/AuthController.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/api/auth/controller/AuthController.java @@ -116,7 +116,7 @@ public ResponseEntity status() { profile = activeProfiles[0]; } try { - return new ResponseEntity<>(new ServiceStatusDTO(profile, appConfigService.isSaasMode() ? "SAAS":"SANDALONE"), HttpStatus.OK); + return new ResponseEntity<>(new ServiceStatusDTO(profile, appConfigService.isSaasMode() ? "SAAS":"STANDALONE"), HttpStatus.OK); } catch (Exception e){ throw new RuntimeException(e); } diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/api/gitlabcicd/service/GitLabCICDService.java b/backend/src/main/java/io/mixeway/mixewayflowapi/api/gitlabcicd/service/GitLabCICDService.java index fcd3751e..2bfb90eb 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/api/gitlabcicd/service/GitLabCICDService.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/api/gitlabcicd/service/GitLabCICDService.java @@ -9,6 +9,7 @@ import io.mixeway.mixewayflowapi.db.repository.CodeRepoBranchRepository; import io.mixeway.mixewayflowapi.db.repository.UserRepository; import io.mixeway.mixewayflowapi.domain.coderepo.FindCodeRepoService; +import io.mixeway.mixewayflowapi.domain.coderepobranch.GetOrCreateCodeRepoBranchService; import io.mixeway.mixewayflowapi.domain.finding.FindFindingService; import io.mixeway.mixewayflowapi.domain.team.FindTeamService; import io.mixeway.mixewayflowapi.scanmanager.service.ScanManagerService; @@ -30,6 +31,7 @@ public class GitLabCICDService { private final CodeRepoBranchRepository codeRepoBranchRepository; private final ScanManagerService scanManagerService; private final FindingService findingService; + GetOrCreateCodeRepoBranchService getOrCreateCodeRepoBranchService; public Boolean isValidApiKey(String apiKey, String repoUrl) { Optional userOptional = userRepository.findByApiKey(apiKey); @@ -54,7 +56,7 @@ public CodeRepo getCodeRepo(String repoUrl) { } public CodeRepoBranch getCodeRepoBranch(String branch, CodeRepo codeRepo) { - return codeRepoBranchRepository.findByNameAndCodeRepo(branch, codeRepo).get(); + return getOrCreateCodeRepoBranchService.getOrCreateCodeRepoBranch(branch, codeRepo); } public void runCodeRepoScan(CodeRepo codeRepo, CodeRepoBranch codeRepoBranch) { diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/domain/coderepo/UpdateCodeRepoService.java b/backend/src/main/java/io/mixeway/mixewayflowapi/domain/coderepo/UpdateCodeRepoService.java index 34698b02..547fcc4c 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/domain/coderepo/UpdateCodeRepoService.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/domain/coderepo/UpdateCodeRepoService.java @@ -29,6 +29,7 @@ public class UpdateCodeRepoService { private final FindingRepository findingRepository; private final CreateScanInfoService createScanInfoService; private final FindCodeRepoService findCodeRepoService; + private boolean scaScanPerformed; /** * Updates the SCA UUID for a given {@link CodeRepo}. @@ -59,11 +60,10 @@ public void updateComponents(List components, CodeRepo codeRepo) { * * @param codeRepo the {@link CodeRepo} entity to update * @param codeRepoBranch the {@link CodeRepoBranch} entity associated with the code repository - * @param scaScanPerformed a boolean indicating if an SCA scan was performed * @param commitId the commit ID associated with the scan */ @Transactional - public void updateCodeRepoStatus(CodeRepo codeRepo, CodeRepoBranch codeRepoBranch, boolean scaScanPerformed, String commitId) { + public void updateCodeRepoStatus(CodeRepo codeRepo, CodeRepoBranch codeRepoBranch, String commitId) { codeRepo = findCodeRepoService.findById(codeRepo.getId()).get(); if (codeRepoBranch == null) { codeRepoBranch = codeRepo.getDefaultBranch(); @@ -117,6 +117,63 @@ public void updateCodeRepoStatus(CodeRepo codeRepo, CodeRepoBranch codeRepoBranc countCriticalFindings(Finding.Source.DAST, codeRepo, codeRepoBranch) ); } +// Dependency Track version +// @Transactional +// public void updateCodeRepoStatus(CodeRepo codeRepo, CodeRepoBranch codeRepoBranch, boolean scaScanPerformed, String commitId) { +// this.scaScanPerformed = scaScanPerformed; +// codeRepo = findCodeRepoService.findById(codeRepo.getId()).get(); +// if (codeRepoBranch == null) { +// codeRepoBranch = codeRepo.getDefaultBranch(); +// } +// // Update status for SECRETS +// int secretsHigh = updateStatusForSource(Finding.Source.SECRETS, codeRepo, codeRepoBranch, false); +// +// // Update status for SAST +// int sastHigh = updateStatusForSource(Finding.Source.SAST, codeRepo, codeRepoBranch, false); +// +// // Update status for IaC +// int iacHigh = updateStatusForSource(Finding.Source.IAC, codeRepo, codeRepoBranch, false); +// +// int gitlabHigh = updateStatusForSource(Finding.Source.GITLAB_SCANNER, codeRepo, codeRepoBranch, false); +// int dastHigh = updateStatusForSource(Finding.Source.DAST, codeRepo, codeRepoBranch, false); +// +// // Initialize SCA counts +// int scaHigh = 0; +// int scaCritical = 0; +// +// // Update status for SCA if the scan was performed +// if (!codeRepo.getComponents().isEmpty()) { +// scaHigh = updateStatusForSource(Finding.Source.SCA, codeRepo, codeRepoBranch, false); +// scaCritical = countCriticalFindings(Finding.Source.SCA, codeRepo, codeRepoBranch); +// } else { +// scaHigh = updateStatusForSource(Finding.Source.SCA, codeRepo, codeRepoBranch, true); +// scaCritical = countCriticalFindings(Finding.Source.SCA, codeRepo, codeRepoBranch); +// } +// +// // Create or update ScanInfo snapshot +// createScanInfoService.createOrUpdateScanInfo( +// codeRepo, +// codeRepoBranch, +// commitId, +// codeRepo.getScaScan(), +// codeRepo.getSastScan(), +// codeRepo.getIacScan(), +// codeRepo.getSecretsScan(), +// codeRepo.getGitlabScan(), +// scaHigh, +// scaCritical, +// sastHigh, +// countCriticalFindings(Finding.Source.SAST, codeRepo, codeRepoBranch), +// iacHigh, +// countCriticalFindings(Finding.Source.IAC, codeRepo, codeRepoBranch), +// secretsHigh, +// countCriticalFindings(Finding.Source.SECRETS, codeRepo, codeRepoBranch), +// gitlabHigh, +// countCriticalFindings(Finding.Source.GITLAB_SCANNER, codeRepo, codeRepo.getDefaultBranch()), +// dastHigh, +// countCriticalFindings(Finding.Source.DAST, codeRepo, codeRepoBranch) +// ); +// } /** * Updates the scan status for a specific source (SAST, SCA, IaC, Secrets) based on findings. diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/domain/finding/CreateFindingService.java b/backend/src/main/java/io/mixeway/mixewayflowapi/domain/finding/CreateFindingService.java index a5dc687c..4278d5ee 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/domain/finding/CreateFindingService.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/domain/finding/CreateFindingService.java @@ -1,7 +1,10 @@ package io.mixeway.mixewayflowapi.domain.finding; import io.mixeway.mixewayflowapi.db.entity.*; +import io.mixeway.mixewayflowapi.db.repository.CodeRepoRepository; import io.mixeway.mixewayflowapi.db.repository.FindingRepository; +import io.mixeway.mixewayflowapi.domain.coderepo.UpdateCodeRepoService; +import io.mixeway.mixewayflowapi.domain.component.GetOrCreateComponentService; import io.mixeway.mixewayflowapi.domain.suppressrule.CheckSuppressRuleService; import io.mixeway.mixewayflowapi.domain.vulnerability.GetOrCreateVulnerabilityService; import io.mixeway.mixewayflowapi.integrations.scanner.cloud_scanner.dto.CloudIssueReport; @@ -9,12 +12,15 @@ import io.mixeway.mixewayflowapi.integrations.scanner.iac.dto.KicsReport; import io.mixeway.mixewayflowapi.integrations.scanner.sast.dto.BearerScanSecurity; import io.mixeway.mixewayflowapi.integrations.scanner.sast.dto.Item; +import io.mixeway.mixewayflowapi.integrations.scanner.sca.dto.GrypeReport; import io.mixeway.mixewayflowapi.integrations.scanner.secrets.dto.Secret; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.hibernate.Hibernate; import org.springframework.stereotype.Service; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -28,6 +34,9 @@ public class CreateFindingService { private final FindingRepository findingRepository; private final GetOrCreateVulnerabilityService getOrCreateVulnerabilityService; private final CheckSuppressRuleService checkSuppressRuleService; + private final GetOrCreateComponentService getOrCreateComponentService; + private final UpdateCodeRepoService updateCodeRepoService; + private final CodeRepoRepository codeRepoRepository; @Transactional public void saveFindings(List newFindings, CodeRepoBranch repoWhereFindingWasFound, CodeRepo repoInWhichFindingWasFound, Finding.Source source, CloudSubscription cloudSubscription) { @@ -289,6 +298,88 @@ public List mapBearerScanToFindings(BearerScanSecurity scanSecurity, Co return findings; } + @Transactional + public void processGrypeComponents(GrypeReport grypeReport, CodeRepo codeRepo) { + CodeRepo managedRepo = codeRepoRepository.findById(codeRepo.getId()) + .orElseThrow(() -> new IllegalArgumentException("CodeRepo not found")); + + List components = grypeReport.getMatches().stream() + .map(match -> getOrCreateComponentService.getOrCreate( + match.getArtifact().getName(), + match.getArtifact().getType(), + match.getArtifact().getVersion(), + "nvd" + )) + .distinct() + .toList(); + + updateCodeRepoService.updateComponents(components, managedRepo); + } + + + @Transactional + public List mapGrypeReportToFindings(GrypeReport grypeReport, CodeRepo codeRepo, CodeRepoBranch codeRepoBranch) { + List findings = new ArrayList<>(); + + for (GrypeReport.Match match : grypeReport.getMatches()) { + GrypeReport.Vulnerability vuln = match.getVulnerability(); + GrypeReport.Artifact artifact = match.getArtifact(); + + Component component = getOrCreateComponentService.getOrCreate( + artifact.getName(), + artifact.getType(), + artifact.getVersion(), + "nvd" + ); + + BigDecimal epssProbability = null; + BigDecimal epssPercentile = null; + if (vuln.getEpss() != null && !vuln.getEpss().isEmpty()) { + epssProbability = BigDecimal.valueOf(vuln.getEpss().get(0).getEpss()); + epssPercentile = BigDecimal.valueOf(vuln.getEpss().get(0).getPercentile()); + } + + String recommendation = null; + if (vuln.getFix() != null && vuln.getFix().getVersions() != null && !vuln.getFix().getVersions().isEmpty()) { + recommendation = "Update package to version " + vuln.getFix().getVersions().get(0); + } + + Vulnerability vulnerability = getOrCreateVulnerabilityService.getOrCreate( + vuln.getId(), + vuln.getDescription(), + vuln.getDataSource(), + recommendation, + mapSeverity(vuln.getSeverity()), + epssProbability, + epssPercentile, + null + ); + + Hibernate.initialize(vulnerability.getComponents()); + if (!vulnerability.getComponents().contains(component)) { + vulnerability.getComponents().add(component); + } + + String location = (artifact.getName() != null ? artifact.getName() : "") + + (artifact.getVersion() != null ? ":" + artifact.getVersion() : ""); + + Finding finding = new Finding( + vulnerability, + component, + codeRepoBranch, + codeRepo, + null, + vuln.getDescription(), + location, + mapSeverity(vuln.getSeverity()), + Finding.Source.SCA + ); + findings.add(finding); + } + + return findings; + } + private List mapItemsToFindings(List items, CodeRepoBranch codeRepoBranch, CodeRepo codeRepo, Finding.Severity severity) { return items.stream().map(item -> { Vulnerability vulnerability = getOrCreateVulnerabilityService.getOrCreate(item.getTitle(), item.getDescription(), null, item.getDocumentationUrl(), null, null, null, null); diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/integrations/scanner/cloud_scanner/service/CloudIssueService.java b/backend/src/main/java/io/mixeway/mixewayflowapi/integrations/scanner/cloud_scanner/service/CloudIssueService.java index 859a22f9..510ffbe4 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/integrations/scanner/cloud_scanner/service/CloudIssueService.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/integrations/scanner/cloud_scanner/service/CloudIssueService.java @@ -24,6 +24,7 @@ public CloudIssueReport runCloudIssueScanner(String projectId, String wizAuthTok ObjectMapper objectMapper = new ObjectMapper(); String cloudIssueJSONReport = fetchCloudIssues(projectId, wizAuthToken); CloudIssueReport cloudIssueReport = objectMapper.readValue(cloudIssueJSONReport, CloudIssueReport.class); + return cloudIssueReport; } diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/integrations/scanner/sca/dto/GrypeReport.java b/backend/src/main/java/io/mixeway/mixewayflowapi/integrations/scanner/sca/dto/GrypeReport.java new file mode 100644 index 00000000..e188fc24 --- /dev/null +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/integrations/scanner/sca/dto/GrypeReport.java @@ -0,0 +1,206 @@ +package io.mixeway.mixewayflowapi.integrations.scanner.sca.dto; + +import java.util.List; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class GrypeReport { + @JsonProperty("matches") + private List matches; + + @Data + public static class Match { + @JsonProperty("vulnerability") + private Vulnerability vulnerability; + + @JsonProperty("artifact") + private Artifact artifact; + + @JsonProperty("matchDetails") + private List matchDetails; + } + + @Data + public static class Vulnerability { + @JsonProperty("id") + private String id; + + @JsonProperty("dataSource") + private String dataSource; + + @JsonProperty("namespace") + private String namespace; + + @JsonProperty("severity") + private String severity; + + @JsonProperty("description") + private String description; + + @JsonProperty("cvss") + private List cvss; + + @JsonProperty("epss") + private List epss; + + @JsonProperty("cwes") + private List cwes; + + @JsonProperty("fix") + private Fix fix; + + @JsonProperty("risk") + private double risk; + } + + @Data + public static class Cvss { + @JsonProperty("type") + private String type; + + @JsonProperty("version") + private String version; + + @JsonProperty("vector") + private String vector; + + @JsonProperty("metrics") + private Metrics metrics; + + @Data + public static class Metrics { + @JsonProperty("baseScore") + private double baseScore; + + @JsonProperty("exploitabilityScore") + private double exploitabilityScore; + + @JsonProperty("impactScore") + private double impactScore; + } + } + + @Data + public static class Epss { + @JsonProperty("cve") + private String cve; + + @JsonProperty("epss") + private double epss; + + @JsonProperty("percentile") + private double percentile; + + @JsonProperty("date") + private String date; + } + + @Data + public static class Cwe { + @JsonProperty("cve") + private String cve; + + @JsonProperty("cwe") + private String cwe; + + @JsonProperty("source") + private String source; + + @JsonProperty("type") + private String type; + } + + @Data + public static class Fix { + @JsonProperty("versions") + private List versions; + + @JsonProperty("state") + private String state; + + @JsonProperty("available") + private List available; + + @Data + public static class AvailableFix { + @JsonProperty("version") + private String version; + + @JsonProperty("date") + private String date; + + @JsonProperty("kind") + private String kind; + } + } + + @Data + public static class Artifact { + @JsonProperty("id") + private String id; + + @JsonProperty("name") + private String name; + + @JsonProperty("version") + private String version; + + @JsonProperty("type") + private String type; + + @JsonProperty("language") + private String language; + + @JsonProperty("purl") + private String purl; + } + + @Data + public static class MatchDetail { + @JsonProperty("type") + private String type; + + @JsonProperty("matcher") + private String matcher; + + @JsonProperty("searchedBy") + private SearchedBy searchedBy; + + @JsonProperty("found") + private Found found; + + @JsonProperty("fix") + private Fix fix; + + @Data + public static class SearchedBy { + @JsonProperty("language") + private String language; + + @JsonProperty("namespace") + private String namespace; + + @JsonProperty("package") + private PackageInfo pkg; + + @Data + public static class PackageInfo { + @JsonProperty("name") + private String name; + + @JsonProperty("version") + private String version; + } + } + + @Data + public static class Found { + @JsonProperty("vulnerabilityID") + private String vulnerabilityID; + + @JsonProperty("versionConstraint") + private String versionConstraint; + } + } +} diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/integrations/scanner/sca/service/SCAGrypeService.java b/backend/src/main/java/io/mixeway/mixewayflowapi/integrations/scanner/sca/service/SCAGrypeService.java new file mode 100644 index 00000000..b38e1293 --- /dev/null +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/integrations/scanner/sca/service/SCAGrypeService.java @@ -0,0 +1,129 @@ +package io.mixeway.mixewayflowapi.integrations.scanner.sca.service; + +import ch.qos.logback.core.spi.ScanException; +import com.fasterxml.jackson.core.JsonParseException; +import io.mixeway.mixewayflowapi.db.entity.CodeRepo; +import io.mixeway.mixewayflowapi.db.entity.CodeRepoBranch; +import io.mixeway.mixewayflowapi.db.entity.Finding; +import io.mixeway.mixewayflowapi.domain.finding.CreateFindingService; +import io.mixeway.mixewayflowapi.integrations.scanner.sca.dto.GrypeReport; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +@Service +@Log4j2 +@RequiredArgsConstructor +public class SCAGrypeService { + private final CdxGenService cdxGenService; + private final ObjectMapper objectMapper; + private final CreateFindingService createFindingService; + + private File findSbom(String dir) { + File directory = new File(dir); + + if (directory.isDirectory()) { + File[] files = directory.listFiles(); + + if (files != null) { + for (File file : files) { + if (file.isFile() && "sbom.json".equals(file.getName())) { + return file; + } + } + } + } + return null; + } + + public void runGrype(String repoDir, CodeRepo codeRepo, CodeRepoBranch codeRepoBranch) throws IOException, InterruptedException, ScanException { + File sbomFile = findSbom(repoDir); + if (sbomFile == null) { + cdxGenService.generateBom(repoDir,codeRepo,codeRepoBranch); + sbomFile = findSbom(repoDir); + } + + log.info("[GrypeService] Starting Grype scan for repository: {} branch: {}", codeRepo.getName(), codeRepoBranch.getName()); + File grypeReportFile = new File(repoDir, "grype_report.json"); + + ProcessBuilder pb = new ProcessBuilder("grype", "sbom:" + sbomFile.getAbsolutePath(), "--by-cve", "-o", "json", "--file", grypeReportFile.getAbsolutePath()); + pb.directory(new File(repoDir)); + pb.redirectOutput(ProcessBuilder.Redirect.PIPE); + pb.redirectError(ProcessBuilder.Redirect.PIPE); + + Process process = pb.start(); + + ExecutorService executorService = Executors.newFixedThreadPool(2); + + try { + // Consume standard output stream silently + executorService.submit(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + while (!Thread.currentThread().isInterrupted()) { + String line = reader.readLine(); + if (line == null) break; + // Silently consume the output + } + } catch (IOException e) { + log.error("Error reading output stream", e); + } + }); + + // Consume error stream silently + executorService.submit(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + while (!Thread.currentThread().isInterrupted()) { + String line = reader.readLine(); + if (line == null) break; + // Silently consume the error stream + } + } catch (IOException e) { + log.error("[GrypeService] Error reading error stream", e); + } + }); + + // Wait for the process to finish with a timeout + boolean finished = process.waitFor(30, TimeUnit.MINUTES); + if (!finished) { + log.warn("[GrypeService] Grype scan did not finish within 20 minutes. Terminating process."); + process.destroyForcibly(); // Terminate the process + process.waitFor(); // Wait for the process to terminate + } + } finally { + executorService.shutdownNow(); // Ensure executor service is properly shut down + if (process.isAlive()) { + process.destroyForcibly(); + } + } + + if (!grypeReportFile.exists()) { + throw new ScanException("[GrypeService] Grype scan did not produce a report file."); + } + + log.info("[GrypeService] Finished scan, starting processing... - [{} / {}]", codeRepo.getRepourl(), codeRepoBranch.getName()); + + try { + GrypeReport grypeReport = objectMapper.readValue(grypeReportFile, GrypeReport.class); + + createFindingService.processGrypeComponents(grypeReport, codeRepo); + + List findings = createFindingService.mapGrypeReportToFindings(grypeReport, codeRepo, codeRepoBranch); + + createFindingService.saveFindings(findings, codeRepoBranch, codeRepo, Finding.Source.SCA, null); + + log.info("[GrypeService] Scan results processed successfully - [{} / {}]", codeRepo.getRepourl(), codeRepoBranch.getName()); + } catch (JsonParseException e) { + log.warn("[GrypeService] Error with running scan for repository - [{} / {}]", codeRepo.getRepourl(), codeRepoBranch.getName()); + } + } +} diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/scanmanager/scheduler/ScanScheduler.java b/backend/src/main/java/io/mixeway/mixewayflowapi/scanmanager/scheduler/ScanScheduler.java index 47355d09..4ffb9456 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/scanmanager/scheduler/ScanScheduler.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/scanmanager/scheduler/ScanScheduler.java @@ -49,10 +49,10 @@ public class ScanScheduler { * This method is executed automatically after the bean's properties have been set. * */ - @PostConstruct - public void runAfterStartup() { - scaService.initialize(); - } +// @PostConstruct +// public void runAfterStartup() { +// scaService.initialize(); +// } @Scheduled(cron = "0 0 1 * * ?") // Every day at 1:00 AM diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/scanmanager/service/ScanManagerService.java b/backend/src/main/java/io/mixeway/mixewayflowapi/scanmanager/service/ScanManagerService.java index ea51662d..6ec3968a 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/scanmanager/service/ScanManagerService.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/scanmanager/service/ScanManagerService.java @@ -25,6 +25,7 @@ import io.mixeway.mixewayflowapi.integrations.scanner.sca.apiclient.KEVApiClient; import io.mixeway.mixewayflowapi.integrations.scanner.sca.dto.CatalogDto; import io.mixeway.mixewayflowapi.integrations.scanner.sca.dto.VulnerabilityDto; +import io.mixeway.mixewayflowapi.integrations.scanner.sca.service.SCAGrypeService; import io.mixeway.mixewayflowapi.integrations.scanner.sca.service.SCAService; import io.mixeway.mixewayflowapi.integrations.scanner.secrets.service.SecretsService; import io.mixeway.mixewayflowapi.integrations.scanner.zap.service.ZAPService; @@ -99,7 +100,7 @@ public class ScanManagerService { .expireAfterWrite(10, TimeUnit.MINUTES) .build(); private final GitLabScannerService gitLabScannerService; - + private final SCAGrypeService sCAGrypeService; /** @@ -146,7 +147,7 @@ public void scanRepository(CodeRepo codeRepo, CodeRepoBranch codeRepoBranch, Str String repoDir = "/tmp/" + codeRepo.getName(); String commit = ""; - AtomicBoolean scaScanPerformed = new AtomicBoolean(false); +// AtomicBoolean scaScanPerformed = new AtomicBoolean(false); Future timeoutFuture = null; try { @@ -163,7 +164,8 @@ public void scanRepository(CodeRepo codeRepo, CodeRepoBranch codeRepoBranch, Str // Run scans in parallel Future secretScanFuture = runSecretScan(repoDir, codeRepo, codeRepoBranch); - Future scaScanFuture = runSCAScan(repoDir, codeRepo, codeRepoBranch, scaScanPerformed); +// Future scaScanFuture = runSCAScan(repoDir, codeRepo, codeRepoBranch, scaScanPerformed); Dependency Track version + Future scaScanFuture = runSCAScan(repoDir, codeRepo, codeRepoBranch); Future sastScanFuture = runSASTScan(repoDir, codeRepo, codeRepoBranch); Future iacScanFuture = runIACScan(repoDir, codeRepo, codeRepoBranch); Future gitlabScanFuture = null; @@ -213,7 +215,8 @@ public void scanRepository(CodeRepo codeRepo, CodeRepoBranch codeRepoBranch, Str } finally { // Update status try { - updateCodeRepoService.updateCodeRepoStatus(codeRepo, codeRepoBranch, scaScanPerformed.get(), commit); +// updateCodeRepoService.updateCodeRepoStatus(codeRepo, codeRepoBranch, scaScanPerformed.get(), commit); + updateCodeRepoService.updateCodeRepoStatus(codeRepo, codeRepoBranch, commit); } catch (Exception updateEx) { log.error("[ScanManagerService] Failed to update CodeRepo status for {}: {}", codeRepo.getName(), updateEx.getMessage(), updateEx); } @@ -273,7 +276,7 @@ public void scanRepositoryViaGitLabCICD(CodeRepo codeRepo, CodeRepoBranch codeRe String repoDir = "/tmp/" + codeRepo.getName(); String commit = ""; - AtomicBoolean scaScanPerformed = new AtomicBoolean(false); +// AtomicBoolean scaScanPerformed = new AtomicBoolean(false); Future timeoutFuture = null; try { @@ -290,7 +293,8 @@ public void scanRepositoryViaGitLabCICD(CodeRepo codeRepo, CodeRepoBranch codeRe // Run scans in parallel Future secretScanFuture = runSecretScan(repoDir, codeRepo, codeRepoBranch); - Future scaScanFuture = runSCAScan(repoDir, codeRepo, codeRepoBranch, scaScanPerformed); +// Future scaScanFuture = runSCAScan(repoDir, codeRepo, codeRepoBranch, scaScanPerformed); Dependency Track version + Future scaScanFuture = runSCAScan(repoDir, codeRepo, codeRepoBranch); Future sastScanFuture = runSASTScan(repoDir, codeRepo, codeRepoBranch); Future iacScanFuture = runIACScan(repoDir, codeRepo, codeRepoBranch); Future gitlabScanFuture = null; @@ -340,7 +344,8 @@ public void scanRepositoryViaGitLabCICD(CodeRepo codeRepo, CodeRepoBranch codeRe } finally { // Update status try { - updateCodeRepoService.updateCodeRepoStatus(codeRepo, codeRepoBranch, scaScanPerformed.get(), commit); +// updateCodeRepoService.updateCodeRepoStatus(codeRepo, codeRepoBranch, scaScanPerformed.get(), commit); Dependency Track version + updateCodeRepoService.updateCodeRepoStatus(codeRepo, codeRepoBranch, commit); } catch (Exception updateEx) { log.error("[ScanManagerService] Failed to update CodeRepo status for {}: {}", codeRepo.getName(), updateEx.getMessage(), updateEx); } @@ -390,14 +395,40 @@ private Future runSecretScan(String repoDir, CodeRepo codeRepo, CodeRepoBr return scanExecutorService.submit(task); } - private Future runSCAScan(String repoDir, CodeRepo codeRepo, CodeRepoBranch codeRepoBranch, AtomicBoolean scaScanPerformed) { +// Dependency Track version +// private Future runSCAScan(String repoDir, CodeRepo codeRepo, CodeRepoBranch codeRepoBranch, AtomicBoolean scaScanPerformed) { +// Callable task = () -> { +// int currentScaScans = scaScansRunning.incrementAndGet(); +// log.info("[ScanManagerService] Starting new SCA scan, parallel SCA scans running {}", currentScaScans); +// +// try { +// log.info("[ScanManagerService] Starting SCA scan... [for: {}]", repoDir); +// scaScanPerformed.set(scaService.runScan(repoDir, codeRepo, codeRepoBranch)); +// } catch (Exception e) { +// if (Thread.currentThread().isInterrupted()) { +// log.warn("[ScanManagerService] SCA scan interrupted for {}.", codeRepo.getRepourl()); +// Thread.currentThread().interrupt(); +// } else { +// log.error("[ScanManagerService] An error occurred during SCA scan for {} - {}.", codeRepo.getRepourl(), e.getLocalizedMessage()); +// e.printStackTrace(); +// } +// } finally { +// int remainingScaScans = scaScansRunning.decrementAndGet(); +// log.debug("[ScanManagerService] SCA scan completed, parallel SCA scans running {}", remainingScaScans); +// } +// return null; +// }; +// return scanExecutorService.submit(task); +// } + + private Future runSCAScan(String repoDir, CodeRepo codeRepo, CodeRepoBranch codeRepoBranch) { Callable task = () -> { int currentScaScans = scaScansRunning.incrementAndGet(); log.info("[ScanManagerService] Starting new SCA scan, parallel SCA scans running {}", currentScaScans); try { log.info("[ScanManagerService] Starting SCA scan... [for: {}]", repoDir); - scaScanPerformed.set(scaService.runScan(repoDir, codeRepo, codeRepoBranch)); + sCAGrypeService.runGrype(repoDir, codeRepo, codeRepoBranch); } catch (Exception e) { if (Thread.currentThread().isInterrupted()) { log.warn("[ScanManagerService] SCA scan interrupted for {}.", codeRepo.getRepourl()); diff --git a/backend/src/test/java/io/mixeway/mixewayflowapi/domain/coderepo/UpdateCodeRepoServiceTest.java b/backend/src/test/java/io/mixeway/mixewayflowapi/domain/coderepo/UpdateCodeRepoServiceTest.java index b5d34815..7ae734ef 100644 --- a/backend/src/test/java/io/mixeway/mixewayflowapi/domain/coderepo/UpdateCodeRepoServiceTest.java +++ b/backend/src/test/java/io/mixeway/mixewayflowapi/domain/coderepo/UpdateCodeRepoServiceTest.java @@ -114,7 +114,7 @@ void updateCodeRepoStatus() throws IOException, ScanException, InterruptedExcept CodeRepo codeRepo = findCodeRepoService.findByRemoteId(9999L); - updateCodeRepoService.updateCodeRepoStatus(codeRepo, codeRepo.getDefaultBranch(),true,"123"); + updateCodeRepoService.updateCodeRepoStatus(codeRepo, codeRepo.getDefaultBranch(),"123"); codeRepo = findCodeRepoService.findByRemoteId(9999L); assertFalse(codeRepo.getIacScan().equals(CodeRepo.ScanStatus.NOT_PERFORMED)); diff --git a/frontend/src/app/views/show-repo/show-repo.component.ts b/frontend/src/app/views/show-repo/show-repo.component.ts index 4a2c3e05..96c5b84f 100644 --- a/frontend/src/app/views/show-repo/show-repo.component.ts +++ b/frontend/src/app/views/show-repo/show-repo.component.ts @@ -554,6 +554,7 @@ export class ShowRepoComponent implements OnInit, AfterViewInit { '#3eabb7', '#FFCE12', '#FF8929D8', + '#C34E75F4' ], hoverBackgroundColor: [ '#FF6384', @@ -561,6 +562,7 @@ export class ShowRepoComponent implements OnInit, AfterViewInit { '#449a77', '#FFCE12', '#FF8929D8', + '#C34E75F4' ], }, ], diff --git a/frontend/src/app/views/show-repo/vulnerabilities-table/vulnerabilities-table.component.ts b/frontend/src/app/views/show-repo/vulnerabilities-table/vulnerabilities-table.component.ts index 770dbec0..320b1463 100644 --- a/frontend/src/app/views/show-repo/vulnerabilities-table/vulnerabilities-table.component.ts +++ b/frontend/src/app/views/show-repo/vulnerabilities-table/vulnerabilities-table.component.ts @@ -277,7 +277,7 @@ export class VulnerabilitiesTableComponent implements OnInit, OnChanges { * @param source The vulnerability source (e.g., 'SAST', 'SCA'). */ isLinkableSource(source: string): boolean { - const linkableSources = ['SAST', 'IAC', 'SECRETS', 'DAST']; + const linkableSources = ['SAST', 'IAC', 'DAST']; return linkableSources.includes(source); } @@ -288,27 +288,30 @@ export class VulnerabilitiesTableComponent implements OnInit, OnChanges { if (!row?.location) { return '#'; } - // For DAST, the location is a full URL and can be used directly. if (row.source === 'DAST') { return row.location.startsWith('http') ? row.location : `//${row.location}`; } - if (!this.repoData?.repourl) { + if (!this.repoData?.repourl || !this.repoData?.type) { return '#'; } + const location = row.location; const repoUrl = this.repoData.repourl; + const repoType = this.repoData.type.toUpperCase(); // Use the type property const branch = this.selectedBranch || this.repoData?.defaultBranch?.name; const match = location.match(/(.*):(\d+)/); - if (!match) return repoUrl; + if (!match) { + return repoUrl; + } const [, filePath, lineNumber] = match; + const baseUrl = repoUrl.replace(/\/$/, ''); - if (repoUrl.includes('github.com')) { - return `${repoUrl}/blob/${branch}/${filePath}#L${lineNumber}`; - } else if (repoUrl.includes('gitlab.com')) { - const baseUrl = repoUrl.replace(/\/?$/, ''); + if (repoType === 'GITHUB') { + return `${baseUrl}/blob/${branch}/${filePath}#L${lineNumber}`; + } else if (repoType === 'GITLAB') { return `${baseUrl}/-/blob/${branch}/${filePath}#L${lineNumber}`; } @@ -316,25 +319,48 @@ export class VulnerabilitiesTableComponent implements OnInit, OnChanges { } /** - * Get formatted location for a vulnerability row + * NEW: Gets the shortened display text for the location column. + */ + getShortenedLocationText(row: any): string { + if (!row?.location) { + return 'Location not available'; + } + + const fullLocation = row.location; + + // For these sources, don't shorten the path, just display it. + if (['DAST', 'SCA', 'GITLAB_SCANNER', 'SECRETS'].includes(row.source)) { + return fullLocation; + } + + // For other sources, shorten the path if it is too long. + const pathParts = fullLocation.split('/'); + if (pathParts.length > 4) { + // e.g., ".../path/to/file.txt:1" + return '...' + pathParts.slice(-3).join('/'); + } + + return fullLocation; + } + + /** + * UNCHANGED: Get formatted location for a vulnerability row. + * This is still used by the Excel export and should return the full path. */ getFormattedLocationForRow(row: any): string { if (!row?.location) { return 'Location not available'; } - // For these types, display the raw location string. - if (row.source === 'DAST' || row.source === 'SCA' || row.source === 'GITLAB_SCANNER') { + if (['DAST', 'SCA', 'GITLAB_SCANNER', 'SECRETS'].includes(row.source)) { return row.location; } - - // For SAST, IaC, Secrets, format it as path:line. const location = row.location; const match = location.match(/(.*):(\d+)/); if (!match) return location; - const [, filePath, lineNumber] = match; return `${filePath}:${lineNumber}`; } + // === XLSX Export === private formatDateForXlsx(d?: string | Date | null) { if (!d) return ''; diff --git a/frontend/src/app/views/show-repo/vulnerability-details/vulnerability-details.component.html b/frontend/src/app/views/show-repo/vulnerability-details/vulnerability-details.component.html index 89a31744..b342324a 100644 --- a/frontend/src/app/views/show-repo/vulnerability-details/vulnerability-details.component.html +++ b/frontend/src/app/views/show-repo/vulnerability-details/vulnerability-details.component.html @@ -12,17 +12,25 @@
{{ singleVuln?.vulnsResponseDto?.name }}
- Location: + Location: - + + - {{ getFormattedLocation() }} + + + {{ getFormattedLocationText() }} + - {{ getFormattedLocation() }} + {{ getFormattedLocationText() }}
diff --git a/frontend/src/app/views/show-repo/vulnerability-details/vulnerability-details.component.ts b/frontend/src/app/views/show-repo/vulnerability-details/vulnerability-details.component.ts index 931a73ae..7874ed60 100644 --- a/frontend/src/app/views/show-repo/vulnerability-details/vulnerability-details.component.ts +++ b/frontend/src/app/views/show-repo/vulnerability-details/vulnerability-details.component.ts @@ -154,49 +154,60 @@ export class VulnerabilityDetailsComponent { * @param source The vulnerability source (e.g., 'SAST', 'SCA'). */ isLinkableSource(source: string): boolean { - const linkableSources = ['SAST', 'IAC', 'SECRETS', 'DAST']; + const linkableSources = ['SAST', 'IAC', 'DAST']; return linkableSources.includes(source); } /** * Get link to the vulnerability in the repository */ + getRepositoryLink(): string { const finding = this.singleVuln?.vulnsResponseDto; if (!finding?.location) { return '#'; } - - // For DAST, the location is a full URL and can be used directly. if (finding.source === 'DAST') { return finding.location.startsWith('http') ? finding.location : `//${finding.location}`; } - - if (!this.repoData?.repourl) { + if (!this.repoData?.repourl || !this.repoData?.type) { return '#'; } - const location = finding.location; const repoUrl = this.repoData.repourl; + const repoType = this.repoData.type.toUpperCase(); const branch = this.selectedBranch || this.repoData?.defaultBranch?.name; - const match = location.match(/(.*):(\d+)/); - if (!match) return repoUrl; - + if (!match) { + return repoUrl; + } const [, filePath, lineNumber] = match; - - if (repoUrl.includes('github.com')) { - return `${repoUrl}/blob/${branch}/${filePath}#L${lineNumber}`; - } else if (repoUrl.includes('gitlab.com')) { - const baseUrl = repoUrl.replace(/\/?$/, ''); + const baseUrl = repoUrl.replace(/\/$/, ''); + if (repoType === 'GITHUB') { + return `${baseUrl}/blob/${branch}/${filePath}#L${lineNumber}`; + } else if (repoType === 'GITLAB') { return `${baseUrl}/-/blob/${branch}/${filePath}#L${lineNumber}`; } - return repoUrl; } /** - * Format the vulnerability location for display + * NEW: Gets the display text for the vulnerability location, shortening it if necessary. + * This function returns ONLY the text string to be displayed. + */ + getFormattedLocationText(): string { + const finding = this.singleVuln?.vulnsResponseDto; + if (!finding?.location) { + return 'Location not available'; + } + // Always return the full location string without shortening. + return finding.location; + } + + /** + * Format the vulnerability location for display. + * For SAST, SECRETS, and IAC, it returns an HTML anchor tag. + * Note: You must use [innerHTML] in your template to render this link. */ getFormattedLocation(): string { const finding = this.singleVuln?.vulnsResponseDto; @@ -204,16 +215,21 @@ export class VulnerabilityDetailsComponent { return 'Location not available'; } - // For these types, display the raw location string. - if (finding.source === 'DAST' || finding.source === 'SCA' || finding.source === 'GITLAB_SCANNER') { - return finding.location; + // For these types, make the location a clickable link to the repo file + if (['SAST','IAC'].includes(finding.source)) { + const repoLink = this.getRepositoryLink(); + + const location = finding.location; + + return `${location}`; } - const location = finding.location; - const match = location.match(/(.*):(\d+)/); - if (!match) return location; + // For other types, just show the plain text location without a link. + if (['DAST', 'SCA', 'GITLAB_SCANNER', 'SECRETS'].includes(finding.source)) { + return finding.location; + } - const [, filePath, lineNumber] = match; - return `${filePath}:${lineNumber}`; + // Fallback for any other case + return finding.location; } } \ No newline at end of file