Skip to content

Commit

Permalink
Merge: Main <- juwon/#8-Controller-Service
Browse files Browse the repository at this point in the history
[Feat] Controller, Service Layer 구현
  • Loading branch information
Juser0 authored Oct 3, 2023
2 parents e8b2ec6 + 09f1a4d commit adedf8f
Show file tree
Hide file tree
Showing 15 changed files with 302 additions and 36 deletions.
1 change: 1 addition & 0 deletions manager/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.pipeline.manager;
package com.analyzer.sbom;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
Expand Down
35 changes: 35 additions & 0 deletions manager/src/main/java/com/analyzer/sbom/common/CommonResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.analyzer.sbom.common;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Getter;

@Getter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CommonResponse<T> {

private String responseCode;
private String responseMessage;
private T data;

@Builder
private CommonResponse(String responseCode, String responseMessage, T data) {
this.responseCode = responseCode;
this.responseMessage = responseMessage;
this.data = data;
}

public static CommonResponse resWithoutData (final String responseCode, final String responseMessage) {
return CommonResponse.builder()
.responseCode(responseCode)
.responseMessage(responseMessage)
.build();
}
public static <T> CommonResponse<T> resWithData (final String responseCode, final String responseMessage, final T data) {
return CommonResponse.<T>builder()
.responseCode(responseCode)
.responseMessage(responseMessage)
.data(data)
.build();
}
}
24 changes: 24 additions & 0 deletions manager/src/main/java/com/analyzer/sbom/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.analyzer.sbom.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.components(new Components())
.info(apiInfo());
}

private Info apiInfo() {
return new Info()
.title("SBOM Vulnerability Analyzer")
.description("SBOM에서 확인 가능한 취약점에 대한 정보를 정리해주는 애플리케이션 명세입니다")
.version("1.0.0");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.analyzer.sbom.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.web.reactive.function.client.WebClient;


@Configuration
public class WebClientConfig {
@Bean
public WebClient.Builder webclientBuilder() {
return WebClient.builder()
.defaultHeader(HttpHeaders.ACCEPT, "*/*")
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/json");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.analyzer.sbom.controller;

import com.analyzer.sbom.common.CommonResponse;
import com.analyzer.sbom.dto.response.SbomResponseDto;
import com.analyzer.sbom.service.SbomService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
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.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/sbom")
@Tag(name = "SBOM", description = "SBOM controller")
public class SbomController {

private final SbomService sbomService;

@GetMapping
@Operation(summary = "SBOM Scan", description = "Scan Software Bill of Materials")
public ResponseEntity<CommonResponse<JsonNode>> scanSBOM(@RequestParam String token, @RequestParam String projectId, @RequestParam String baseUrl) throws JsonProcessingException {
JsonNode sbomResult = sbomService.scanVulnerability(token, projectId, baseUrl);
return ResponseEntity.status(OK).body(CommonResponse.resWithData("SBOM_SCAN_COMPLETED", "SBOM 스캔이 완료되었습니다", sbomResult));
}

@GetMapping("/report")
@Operation(summary = "Get SBOM Report", description = "Get Software Bill of Materials' security report")
public ResponseEntity<CommonResponse<List<SbomResponseDto>>> getReport(@RequestParam String token, @RequestParam String projectId, @RequestParam String baseUrl) throws IOException {
List<SbomResponseDto> sbomReport = sbomService.generateReport(token, projectId, baseUrl);
return ResponseEntity.status(OK).body(CommonResponse.resWithData("SBOM_REPORT_GENERATED", "SBOM 보안 보고서가 생성되었습니다", sbomReport));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.analyzer.sbom.dto.response;

import lombok.Builder;
import lombok.Getter;

import java.util.List;

@Getter
public class SbomResponseDto {
private String name;
private String purl;
private String version;
private String group;
private String vulnId;
private String severity;
private String description;
private String source;
private String referenceUrl;
private String suggestion;
private List<String> 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<String> suggestionUrl) {
this.name = name;
this.purl = purl;
this.version = version;
this.group = group;
this.vulnId = vulnId;
this.severity = severity;
this.description = description;
this.source = source;
this.referenceUrl = referenceUrl;
this.suggestion = suggestion;
this.suggestionUrl = suggestionUrl;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.pipeline.manager.exception;
package com.analyzer.sbom.exception;

import org.springframework.web.bind.annotation.RestControllerAdvice;

Expand Down
143 changes: 143 additions & 0 deletions manager/src/main/java/com/analyzer/sbom/service/SbomService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package com.analyzer.sbom.service;

import com.analyzer.sbom.dto.response.SbomResponseDto;
import com.fasterxml.jackson.core.JsonProcessingException;
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
public class SbomService {

private final WebClient.Builder webClientBuilder;
private final ObjectMapper objectMapper;

@Value("${webclient.nvd}")
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<SbomResponseDto> generateReport(String token, String projectId, String baseUrl) throws IOException {
String sbomResult = getAPI(token, projectId, baseUrl);

List<SbomResponseDto> sbomReport = new ArrayList<>();

JsonNode jsonNode = objectMapper.readTree(sbomResult).get("findings");
if (jsonNode.isArray()) {
for (JsonNode finding : jsonNode) {
JsonNode component = finding.get("component");
String name = isExist(component, "name") ? component.get("name").asText() : "";
String version = isExist(component, "version") ? component.get("version").asText() : "";
String purl = isExist(component, "purl") ? component.get("purl").asText() : "";
String group = isExist(component, "group") ? component.get("group").asText() : "";

JsonNode attribution = finding.get("attribution");
String suggestionLink = isExist(attribution, "referenceUrl") ? attribution.get("referenceUrl").asText() : "";

JsonNode vulnerability = finding.get("vulnerability");
String severity = isExist(vulnerability, "severity") ? vulnerability.get("severity").asText() : "";
String vulnId = isExist(vulnerability, "vulnId") ? vulnerability.get("vulnId").asText() : "";
String source = isExist(vulnerability, "source") ? vulnerability.get("source").asText() : "";
String description = isExist(vulnerability, "description") ? vulnerability.get("description").asText() : "";

String referenceUrl = cveUrl + vulnId;

List<String> 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)
.version(version)
.purl(purl)
.group(group)
.severity(severity)
.vulnId(vulnId)
.source(source)
.description(description)
.referenceUrl(referenceUrl)
.suggestionUrl(suggestionUrl)
.suggestion(suggestion)
.build();

sbomReport.add(sbomResponseDto);
}
}
return sbomReport;
}

private String getAPI(String token, String projectId, String baseUrl) {
WebClient webClient = webClientBuilder
.baseUrl(baseUrl)
.defaultHeader("X-Api-Key", token)
.build();

return webClient
.get()
.uri("/api/v1/finding/project/{projectId}/export", projectId)
.retrieve()
.bodyToMono(String.class)
.block();
}

private boolean isExist(JsonNode source, String fieldName) {
return source.has(fieldName);
}

private List<String> 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(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;
}

}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.pipeline.manager;
package com.analyzer.sbom;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
Expand Down

0 comments on commit adedf8f

Please sign in to comment.