diff --git a/manager/build.gradle b/manager/build.gradle index f9754a8..c92b5e2 100644 --- a/manager/build.gradle +++ b/manager/build.gradle @@ -50,6 +50,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springdoc:springdoc-openapi-ui:1.6.15' + implementation 'org.jsoup:jsoup:1.16.1' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' annotationProcessor 'org.projectlombok:lombok' diff --git a/manager/src/main/java/com/analyzer/sbom/controller/SbomController.java b/manager/src/main/java/com/analyzer/sbom/controller/SbomController.java index a7b5fc0..5af0f68 100644 --- a/manager/src/main/java/com/analyzer/sbom/controller/SbomController.java +++ b/manager/src/main/java/com/analyzer/sbom/controller/SbomController.java @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.io.IOException; import java.util.List; import static org.springframework.http.HttpStatus.*; @@ -35,7 +36,7 @@ public ResponseEntity> scanSBOM(@RequestParam String to @GetMapping("/report") @Operation(summary = "Get SBOM Report", description = "Get Software Bill of Materials' security report") - public ResponseEntity>> getReport(@RequestParam String token, @RequestParam String projectId, @RequestParam String baseUrl) throws JsonProcessingException { + public ResponseEntity>> getReport(@RequestParam String token, @RequestParam String projectId, @RequestParam String baseUrl) throws IOException { List sbomReport = sbomService.generateReport(token, projectId, baseUrl); return ResponseEntity.status(OK).body(CommonResponse.resWithData("SBOM_REPORT_GENERATED", "SBOM 보안 보고서가 생성되었습니다", sbomReport)); } diff --git a/manager/src/main/java/com/analyzer/sbom/dto/response/SbomResponseDto.java b/manager/src/main/java/com/analyzer/sbom/dto/response/SbomResponseDto.java index 959918d..926a7d6 100644 --- a/manager/src/main/java/com/analyzer/sbom/dto/response/SbomResponseDto.java +++ b/manager/src/main/java/com/analyzer/sbom/dto/response/SbomResponseDto.java @@ -17,10 +17,10 @@ public class SbomResponseDto { private String source; private String referenceUrl; private String suggestion; - private List recommendUrl; + private List suggestionUrl; @Builder - public SbomResponseDto(String name, String purl, String version, String group, String vulnId, String severity, String description, String source, String referenceUrl, String suggestion, List recommendUrl) { + public SbomResponseDto(String name, String purl, String version, String group, String vulnId, String severity, String description, String source, String referenceUrl, String suggestion, List suggestionUrl) { this.name = name; this.purl = purl; this.version = version; @@ -31,6 +31,6 @@ public SbomResponseDto(String name, String purl, String version, String group, S this.source = source; this.referenceUrl = referenceUrl; this.suggestion = suggestion; - this.recommendUrl = recommendUrl; + this.suggestionUrl = suggestionUrl; } } diff --git a/manager/src/main/java/com/analyzer/sbom/service/SbomService.java b/manager/src/main/java/com/analyzer/sbom/service/SbomService.java index 0f6ecd3..538a60e 100644 --- a/manager/src/main/java/com/analyzer/sbom/service/SbomService.java +++ b/manager/src/main/java/com/analyzer/sbom/service/SbomService.java @@ -5,12 +5,19 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; +import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -20,17 +27,32 @@ public class SbomService { private final ObjectMapper objectMapper; @Value("${webclient.nvd}") - private String ref; + private String cveUrl; + + @Value("${webclient.nvdSelector}") + private String nvdSelector; + + @Value("${webclient.snyk}") + private String snykUrl; + + @Value("${webclient.snykXpath}") + private String snykXpath; + + @Value("${webclient.snykSearchXpath}") + private String snykSearchXpath; + + @Value("${webclient.snykBase}") + private String snykBase; public JsonNode scanVulnerability(String token, String projectId, String baseUrl) throws JsonProcessingException { String jsonData = getAPI(token, projectId, baseUrl); return objectMapper.readTree(jsonData); } - public List generateReport(String token, String projectId, String baseUrl) throws JsonProcessingException { + public List generateReport(String token, String projectId, String baseUrl) throws IOException { String sbomResult = getAPI(token, projectId, baseUrl); - List sbomReport = new ArrayList(); + List sbomReport = new ArrayList<>(); JsonNode jsonNode = objectMapper.readTree(sbomResult).get("findings"); if (jsonNode.isArray()) { @@ -42,7 +64,7 @@ public List generateReport(String token, String projectId, Stri String group = isExist(component, "group") ? component.get("group").asText() : ""; JsonNode attribution = finding.get("attribution"); - String recommendUrl = isExist(attribution, "referenceUrl") ? attribution.get("referenceUrl").asText() : ""; + String suggestionLink = isExist(attribution, "referenceUrl") ? attribution.get("referenceUrl").asText() : ""; JsonNode vulnerability = finding.get("vulnerability"); String severity = isExist(vulnerability, "severity") ? vulnerability.get("severity").asText() : ""; @@ -50,7 +72,13 @@ public List generateReport(String token, String projectId, Stri String source = isExist(vulnerability, "source") ? vulnerability.get("source").asText() : ""; String description = isExist(vulnerability, "description") ? vulnerability.get("description").asText() : ""; - String referenceUrl = ref + vulnId; + String referenceUrl = cveUrl + vulnId; + + List suggestionUrl = getSuggestionUrl(referenceUrl); + suggestionUrl.add(getSuggestion(snykUrl + vulnId, true)); + if(!Objects.equals(suggestionLink, "")) suggestionUrl.add(suggestionLink); + + String suggestion = getSuggestion(snykUrl + vulnId, false); SbomResponseDto sbomResponseDto = SbomResponseDto.builder() .name(name) @@ -62,8 +90,8 @@ public List generateReport(String token, String projectId, Stri .source(source) .description(description) .referenceUrl(referenceUrl) - .recommendUrl(null) - .suggestion("") + .suggestionUrl(suggestionUrl) + .suggestion(suggestion) .build(); sbomReport.add(sbomResponseDto); @@ -72,32 +100,44 @@ public List generateReport(String token, String projectId, Stri return sbomReport; } - public String getAPI(String token, String projectId, String baseUrl) { + private String getAPI(String token, String projectId, String baseUrl) { WebClient webClient = webClientBuilder .baseUrl(baseUrl) .defaultHeader("X-Api-Key", token) .build(); - String sbomResult = webClient + return webClient .get() .uri("/api/v1/finding/project/{projectId}/export", projectId) .retrieve() .bodyToMono(String.class) .block(); - - return sbomResult; } private boolean isExist(JsonNode source, String fieldName) { return source.has(fieldName); } - private String getSuggestionUrl() { - return ""; + private List getSuggestionUrl(String cveUrl) throws IOException { + Document doc = Jsoup.connect(cveUrl).get(); + Element table = doc.select(nvdSelector).first(); + Elements tdElements = Objects.requireNonNull(table).select("td"); + + return tdElements.stream() + .flatMap(td -> td.select("a").stream()) + .map(Element::text) + .collect(Collectors.toList()); } - private String getSuggestion() { - return ""; + private String getSuggestion(String snykUrl, Boolean isLink) throws IOException { + Document doc = Jsoup.connect(snykUrl).get(); + String link = String.join("", doc.selectXpath(snykXpath).eachAttr("href")); + String suggestionLink = snykBase + link; + + Document solutionDoc = Jsoup.connect(suggestionLink).get(); + String suggestion = solutionDoc.selectXpath(snykSearchXpath).text(); + + return isLink ? suggestionLink : suggestion; } }