hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/strategies/VersionedEngagementsStrategy.java
// SPDX-FileCopyrightText: the secureCodeBox authors
//
// SPDX-License-Identifier: Apache-2.0
package io.securecodebox.persistence.strategies;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.kubernetes.client.openapi.models.V1OwnerReference;
import io.securecodebox.persistence.config.EnvConfig;
import io.securecodebox.persistence.config.PersistenceProviderConfig;
import io.securecodebox.persistence.defectdojo.config.Config;
import io.securecodebox.persistence.defectdojo.model.*;
import io.securecodebox.persistence.defectdojo.service.*;
import io.securecodebox.persistence.exceptions.DefectDojoPersistenceException;
import io.securecodebox.persistence.models.Scan;
import io.securecodebox.persistence.util.DescriptionGenerator;
import io.securecodebox.persistence.util.ScanNameMapping;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.map.LinkedMap;
import java.net.URISyntaxException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* VersionedEngagementsStrategy creates a new Engagement for every new version of the software
* <p>
* If a engagement already exists for this version it'll reuse the engagement and append new tests for every scan until
* the version gets bumped.
* </p>
*/
@Slf4j
public class VersionedEngagementsStrategy implements Strategy {
private static final String SCB_GITHUB_REPO_URL = "https://github.com/secureCodeBox";
private final DescriptionGenerator descriptionGenerator = new DescriptionGenerator();
ProductService productService;
ProductTypeService productTypeService;
UserService userService;
UserProfileService userProfileService;
ToolTypeService toolTypeService;
ToolConfigService toolConfigService;
EngagementService engagementService;
TestService testService;
TestTypeService testTypeService;
ImportScanService importScanService;
FindingService findingService;
Config config;
PersistenceProviderConfig persistenceProviderConfig;
public VersionedEngagementsStrategy() {
super();
}
@Override
public void init(Config defectDojoConfig, PersistenceProviderConfig persistenceProviderConfig) {
this.productService = new ProductService(defectDojoConfig);
this.productTypeService = new ProductTypeService(defectDojoConfig);
this.userService = new UserService(defectDojoConfig);
this.userProfileService = new UserProfileService(defectDojoConfig);
this.toolTypeService = new ToolTypeService(defectDojoConfig);
this.toolConfigService = new ToolConfigService(defectDojoConfig);
this.engagementService = new EngagementService(defectDojoConfig);
this.testService = new TestService(defectDojoConfig);
this.testTypeService = new TestTypeService(defectDojoConfig);
this.importScanService = ImportScanService.createDefault(defectDojoConfig);
this.findingService = new FindingService(defectDojoConfig);
this.config = defectDojoConfig;
this.persistenceProviderConfig = persistenceProviderConfig;
}
@Override
public List<Finding> run(Scan scan, ScanFile scanResultFile) throws DefectDojoPersistenceException {
log.debug("Getting DefectDojo User Id via user profile API");
final List<UserProfile> userProfiles;
try {
userProfiles = userProfileService.search();
} catch (URISyntaxException | JsonProcessingException e) {
throw new DefectDojoPersistenceException(e.getMessage(), e);
}
final long userId;
if (userProfiles.isEmpty()) {
throw new DefectDojoPersistenceException("UserProfileService did return empty list. Expected current user to be in list");
} else {
userId = userProfiles.get(0).getUser().getId();
}
log.info("Running with DefectDojo User Id: {}", userId);
final long productTypeId;
final long productId;
try {
productTypeId = this.ensureProductTypeExistsForScan(scan);
productId = this.ensureProductExistsForScan(scan, productTypeId).getId();
} catch (URISyntaxException | JsonProcessingException e) {
throw new DefectDojoPersistenceException(e.getMessage(), e);
}
log.debug("Looking for existing or creating new DefectDojo Engagement");
final long engagementId;
try {
engagementId = this.createEngagement(scan, productId, userId).getId();
} catch (URISyntaxException | JsonProcessingException e) {
throw new DefectDojoPersistenceException(e.getMessage(), e);
}
log.debug("Using Engagement with Id: {}", engagementId);
long testId;
try {
testId = this.createTest(scan, engagementId, userId);
} catch (URISyntaxException | JsonProcessingException e) {
throw new DefectDojoPersistenceException(e.getMessage(), e);
}
log.debug("Uploading Scan Report to DefectDojo");
final var scanType = ScanNameMapping.bySecureCodeBoxScanType(scan.getSpec().getScanType()).defectDojoScanType;
final var searchObject = TestType.builder().name(scanType.getTestType()).build();
final TestType testType;
try {
testType = testTypeService.searchUnique(searchObject)
.orElseThrow(() -> {
final var message = String.format(
"Could not find test type '%s' in DefectDojo API. DefectDojo might be running in an unsupported version.",
scanType.getTestType());
return new DefectDojoPersistenceException(message);
});
} catch (URISyntaxException | JsonProcessingException e) {
throw new DefectDojoPersistenceException(e.getMessage(), e);
}
final var additionalValues = new LinkedMap<String, String>();
if (scan.getMinimumSeverity().isPresent()) {
additionalValues.put("minimum-severity", scan.getMinimumSeverity().get());
}
importScanService.reimportScan(
scanResultFile,
testId,
userId,
this.descriptionGenerator.currentDate(),
scanType,
testType.getId(),
additionalValues
);
log.info("Uploaded Scan Report as testID {} to DefectDojo", testId);
waitUntilDeduplicationIsDone();
try {
return findingService.search(Map.of("test", String.valueOf(testId)));
} catch (URISyntaxException | JsonProcessingException e) {
throw new DefectDojoPersistenceException(e.getMessage(), e);
}
}
/**
* Creates a new DefectDojo engagement for the given securityTest.
*
* @param scan The Scan to crete an DefectDojo engagement for.
* @return The newly created engagement.
*/
public Engagement createEngagement(Scan scan, Long productId, Long userId) throws URISyntaxException, JsonProcessingException {
String engagementName = this.getEngagementsName(scan);
List<String> tags = scan.getEngagementTags().orElseGet(List::of);
String version = scan.getEngagementVersion().orElse("");
var engagementBuilder = Engagement.builder()
.product(productId)
.name(engagementName)
.lead(userId)
.description("")
.tags(tags)
.version(version)
.branch(version)
.targetStart(descriptionGenerator.currentDate())
.targetEnd(descriptionGenerator.currentDate())
.deduplicationOnEngagement(scan.getDeDuplicateOnEngagement().orElse(false))
.status(Engagement.Status.IN_PROGRESS);
if (!this.persistenceProviderConfig.isInLowPrivilegedMode()) {
final String SECURITY_TEST_SERVER_NAME = "secureCodeBox";
final String SECURITY_TEST_SERVER_DESCRIPTION = "secureCodeBox is a kubernetes based, modularized toolchain for continuous security scans of your software project.";
var securityTestOrchestrationEngine = toolTypeService.searchUnique(ToolType.builder().name(ToolTypeService.SECURITY_TEST_SERVER_NAME).build()).orElseGet(
() -> toolTypeService.create(
ToolType.builder()
.name(ToolTypeService.SECURITY_TEST_SERVER_NAME)
.description("Security Test Orchestration Engine")
.build()
)
);
var toolConfig = toolConfigService.searchUnique(
ToolConfig.builder().name(SECURITY_TEST_SERVER_NAME).url(SCB_GITHUB_REPO_URL).build()
).orElseGet(() -> {
log.info("Creating secureCodeBox Tool Config");
return toolConfigService.create(
ToolConfig.builder()
.toolType(securityTestOrchestrationEngine.getId())
.name(SECURITY_TEST_SERVER_NAME)
.description(SECURITY_TEST_SERVER_DESCRIPTION)
.url(SCB_GITHUB_REPO_URL)
.configUrl(SCB_GITHUB_REPO_URL)
.build()
);
});
engagementBuilder.orchestrationEngine(toolConfig.getId());
}
var engagement = engagementBuilder.build();
return engagementService.searchUnique(Engagement.builder().product(productId).name(engagementName).version(version).build()).orElseGet(() -> {
log.info("Creating new Engagement as no matching Engagements could be found.");
return engagementService.create(engagement);
});
}
/**
* Creates a new productType in DefectDojo if none exists already for the given scan.
* If no productType is defined for the given scan a default productType will be used (productType Id = 1).
*
* @param scan The scan to ensure the DefectDojo productType for.
* @return The productType Id already existing or newly created.
* @throws URISyntaxException
* @throws JsonProcessingException
*/
private long ensureProductTypeExistsForScan(Scan scan) throws URISyntaxException, JsonProcessingException {
var productTypeNameOptional = scan.getProductType();
if (productTypeNameOptional.isEmpty()) {
log.info("Using default ProductType as no '{}' annotation was found on the scan", Scan.SecureCodeBoxScanAnnotations.PRODUCT_TYPE.getLabel());
return 1;
}
var productTypeName = productTypeNameOptional.get();
log.info("Looking for ID of ProductType '{}'", productTypeName);
var productType = productTypeService.searchUnique(ProductType.builder().name(productTypeName).build()).orElseGet(() -> {
log.info("ProductType '{}' didn't already exists creating now", productTypeName);
return productTypeService.create(ProductType.builder().name(productTypeName).build());
});
log.info("Using ProductType Id: {}", productType.getId());
return productType.getId();
}
/**
* Creates a new product in DefectDojo if none exists already related to the given scan and productType.
*
* @param scan The scan to ensure the DefectDojo product for.
* @param productTypeId The id of the productType.
* @return The existing or newly created product releated to the given scan.
* @throws URISyntaxException
* @throws JsonProcessingException
*/
private Product ensureProductExistsForScan(Scan scan, long productTypeId) throws URISyntaxException, JsonProcessingException {
String productName = this.getProductName(scan);
String productDescription = scan.getProductDescription().orElse("Product was automatically created by the secureCodeBox DefectDojo integration");
List<String> tags = scan.getProductTags().orElseGet(List::of);
return productService.searchUnique(Product.builder().name(productName).productType(productTypeId).build()).orElseGet(() -> {
log.info("Creating Product: '{}'", productName);
return productService.create(Product.builder()
.name(productName)
.description(productDescription)
.productType(productTypeId)
.tags(tags)
.build()
);
});
}
/**
* Creates a new test in DefectDojo related to the given scan and engagement.
*
* @param scan The scan to create a new test in defectDojo for (related to the given engagement).
* @param engagementId The engagement (referenced by id) to relate the new test to.
* @param userId The user id corresponding to create the test on behalf to.
* @return The newly created test id.
* @throws URISyntaxException
* @throws JsonProcessingException
*/
private long createTest(Scan scan, long engagementId, long userId) throws URISyntaxException, JsonProcessingException {
var dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssZ");
var startDate = Objects.requireNonNull(scan.getMetadata().getCreationTimestamp()).format(dateFormat);
// End date on the Scan Object isn't set when the DefectDojo Hook runs, best approximation of the end date is the current time.
String endDate = ZonedDateTime.now().format(dateFormat);
String version = scan.getEngagementVersion().orElse(null);
String scanType = ScanNameMapping.bySecureCodeBoxScanType(scan.getSpec().getScanType()).defectDojoScanType.getTestType();
TestType testType = testTypeService.searchUnique(TestType.builder().name(scanType).build()).orElseThrow(() -> new DefectDojoPersistenceException("Could not find test type '" + scanType + "' in DefectDojo API. DefectDojo might be running in an unsupported version."));
String testTitle = scan.getTestTitle().orElse(scan.getMetadata().getName());
var test = Test.builder()
.title(testTitle)
.description(descriptionGenerator.generate(scan))
.testType(testType.getId())
.targetStart(startDate)
.targetEnd(endDate)
.engagement(engagementId)
.lead(userId)
.percentComplete(100L)
.version(version)
.build();
return testService.create(test).getId();
}
/**
* Returns the DefectDojo Product Name related to the given scan.
*
* @param scan The scan the productName relates to.
* @return The productName related to the given scan.
*/
protected String getProductName(Scan scan) {
String result = "unknown";
if (scan.getProductName().isPresent()) {
result = scan.getProductName().get();
} else if (scan.getMetadata().getOwnerReferences() != null) {
// try to use the scheduledScan name if no productName is defined
result = getProductNameForParentScan(scan.getMetadata().getOwnerReferences());
} else {
result = scan.getMetadata().getName();
}
return result;
}
/**
* Returns the DefectDojo Product Name related to the given scan.
* If the Scan was created via a scheduled scan, the Name of the ScheduledScan should be preferred to the scans name.
*
* @param ownerReferences The ownerReferences of the child Object.
* @return The productName related to the given scan.
*/
protected String getProductNameForParentScan(List<V1OwnerReference> ownerReferences) {
String result = "";
for (var ownerReference : ownerReferences) {
if ("ScheduledScan".equals(ownerReference.getKind())) {
result = ownerReference.getName();
}
}
return result;
}
/**
* Returns the DefectDojo Engagement Name related to the given scan.
*
* @param scan The scan the Engagement Name relates to.
* @return the DefectDojo Engagement Name related to the given scan.
*/
protected String getEngagementsName(Scan scan) {
return scan.getEngagementName().orElseGet(() -> scan.getMetadata().getName());
}
/**
* As a workaround we wait a configurable amount of time and block the execution
* <p>
* We hope that Defectdojo will provide an API in the future to check if the deduplication is done.
* </p>
*/
private static void waitUntilDeduplicationIsDone() {
final var config = new EnvConfig();
final var awaiter = new Awaiter(config.refetchWaitSeconds());
log.info("Waiting for {} seconds for deduplication to finish before continuing", config.refetchWaitSeconds());
awaiter.await();
}
}