Skip to content
38 changes: 20 additions & 18 deletions src/main/java/org/frankframework/insights/InsightsApplication.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package org.frankframework.insights;

import static org.springframework.web.servlet.function.RequestPredicates.path;
import static org.springframework.web.servlet.function.RequestPredicates.pathExtension;
import static org.springframework.web.servlet.function.RouterFunctions.route;

import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
Expand All @@ -11,10 +15,6 @@
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;

import static org.springframework.web.servlet.function.RequestPredicates.path;
import static org.springframework.web.servlet.function.RequestPredicates.pathExtension;
import static org.springframework.web.servlet.function.RouterFunctions.route;

@SpringBootApplication
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "PT2H")
Expand All @@ -29,18 +29,20 @@ public static SpringApplication configureApplication() {
return new SpringApplication(InsightsApplication.class);
}


/**
* This is a custom router function to accommodate to our single page application that we serve from this spring boot backend as well.
* This RouterFunction will make sure that we serve `frontend/index.html` whenever the path does not start with `/api/`, is not `/error` and does
* not have a path extension (to exclude static resources).
*
* @see <a href="https://github.com/spring-projects/spring-framework/issues/27257">Spring framework issue 27257</a> for more details.
*/
@Bean
RouterFunction<ServerResponse> spaRouter() {
ClassPathResource index = new ClassPathResource("frontend/index.html");
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extension -> !extension.isBlank())).negate();
return route().resource(spaPredicate, index).build();
}
/**
* This is a custom router function to accommodate to our single page application that we serve from this spring boot backend as well.
* This RouterFunction will make sure that we serve `frontend/index.html` whenever the path does not start with `/api/`, is not `/error` and does
* not have a path extension (to exclude static resources).
*
* @see <a href="https://github.com/spring-projects/spring-framework/issues/27257">Spring framework issue 27257</a> for more details.
*/
@Bean
RouterFunction<ServerResponse> spaRouter() {
ClassPathResource index = new ClassPathResource("frontend/index.html");
RequestPredicate spaPredicate = path("/api/**")
.or(path("/error"))
.or(pathExtension(extension -> !extension.isBlank()))
.negate();
return route().resource(spaPredicate, index).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.frankframework.insights.branch.BranchService;
import org.frankframework.insights.github.GitHubClientException;
Expand All @@ -20,6 +18,7 @@
import org.frankframework.insights.release.ReleaseService;
import org.frankframework.insights.vulnerability.VulnerabilityService;
import org.owasp.dependencycheck.utils.Settings;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Scheduled;
Expand All @@ -38,8 +37,8 @@ public class SystemDataInitializer implements CommandLineRunner {
private final ReleaseService releaseService;
private final VulnerabilityService vulnerabilityService;

@Value("${data.fetch-enabled}")
private boolean dataFetchEnabled;
@Value("${data.fetch-enabled}")
private boolean dataFetchEnabled;

public SystemDataInitializer(
GitHubRepositoryStatisticsService gitHubRepositoryStatisticsService,
Expand Down Expand Up @@ -145,6 +144,7 @@ public void initializeSystemData() {
private void cleanUpOwaspLockFile() {
try {
Settings settings = new Settings();
settings.setString(Settings.KEYS.DATA_DIRECTORY, "/owasp-data");
Path dataDirectory = settings.getDataDirectory().toPath();

if (!Files.isDirectory(dataDirectory)) {
Expand All @@ -154,12 +154,12 @@ private void cleanUpOwaspLockFile() {
try (Stream<Path> files = Files.list(dataDirectory)) {
files.filter(path -> path.getFileName().toString().endsWith(".lock"))
.forEach(lockFile -> {
try {
Files.delete(lockFile);
log.warn("Removed stale OWASP dependency-check lock file: {}", lockFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
Files.delete(lockFile);
log.warn("Removed stale OWASP dependency-check lock file: {}", lockFile);
} catch (IOException e) {
log.error("Failed to delete OWASP lock file: {}", lockFile, e);
}
});
}
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
import org.springframework.stereotype.Repository;

@Repository
public interface PullRequestIssueRepository extends JpaRepository<PullRequestIssue, PullRequestIssueId> { }
public interface PullRequestIssueRepository extends JpaRepository<PullRequestIssue, PullRequestIssueId> {}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@
import org.springframework.stereotype.Repository;

@Repository
public interface PullRequestLabelRepository extends JpaRepository<PullRequestLabel, PullRequestLabelId>{
}
public interface PullRequestLabelRepository extends JpaRepository<PullRequestLabel, PullRequestLabelId> {}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
import org.springframework.stereotype.Repository;

@Repository
public interface ReleasePullRequestRepository extends JpaRepository<ReleasePullRequest, ReleasePullRequestId> { }
public interface ReleasePullRequestRepository extends JpaRepository<ReleasePullRequest, ReleasePullRequestId> {}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ public interface ReleaseVulnerabilityRepository extends JpaRepository<ReleaseVul
@Modifying
@Transactional
void deleteAllByRelease(Release release);

List<ReleaseVulnerability> findAllByReleaseId(String releaseId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ public enum VulnerabilitySeverity {
MEDIUM(4.0, 6.9),
LOW(0.1, 3.9),
NONE(0.0, 0.0),
UNKNOWN(-1.0, -1.0);
UNKNOWN(0.0, 0.0);

private static final int MAX_CVSS_SCORE = 10;
private static final double CRITICAL_REPRESENTATIVE_SCORE = 9.5;
private static final double HIGH_REPRESENTATIVE_SCORE = 8.0;
private static final double MEDIUM_REPRESENTATIVE_SCORE = 5.5;
private static final double LOW_REPRESENTATIVE_SCORE = 2.0;
private final double minScore;
private final double maxScore;

Expand All @@ -36,4 +40,20 @@ public static VulnerabilitySeverity fromScore(double score) {
.findFirst()
.orElse(UNKNOWN);
}

/**
* Gets a representative CVSS score for this severity level.
* Used when only severity is known but no actual CVSS score is available.
*
* @return A representative score for this severity.
*/
public double getRepresentativeScore() {
return switch (this) {
case CRITICAL -> CRITICAL_REPRESENTATIVE_SCORE;
case HIGH -> HIGH_REPRESENTATIVE_SCORE;
case MEDIUM -> MEDIUM_REPRESENTATIVE_SCORE;
case LOW -> LOW_REPRESENTATIVE_SCORE;
case NONE, UNKNOWN -> 0.0;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,8 @@ private Set<Issue> saveIssues(Set<Issue> issues) {
public Set<IssueResponse> getIssuesByReleaseId(String releaseId) throws ReleaseNotFoundException {
Release release = releaseService.checkIfReleaseExists(releaseId);
Set<Issue> allIssues = issueRepository.findIssuesByReleaseId(release.getId());
Set<Issue> rootIssues = filterRootIssues(allIssues);
return buildIssueResponseTree(rootIssues);
Set<Issue> rootIssues = filterRootIssues(allIssues);
return buildIssueResponseTree(rootIssues);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,27 @@
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Slf4j
public class ReleaseArtifactService {

private static final Path ARCHIVE_DIR = Paths.get("release-archive");
private static final String GITHUB_ZIP_URL_FORMAT =
"https://github.com/frankframework/frankframework/archive/refs/tags/%s.zip";

private static final int MAX_ENTRIES = 50000;
private static final long MAX_UNCOMPRESSED_SIZE = 1024L * 1024 * 1024 * 4;
private static final double COMPRESSION_RATIO_LIMIT = 1000.0;
private static final int BUFFER_SIZE = 4096;

@Value("${release.archive.directory}")
private String archiveDirectory;

@Transactional
public Path prepareReleaseArtifacts(Release release) throws IOException {
Path releaseDir = ARCHIVE_DIR.resolve(release.getName());
Path releaseDir = Paths.get(archiveDirectory).resolve(release.getName());

if (releaseDirectoryExists(releaseDir, release)) {
return releaseDir;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,30 @@
package org.frankframework.insights.vulnerability;

import java.util.Set;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;
import java.util.Set;

@RestController
@RequestMapping("/vulnerabilities")
public class VulnerabilityController {
private final VulnerabilityService vulnerabilityService;
private final VulnerabilityService vulnerabilityService;

public VulnerabilityController(VulnerabilityService vulnerabilityService) {
this.vulnerabilityService = vulnerabilityService;
}
public VulnerabilityController(VulnerabilityService vulnerabilityService) {
this.vulnerabilityService = vulnerabilityService;
}

/**
* Fetches all vulnerabilities associated with a given release ID.
* @param releaseId The ID of the release to fetch vulnerabilities for
* @return Set of vulnerabilities associated with the release
*/
@GetMapping("/release/{releaseId}")
public ResponseEntity<Set<VulnerabilityResponse>> getVulnerabilitiesByReleaseId(@PathVariable String releaseId) {
Set<VulnerabilityResponse> vulnerabilities = vulnerabilityService.getVulnerabilitiesByReleaseId(releaseId);
if (vulnerabilities == null) vulnerabilities = Collections.emptySet();
return ResponseEntity.status(HttpStatus.OK).body(vulnerabilities);
}
/**
* Fetches all vulnerabilities associated with a given release ID.
* @param releaseId The ID of the release to fetch vulnerabilities for
* @return Set of vulnerabilities associated with the release
*/
@GetMapping("/release/{releaseId}")
public ResponseEntity<Set<VulnerabilityResponse>> getVulnerabilitiesByReleaseId(@PathVariable String releaseId) {
Set<VulnerabilityResponse> vulnerabilities = vulnerabilityService.getVulnerabilitiesByReleaseId(releaseId);
return ResponseEntity.status(HttpStatus.OK).body(vulnerabilities);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.frankframework.insights.vulnerability;

import org.frankframework.insights.common.enums.VulnerabilitySeverity;

import java.util.Set;
import org.frankframework.insights.common.enums.VulnerabilitySeverity;

public record VulnerabilityResponse(String cveId, VulnerabilitySeverity severity, Double cvssScore, String description, Set<String> cwes) { }
public record VulnerabilityResponse(
String cveId, VulnerabilitySeverity severity, Double cvssScore, String description, Set<String> cwes) {}
Loading