diff --git a/gradle/libraries.gradle b/gradle/libraries.gradle index 65357a870e..43424272e4 100644 --- a/gradle/libraries.gradle +++ b/gradle/libraries.gradle @@ -88,8 +88,12 @@ ext { cycloneDX_core: "8.0.0", cyclonedx_gradle_plugin: "1.7.4", - /* Prepare wrapper */ - jgit_core: "6.9.0.202403050737-r" + /* Prepare wrapper */ + jgit_core: "6.9.0.202403050737-r", + + /* encryption */ + // https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk18on + bouncy_castle_bcprov_jdk8: "1.78.1" ] @@ -187,7 +191,10 @@ ext { /* cycloneDX core for Xray and sechub importer */ cycloneDX_core: "org.cyclonedx:cyclonedx-core-java:${libraryVersion.cycloneDX_core}", - jgit_core: "org.eclipse.jgit:org.eclipse.jgit:${libraryVersion.jgit_core}" + jgit_core: "org.eclipse.jgit:org.eclipse.jgit:${libraryVersion.jgit_core}", + + bouncy_castle_bcprov_jdk8: "org.bouncycastle:bcprov-jdk18on:${libraryVersion.bouncy_castle_bcprov_jdk8}" + ] diff --git a/gradle/projects.gradle b/gradle/projects.gradle index 9d967e330e..c2aa00a027 100644 --- a/gradle/projects.gradle +++ b/gradle/projects.gradle @@ -19,6 +19,7 @@ projectType = [ project(':sechub-commons-model-testframework'), project(':sechub-commons-pds'), project(':sechub-commons-archive'), + project(':sechub-commons-encryption'), project(':sechub-storage-core'), project(':sechub-wrapper-owasp-zap'), project(':sechub-pds-commons-core'), diff --git a/sechub-adapter-pds/src/main/java/com/mercedesbenz/sechub/adapter/pds/PDSAdapterV1.java b/sechub-adapter-pds/src/main/java/com/mercedesbenz/sechub/adapter/pds/PDSAdapterV1.java index 728f1a29ef..1f88d03cee 100644 --- a/sechub-adapter-pds/src/main/java/com/mercedesbenz/sechub/adapter/pds/PDSAdapterV1.java +++ b/sechub-adapter-pds/src/main/java/com/mercedesbenz/sechub/adapter/pds/PDSAdapterV1.java @@ -169,12 +169,15 @@ private void waitForJobDone(PDSContext context) throws Exception { /* see PDSJobStatusState.java */ jobstatus = context.getResilientJobStatusResultExecutor().executeResilient(() -> getJobStatus(context)); - PDSJobStatusState state = jobstatus.state; + PDSJobStatusState state = jobstatus.getState(); switch (state) { case DONE: jobEnded = true; break; case FAILED: + if (jobstatus.isEncryptionOutOfSync()) { + throw new PDSEncryptionOutOfSyncException(); + } throw asAdapterException("PDS job execution failed: TimeOut=" + timeOutCheck.wasTimeOut() + ",JobEnded=" + jobEnded, config); case CANCELED: case CANCEL_REQUESTED: @@ -454,7 +457,7 @@ private AdapterExecutionResult handleExecutionTypeRestart(PDSContext context, Ad /* check job status */ try { PDSJobStatus currentPdsJobStatus = getJobStatus(context, UUID.fromString(pdsJobUUID)); - currentPdsJobState = currentPdsJobStatus.state; + currentPdsJobState = currentPdsJobStatus.getState(); if (currentPdsJobState == null) { throw new IllegalStateException("PDS job state null is not supported!"); diff --git a/sechub-adapter-pds/src/main/java/com/mercedesbenz/sechub/adapter/pds/PDSEncryptionOutOfSyncException.java b/sechub-adapter-pds/src/main/java/com/mercedesbenz/sechub/adapter/pds/PDSEncryptionOutOfSyncException.java new file mode 100644 index 0000000000..30cb2d6a15 --- /dev/null +++ b/sechub-adapter-pds/src/main/java/com/mercedesbenz/sechub/adapter/pds/PDSEncryptionOutOfSyncException.java @@ -0,0 +1,7 @@ +package com.mercedesbenz.sechub.adapter.pds; + +public class PDSEncryptionOutOfSyncException extends Exception { + + private static final long serialVersionUID = 1L; + +} diff --git a/sechub-adapter-pds/src/test/java/com/mercedesbenz/sechub/adapter/pds/PDSAdapterV1Test.java b/sechub-adapter-pds/src/test/java/com/mercedesbenz/sechub/adapter/pds/PDSAdapterV1Test.java index 57b2c402ff..cac27e620a 100644 --- a/sechub-adapter-pds/src/test/java/com/mercedesbenz/sechub/adapter/pds/PDSAdapterV1Test.java +++ b/sechub-adapter-pds/src/test/java/com/mercedesbenz/sechub/adapter/pds/PDSAdapterV1Test.java @@ -249,7 +249,7 @@ private void preparePDSJobCreation(UUID pdsJobUUID) throws AdapterException { private void preparePDSJobStatus(UUID pdsJobUUID, PDSJobStatusState state) { PDSJobStatus jobStatus = new PDSJobStatus(); - jobStatus.state = state; + jobStatus.setState(state); ResponseEntity responseEntity = new ResponseEntity<>(jobStatus, HttpStatus.OK); when(restOperations.getForEntity(eq("null/api/job/" + pdsJobUUID.toString() + "/status"), eq(PDSJobStatus.class))).thenReturn(responseEntity); } diff --git a/sechub-administration/build.gradle b/sechub-administration/build.gradle index d070c4f68b..5f084d5c08 100644 --- a/sechub-administration/build.gradle +++ b/sechub-administration/build.gradle @@ -7,6 +7,7 @@ */ dependencies { implementation project(':sechub-shared-kernel') + implementation project(':sechub-commons-encryption') // necessary to validate implementation library.springboot_starter_thymeleaf testImplementation project(':sechub-testframework') diff --git a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/AdministrationAPIConstants.java b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/AdministrationAPIConstants.java index 78defbfc1c..ed8a2aab44 100644 --- a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/AdministrationAPIConstants.java +++ b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/AdministrationAPIConstants.java @@ -106,6 +106,12 @@ private AdministrationAPIConstants() { public static final String API_CHANGE_PROJECT_ACCESSLEVEL = API_ADMINISTRATION + "project/{projectId}/accesslevel/{projectAccessLevel}"; + /* +-----------------------------------------------------------------------+ */ + /* +............................ Encryption................................+ */ + /* +-----------------------------------------------------------------------+ */ + public static final String API_ADMIN_ENCRYPTION_ROTATION = API_ADMINISTRATION + "encryption/rotate"; + public static final String API_ADMIN_ENCRYPTION_STATUS = API_ADMINISTRATION + "encryption/status"; + /* +-----------------------------------------------------------------------+ */ /* +............................ Anonymous ................................+ */ /* +-----------------------------------------------------------------------+ */ diff --git a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/encryption/AdministrationEncryptionRotationService.java b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/encryption/AdministrationEncryptionRotationService.java new file mode 100644 index 0000000000..10f140b32f --- /dev/null +++ b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/encryption/AdministrationEncryptionRotationService.java @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.administration.encryption; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.UserContextService; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionData; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionDataValidator; +import com.mercedesbenz.sechub.sharedkernel.logging.AuditLogService; +import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessage; +import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessageService; +import com.mercedesbenz.sechub.sharedkernel.messaging.IsSendingAsyncMessage; +import com.mercedesbenz.sechub.sharedkernel.messaging.MessageDataKeys; +import com.mercedesbenz.sechub.sharedkernel.messaging.MessageID; +import com.mercedesbenz.sechub.sharedkernel.usecases.encryption.UseCaseAdminStartsEncryptionRotation; + +@Service +public class AdministrationEncryptionRotationService { + + @Autowired + DomainMessageService domainMessageService; + + @Autowired + SecHubEncryptionDataValidator validator; + + @Autowired + AuditLogService auditLogService; + + @Autowired + UserContextService userContextService; + + @UseCaseAdminStartsEncryptionRotation(@Step(number = 2, name = "Service call", description = "Triggers rotation of encryption via domain message")) + @IsSendingAsyncMessage(MessageID.START_ENCRYPTION_ROTATION) + public void rotateEncryption(SecHubEncryptionData data) { + if (data == null) { + throw new IllegalArgumentException("data may not be null!"); + } + auditLogService.log("started encryption rotation. New cipher algorithm will be: {}, datasource type:{}, datasource: {}", data.getAlgorithm(), + data.getPasswordSourceType(), data.getPasswordSourceData()); + + String executedBy = userContextService.getUserId(); + + DomainMessage message = new DomainMessage(MessageID.START_ENCRYPTION_ROTATION); + message.set(MessageDataKeys.SECHUB_ENCRYPT_ROTATION_DATA, data); + message.set(MessageDataKeys.EXECUTED_BY, executedBy); + + domainMessageService.sendAsynchron(message); + } + +} diff --git a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/encryption/AdministrationEncryptionStatusService.java b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/encryption/AdministrationEncryptionStatusService.java new file mode 100644 index 0000000000..3e9dbf8486 --- /dev/null +++ b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/encryption/AdministrationEncryptionStatusService.java @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.administration.encryption; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubDomainEncryptionStatus; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionStatus; +import com.mercedesbenz.sechub.sharedkernel.logging.AuditLogService; +import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessage; +import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessageService; +import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessageSynchronousResult; +import com.mercedesbenz.sechub.sharedkernel.messaging.IsSendingSyncMessage; +import com.mercedesbenz.sechub.sharedkernel.messaging.MessageDataKeys; +import com.mercedesbenz.sechub.sharedkernel.messaging.MessageID; +import com.mercedesbenz.sechub.sharedkernel.usecases.encryption.UseCaseAdminFetchesEncryptionStatus; + +@Service +public class AdministrationEncryptionStatusService { + + @Autowired + DomainMessageService domainMessageService; + + @Autowired + AuditLogService auditLogService; + + @UseCaseAdminFetchesEncryptionStatus(@Step(number = 1, name = "Service call", description = "Services collects encryption status from domains via event bus")) + public SecHubEncryptionStatus fetchStatus() { + auditLogService.log("starts collecting encryption status"); + + SecHubEncryptionStatus sechubEncryptionStatus = new SecHubEncryptionStatus(); + collectScheduleEncryptionStatus(sechubEncryptionStatus); + + return sechubEncryptionStatus; + + } + + @IsSendingSyncMessage(MessageID.GET_ENCRYPTION_STATUS_SCHEDULE_DOMAIN) + private void collectScheduleEncryptionStatus(SecHubEncryptionStatus status) { + DomainMessage message = new DomainMessage(MessageID.GET_ENCRYPTION_STATUS_SCHEDULE_DOMAIN); + + DomainMessageSynchronousResult result = domainMessageService.sendSynchron(message); + SecHubDomainEncryptionStatus schedulerStatus = result.get(MessageDataKeys.SECHUB_DOMAIN_ENCRYPTION_STATUS); + + status.getDomains().add(schedulerStatus); + } + +} diff --git a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/encryption/EncryptionAdministrationRestController.java b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/encryption/EncryptionAdministrationRestController.java new file mode 100644 index 0000000000..b7cd81ebb7 --- /dev/null +++ b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/encryption/EncryptionAdministrationRestController.java @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.administration.encryption; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.MediaType; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import com.mercedesbenz.sechub.domain.administration.AdministrationAPIConstants; +import com.mercedesbenz.sechub.sharedkernel.Profiles; +import com.mercedesbenz.sechub.sharedkernel.RoleConstants; +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionData; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionDataValidator; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionStatus; +import com.mercedesbenz.sechub.sharedkernel.usecases.encryption.UseCaseAdminFetchesEncryptionStatus; +import com.mercedesbenz.sechub.sharedkernel.usecases.encryption.UseCaseAdminStartsEncryptionRotation; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.validation.Valid; + +/** + * The rest api for encryption done by a super admin. + * + * @author Albert Tregnaghi + * + */ +@RestController +@EnableAutoConfiguration +@RolesAllowed(RoleConstants.ROLE_SUPERADMIN) +@Profile({ Profiles.TEST, Profiles.ADMIN_ACCESS }) +public class EncryptionAdministrationRestController { + + @Autowired + AdministrationEncryptionRotationService administrationEncryptionRotationService; + + @Autowired + AdministrationEncryptionStatusService administrationStatusService; + + @Autowired + SecHubEncryptionDataValidator encryptionDataValidator; + + /* @formatter:off */ + @UseCaseAdminStartsEncryptionRotation(@Step(number=1,name="Rest call",description="Admin triggers rotation of encryption via REST", needsRestDoc =true)) + @RequestMapping(path = AdministrationAPIConstants.API_ADMIN_ENCRYPTION_ROTATION, method = RequestMethod.POST, produces= {MediaType.APPLICATION_JSON_VALUE}) + public void rotateEncryption(@RequestBody @Valid SecHubEncryptionData data) { + /* @formatter:on */ + administrationEncryptionRotationService.rotateEncryption(data); + } + + /* @formatter:off */ + @UseCaseAdminFetchesEncryptionStatus(@Step(number=1,name="Rest call",description="Admin fetches encryption status from domains via REST", needsRestDoc =true)) + @RequestMapping(path = AdministrationAPIConstants.API_ADMIN_ENCRYPTION_STATUS, method = RequestMethod.GET, produces= {MediaType.APPLICATION_JSON_VALUE}) + public SecHubEncryptionStatus fetchEncryptionStatus() { + /* @formatter:on */ + return administrationStatusService.fetchStatus(); + } + + @InitBinder + protected void initBinder(WebDataBinder binder) { + binder.setValidator(encryptionDataValidator); + } + +} diff --git a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/job/JobInformation.java b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/job/JobInformation.java index 63c3743c9f..dc34c95f03 100644 --- a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/job/JobInformation.java +++ b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/job/JobInformation.java @@ -44,11 +44,6 @@ public class JobInformation { * This is a free text field and shows up some information about */ public static final String COLUMN_INFO = "INFO"; - /** - * JSON configuration for this job. Interesting for administrators because of - * support - */ - public static final String COLUMN_CONFIGURATION = "CONFIGURATION"; /* +-----------------------------------------------------------------------+ */ /* +............................ JPQL .....................................+ */ @@ -61,7 +56,6 @@ public class JobInformation { public static final String PROPERTY_PROJECT_ID = "projectId"; public static final String PROPERTY_OWNER = "owner"; public static final String PROPERTY_SINCE = "since"; - public static final String PROPERTY_CONFIGURATION = "configuration"; public static final String QUERY_FIND_ALL_RUNNING_JOBS = "SELECT j FROM JobInformation j where j.status = com.mercedesbenz.sechub.domain.administration.job.JobStatus.RUNNING"; public static final String QUERY_DELETE_JOBINFORMATION_FOR_JOBUUID = "DELETE FROM JobInformation j WHERE j.jobUUID=:jobUUID"; @@ -95,9 +89,6 @@ public JobInformation(UUID jobUUID) { @Column(name = COLUMN_SINCE) // remark: we setup hibernate to use UTC settings - see application.properties LocalDateTime since; - @Column(name = COLUMN_CONFIGURATION) - String configuration; - @Column(name = COLUMN_INFO) String info; @@ -113,14 +104,6 @@ public void setOwner(String owner) { this.owner = owner; } - public String getConfiguration() { - return configuration; - } - - public void setConfiguration(String jsonConfiguration) { - this.configuration = jsonConfiguration; - } - public void setProjectId(String projectId) { this.projectId = projectId; } diff --git a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/job/JobInformationCreateService.java b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/job/JobInformationCreateService.java index 0277fa97ea..9d314f2fa2 100644 --- a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/job/JobInformationCreateService.java +++ b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/job/JobInformationCreateService.java @@ -37,26 +37,25 @@ public void createByMessage(JobMessage message, JobStatus status) { LOG.debug("Creating a new job information entry for project: {}, SecHub job: {}", projectId, jobUUID); - JobInformation entity = null; + JobInformation jobInformation = null; Optional existingInfo = repository.findById(jobUUID); if (existingInfo.isPresent()) { - entity = existingInfo.get(); + jobInformation = existingInfo.get(); LOG.warn("There was an existing information entity about SecHub job: {}. The entity will be reused and updated.", jobUUID); } else { - entity = new JobInformation(jobUUID); + jobInformation = new JobInformation(jobUUID); } - entity.setProjectId(projectId); - entity.setConfiguration(message.getConfiguration()); - entity.setOwner(message.getOwner()); - entity.setSince(message.getSince()); + jobInformation.setProjectId(projectId); + jobInformation.setOwner(message.getOwner()); + jobInformation.setSince(message.getSince()); - entity.setStatus(status); + jobInformation.setStatus(status); - entity = repository.save(entity); + jobInformation = repository.save(jobInformation); LOG.debug("Saved job information entry for project: {}, SecHub job: {}, ", projectId, jobUUID); } diff --git a/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/job/JobInformationCreateServiceTest.java b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/job/JobInformationCreateServiceTest.java index 353b9844a6..336c9d3df1 100644 --- a/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/job/JobInformationCreateServiceTest.java +++ b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/job/JobInformationCreateServiceTest.java @@ -39,7 +39,6 @@ void beforeEach() { @Test void createByMessage_no_existing_entity_new_entry_will_be_created_and_saved() { /* prepare */ - String configuration = "{ dummy }"; when(repository.findById(jobUUID)).thenReturn(Optional.empty()); @@ -50,7 +49,6 @@ void createByMessage_no_existing_entity_new_entry_will_be_created_and_saved() { message.setOwner("newOwner"); message.setProjectId("newProjectId"); message.setSince(since); - message.setConfiguration(configuration); JobStatus status = JobStatus.RUNNING; @@ -69,7 +67,6 @@ void createByMessage_no_existing_entity_new_entry_will_be_created_and_saved() { assertEquals("newOwner", savedJobInformation.getOwner()); assertEquals("newProjectId", savedJobInformation.getProjectId()); assertEquals(since, savedJobInformation.getSince()); - assertEquals(configuration, savedJobInformation.getConfiguration()); assertEquals(null, savedJobInformation.version); assertEquals(status, savedJobInformation.getStatus()); @@ -93,7 +90,6 @@ void createByMessage_an_existing_entity_entry_will_be_reused_and_saved() { message.setOwner("newOwner"); message.setProjectId("newProjectId"); message.setSince(since); - message.setConfiguration(configuration); JobStatus status = JobStatus.RUNNING; @@ -113,7 +109,6 @@ void createByMessage_an_existing_entity_entry_will_be_reused_and_saved() { assertEquals("newOwner", savedJobInformation.getOwner()); assertEquals("newProjectId", savedJobInformation.getProjectId()); assertEquals(since, savedJobInformation.getSince()); - assertEquals(configuration, savedJobInformation.getConfiguration()); assertEquals(42, savedJobInformation.version.intValue()); assertEquals(status, savedJobInformation.getStatus()); diff --git a/sechub-commons-core/build.gradle b/sechub-commons-core/build.gradle index f373ca021d..c3a3d14c85 100644 --- a/sechub-commons-core/build.gradle +++ b/sechub-commons-core/build.gradle @@ -3,8 +3,6 @@ dependencies { api spring_boot_dependency.slf4j_api - implementation 'org.bouncycastle:bcprov-jdk18on:1.73' - testImplementation spring_boot_dependency.junit_jupiter testImplementation spring_boot_dependency.mockito_core diff --git a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/environment/SecureEnvironmentVariableKeyValueRegistry.java b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/environment/SecureEnvironmentVariableKeyValueRegistry.java index 69382ab9cb..4734308770 100644 --- a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/environment/SecureEnvironmentVariableKeyValueRegistry.java +++ b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/environment/SecureEnvironmentVariableKeyValueRegistry.java @@ -111,7 +111,7 @@ public EnvironmentVariableKeyValueEntry build() { entry.variableName = variableName; if (entry.variableName == null) { - entry.variableName = key.toUpperCase().replace('.', '_'); + entry.variableName = key.toUpperCase().replace('.', '_').replace('-', '_'); } return entry; } diff --git a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/AbstractBinaryString.java b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/AbstractBinaryString.java deleted file mode 100644 index c94db89fd4..0000000000 --- a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/AbstractBinaryString.java +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -import java.util.Arrays; - -/** - * @author Jeremias Eppler - */ -public abstract class AbstractBinaryString implements BinaryString { - byte[] bytes; - - protected AbstractBinaryString(byte[] bytes) { - if (bytes == null) { - throw new IllegalArgumentException("Byte array cannot be null."); - } - - this.bytes = bytes; - } - - protected AbstractBinaryString(String string) { - if (string == null) { - throw new IllegalArgumentException("String cannot be null."); - } - - this.bytes = string.getBytes(); - } - - public abstract String toString(); - - public byte[] getBytes() { - // deep copy - return Arrays.copyOf(bytes, bytes.length); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + Arrays.hashCode(bytes); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - AbstractBinaryString other = (AbstractBinaryString) obj; - return Arrays.equals(bytes, other.bytes); - } -} diff --git a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/AesGcmSiv.java b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/AesGcmSiv.java deleted file mode 100644 index 72291d3e03..0000000000 --- a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/AesGcmSiv.java +++ /dev/null @@ -1,142 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.Provider; -import java.security.SecureRandom; -import java.security.Security; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import javax.crypto.spec.GCMParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; - -/** - * Providing access to AES-GCM-SIV - * - * AES-GCM-SIV is a nonce misuse-resistant authenticated encryption algorithm. - * - * For more information refer to - * RFC 8452 - * - * @author Jeremias Eppler - */ -public class AesGcmSiv implements PersistenceCipher { - - private SecretKey secret; - private Provider cryptoProvider; - private PersistenceCipherType cipherType; - - private static final String ALGORITHM = "AES/GCM-SIV/NoPadding"; - - /** - * The recommended initialization vector (iv) for AES-GCM-SIV is 12 bytes or 96 - * bits. - * - * For an explanation have a look at: - - * https://datatracker.ietf.org/doc/html/rfc8452#section-4 - - * https://crypto.stackexchange.com/questions/41601/aes-gcm-recommended-iv-size-why-12-bytes - */ - public static final int IV_LENGTH_IN_BYTES = 12; - - public static final int AUTHENTICATION_TAG_LENGTH_IN_BITS = 16 * 8; // 16 bytes (128 bits) - - private AesGcmSiv(SecretKey secret, PersistenceCipherType cipherType) { - this.secret = secret; - this.cipherType = cipherType; - cryptoProvider = new BouncyCastleProvider(); - Security.addProvider(cryptoProvider); - } - - public static AesGcmSiv create(BinaryString secret) throws InvalidKeyException { - AesGcmSiv instance = null; - - byte[] rawSecret = secret.getBytes(); - - if (rawSecret.length == 32 || rawSecret.length == 16) { - PersistenceCipherType cipherType = (rawSecret.length == 32) ? PersistenceCipherType.AES_GCM_SIV_256 : PersistenceCipherType.AES_GCM_SIV_128; - SecretKey secretKey = new SecretKeySpec(rawSecret, 0, rawSecret.length, "AES"); - - instance = new AesGcmSiv(secretKey, cipherType); - } else { - throw new InvalidKeyException("The secret has to be 128 or 256 bits long, but was " + (rawSecret.length * 8) + " bits long."); - } - - return instance; - } - - public BinaryString generateNewInitializationVector() { - return generateNewInitializationVector(BinaryStringEncodingType.BASE64); - } - - public BinaryString generateNewInitializationVector(BinaryStringEncodingType encodingType) { - byte[] initializationVector = new byte[IV_LENGTH_IN_BYTES]; - - SecureRandom random = new SecureRandom(); - random.nextBytes(initializationVector); - - return BinaryStringFactory.createFromBytes(initializationVector, encodingType); - } - - public BinaryString encrypt(String plaintext, BinaryString initializationVector) throws InvalidAlgorithmParameterException, InvalidKeyException { - return encrypt(plaintext, initializationVector, BinaryStringEncodingType.BASE64); - } - - public String decrypt(BinaryString ciphertext, BinaryString initializationVector) - throws InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { - Cipher cipher; - try { - cipher = Cipher.getInstance(ALGORITHM, cryptoProvider); - - SecretKeySpec keySpec = new SecretKeySpec(secret.getEncoded(), "AES"); - - GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(AUTHENTICATION_TAG_LENGTH_IN_BITS, initializationVector.getBytes()); - - cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec); - - byte[] ciphertextBytes = ciphertext.getBytes(); - - byte[] plaintextBytes = cipher.doFinal(ciphertextBytes); - - String plaintext = new String(plaintextBytes); - - return plaintext; - } catch (NoSuchAlgorithmException | NoSuchPaddingException providerException) { - throw new IllegalStateException("Decryption not possible, please check the provider", providerException); - } - } - - @Override - public PersistenceCipherType getCipherType() { - return cipherType; - } - - @Override - public BinaryString encrypt(String plaintext, BinaryString initializationVector, BinaryStringEncodingType encodingType) - throws InvalidAlgorithmParameterException, InvalidKeyException { - try { - Cipher cipher = Cipher.getInstance(ALGORITHM, cryptoProvider); - - SecretKeySpec keySpec = new SecretKeySpec(secret.getEncoded(), "AES"); - - GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(AUTHENTICATION_TAG_LENGTH_IN_BITS, initializationVector.getBytes()); - - cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmParameterSpec); - - byte[] ciphertext = cipher.doFinal(plaintext.getBytes()); - - return BinaryStringFactory.createFromBytes(ciphertext, encodingType); - } catch (NoSuchAlgorithmException | NoSuchPaddingException providerException) { - throw new IllegalStateException("Encryption not possible, please check the provider", providerException); - } catch (BadPaddingException | IllegalBlockSizeException paddingBlockException) { - throw new IllegalStateException("Should not occure. AES in GCM-SIV mode does not require padding.", paddingBlockException); - } - } -} diff --git a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/Base64String.java b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/Base64String.java deleted file mode 100644 index e957c61467..0000000000 --- a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/Base64String.java +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -import java.util.Base64; - -/** - * A base64 encoded string. - * - * @author Jeremias Eppler - * - */ -public class Base64String extends AbstractBinaryString { - - Base64String(byte[] bytes) { - super(bytes); - } - - Base64String(String string) { - super(string); - } - - @Override - public String toString() { - return Base64.getEncoder().encodeToString(bytes); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + java.util.Arrays.hashCode(bytes); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - Base64String other = (Base64String) obj; - return java.util.Arrays.equals(bytes, other.bytes); - } - - @Override - public BinaryStringEncodingType getType() { - return BinaryStringEncodingType.BASE64; - } -} diff --git a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/BinaryString.java b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/BinaryString.java deleted file mode 100644 index 94d2dd23d7..0000000000 --- a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/BinaryString.java +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -/** - * A binary string type which knows it's encoding. - * - * The binary string keeps it's internal representation in binary format. In - * addition, the binary string knows it's encoding (e.g. base64 etc.). - * - * @see BinaryStringEncodingType - * - * @author Jeremias Eppler - */ -public interface BinaryString { - public byte[] getBytes(); - - public String toString(); - - public BinaryStringEncodingType getType(); -} diff --git a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/BinaryStringEncodingType.java b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/BinaryStringEncodingType.java deleted file mode 100644 index 9fb39655b1..0000000000 --- a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/BinaryStringEncodingType.java +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -/** - * Encoding type of a binary string. - * - * @see BinaryString - * - * @author Jeremias Eppler - */ -public enum BinaryStringEncodingType { - /** - * A string with no encoding. - */ - PLAIN, - - /** - * A string encoded in hexadecimal format - */ - HEX, - - /** - * A string encoded in base64 format - */ - BASE64, -} diff --git a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/BinaryStringFactory.java b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/BinaryStringFactory.java deleted file mode 100644 index ba5cc2bc27..0000000000 --- a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/BinaryStringFactory.java +++ /dev/null @@ -1,171 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -import java.util.Base64; -import java.util.HexFormat; - -/** - * Creates {@link BinaryString} differently encoded strings. - * - * It can create {@link BinaryString} from differently encoded strings and - * returns a sub-type of {@link BinaryString} based on the desired output - * encoding. - * - * @author Jeremias Eppler - */ -public class BinaryStringFactory { - private static final BinaryStringEncodingType DEFAULT_ENCODING_TYPE = BinaryStringEncodingType.BASE64; - - public static BinaryString createFromBytes(byte[] bytes) { - return createFromBytes(bytes, DEFAULT_ENCODING_TYPE); - } - - public static BinaryString createFromBytes(byte[] bytes, BinaryStringEncodingType encodingType) { - if (bytes == null) { - throw new IllegalArgumentException("String cannot be null."); - } - - BinaryString binaryString; - - switch (encodingType) { - case BASE64: - binaryString = new Base64String(bytes); - break; - case HEX: - binaryString = new HexString(bytes); - break; - case PLAIN: - binaryString = new PlainString(bytes); - break; - default: - throw new IllegalArgumentException("Unknown binary string type"); - } - - return binaryString; - } - - /** - * Creates a {@link BinaryString} from a plain Java string. - * - * The BinaryString will be in the default encoding. - * - * @param string - * @return - */ - public static BinaryString createFromString(String string) { - return createFromString(string, DEFAULT_ENCODING_TYPE); - } - - /** - * Creates a {@link BinaryString} from a plain Java string. - * - * It returns the string in the specified encoding. - * - * @param string - * @param encodingType - * @return - */ - public static BinaryString createFromString(String string, BinaryStringEncodingType encodingType) { - if (string == null) { - throw new IllegalArgumentException("String cannot be null."); - } - - BinaryString binaryString; - - switch (encodingType) { - case BASE64: - binaryString = new Base64String(string); - break; - case HEX: - binaryString = new HexString(string); - break; - case PLAIN: - binaryString = new PlainString(string); - break; - default: - throw new IllegalArgumentException("Unknown binary string type"); - } - - return binaryString; - } - - /** - * Create a new {@link BinaryString} from a string which is in hexadecimal - * encoding. - * - * The given string needs to be hexadecimal encoded. - * - * For example: Given the string "616263" (orginal value "abc") it will decode - * it and store it as bytes. - * - * @param stringInHexFormat - * @param encodingType - * @return - */ - public static BinaryString createFromHex(String stringInHexFormat) { - return createFromHex(stringInHexFormat, DEFAULT_ENCODING_TYPE); - } - - /** - * Create a new {@link BinaryString} from a string which is in hexadecimal - * encoding. - * - * The given string needs to be hexadecimal encoded. - * - * For example: Given the string "616263" (orginal value "abc") it will decode - * it and store it as bytes. - * - * @param stringInHexFormat - * @param encodingType - * @return - */ - public static BinaryString createFromHex(String stringInHexFormat, BinaryStringEncodingType encodingType) { - if (stringInHexFormat == null) { - throw new IllegalArgumentException("String cannot be null."); - } - - HexFormat hexFormat = HexFormat.of(); - byte[] hexBytes = hexFormat.parseHex(stringInHexFormat); - - return createFromBytes(hexBytes, encodingType); - } - - /** - * Create a new {@link BinaryString} from a string which is base64 encoded. - * - * The given string needs to be base64 encoded. - * - * For example: Given the string "YWJj" (orginal value "abc") it will decode it - * and store it as bytes. - * - * @param stringInBase64Format - * @return - */ - public static BinaryString createFromBase64(String stringInBase64Format) { - return createFromBase64(stringInBase64Format, DEFAULT_ENCODING_TYPE); - } - - /** - * Create a new {@link BinaryString} from a string which is base64 encoded. - * - * The given string needs to be base64 encoded. - * - * For example: Given the string "YWJj" (orginal value "abc") it will decode it - * and store it as bytes. - * - * @param stringInBase64Format - * @param encodingType - * @return - */ - public static BinaryString createFromBase64(String stringInBase64Format, BinaryStringEncodingType encodingType) { - if (stringInBase64Format == null) { - throw new IllegalArgumentException("String cannot be null."); - } - - // if it cannot be decoded, - // it will throw an IllegalArgumentException - byte[] decoded = Base64.getDecoder().decode(stringInBase64Format); - - return createFromBytes(decoded, encodingType); - } -} diff --git a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/HexString.java b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/HexString.java deleted file mode 100644 index 81c8880297..0000000000 --- a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/HexString.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -import java.util.HexFormat; - -/** - * Hexadecimal encoded string. - * - * @author Jeremias Eppler - */ -public class HexString extends AbstractBinaryString { - - protected HexFormat hexFormat = HexFormat.of(); - - HexString(byte[] bytes) { - super(bytes); - } - - HexString(String string) { - super(string); - } - - @Override - public String toString() { - return hexFormat.formatHex(bytes); - } - - @Override - public BinaryStringEncodingType getType() { - return BinaryStringEncodingType.HEX; - } - -} diff --git a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/NoneCipher.java b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/NoneCipher.java deleted file mode 100644 index 65da105f74..0000000000 --- a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/NoneCipher.java +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; - -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; - -/** - * A cipher which does not encrypt anything. - * - * It can be used during testing or debugging. - * - * Furthermore, this cipher can be used to change the encoding of the data. For - * example, from plain to Base64. - * - * @author Jeremias Eppler - */ -public class NoneCipher implements PersistenceCipher { - private NoneCipher() { - } - - @Override - public PersistenceCipherType getCipherType() { - return PersistenceCipherType.NONE; - } - - public static PersistenceCipher create(BinaryString secret) throws InvalidKeyException { - return new NoneCipher(); - } - - @Override - public BinaryString encrypt(String plaintext, BinaryString initializationVector) throws InvalidAlgorithmParameterException, InvalidKeyException { - return encrypt(plaintext, initializationVector, BinaryStringEncodingType.PLAIN); - } - - @Override - public BinaryString encrypt(String plaintext, BinaryString initializationVector, BinaryStringEncodingType encodingType) - throws InvalidAlgorithmParameterException, InvalidKeyException { - return BinaryStringFactory.createFromString(plaintext, encodingType); - } - - @Override - public String decrypt(BinaryString ciphertext, BinaryString initializationVector) - throws InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - - return ciphertext.toString(); - } - - @Override - public BinaryString generateNewInitializationVector() { - return generateNewInitializationVector(BinaryStringEncodingType.PLAIN); - } - - @Override - public BinaryString generateNewInitializationVector(BinaryStringEncodingType encodingType) { - return BinaryStringFactory.createFromString("", encodingType); - } -} diff --git a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/PersistenceCipher.java b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/PersistenceCipher.java deleted file mode 100644 index bf8ff6049e..0000000000 --- a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/PersistenceCipher.java +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; - -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; - -/** - * Interface for cryptographic algorithms used to protect data at rest. - * - * "At rest" refers to data which is usually stored in a database, file or other - * persistent storage. - * - * @author Jeremias Eppler - */ -public interface PersistenceCipher { - public static PersistenceCipher create(BinaryString secret) throws InvalidKeyException { - return null; - } - - public BinaryString generateNewInitializationVector(); - - public BinaryString generateNewInitializationVector(BinaryStringEncodingType encodingType); - - public BinaryString encrypt(String plaintext, BinaryString initializationVector) throws InvalidAlgorithmParameterException, InvalidKeyException; - - public BinaryString encrypt(String plaintext, BinaryString initializationVector, BinaryStringEncodingType encodingType) - throws InvalidAlgorithmParameterException, InvalidKeyException; - - public String decrypt(BinaryString ciphertext, BinaryString initializationVector) - throws IllegalArgumentException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException; - - public PersistenceCipherType getCipherType(); -} diff --git a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/PersistenceCipherFactory.java b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/PersistenceCipherFactory.java deleted file mode 100644 index 76b56c83fb..0000000000 --- a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/PersistenceCipherFactory.java +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -import java.security.InvalidKeyException; - -/** - * The factory creates persistent ciphers used to protect data at rest. - * - * The algorithms protect both the confidentiality and integrity of the - * information at rest. - * - * In addition, the ciphers which it creates are nonce (initialization vector) - * misuse-resistant. - * - * @author Jeremias Eppler - */ -public class PersistenceCipherFactory { - - /** - * Creates a new persistent cipher based on the type and secret. - * - * @param cipherType - * @param secret - * @return - * @throws InvalidKeyException - */ - public static PersistenceCipher create(PersistenceCipherType cipherType, BinaryString secret) throws InvalidKeyException { - PersistenceCipher cipher = null; - - switch (cipherType) { - case NONE: - cipher = NoneCipher.create(secret); - break; - case AES_GCM_SIV_256: - case AES_GCM_SIV_128: - cipher = AesGcmSiv.create(secret); - break; - default: - throw new IllegalArgumentException("Unable to create cipher. Unknown cipher."); - } - - return cipher; - } -} diff --git a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/PersistenceCipherType.java b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/PersistenceCipherType.java deleted file mode 100644 index 8442acca71..0000000000 --- a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/PersistenceCipherType.java +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -/** - * Contains the available ciphers. - * - * @author Jeremias Eppler - */ -public enum PersistenceCipherType { - /** - * A special cipher type which does not protect data. - * - * This is intended for testing. - */ - NONE, - - /** - * Advanced Encryption Standard (AES) 128 bit key (secret) in Galois/Counter - * Mode (GCM) and synthetic initialization vector (SIV). - * - * AES GCM-SIV is a nonce (initialization vector) misuse-resistant authenticated - * encryption. - * - * @see https://datatracker.ietf.org/doc/html/rfc8452 - */ - AES_GCM_SIV_128, - - /** - * Advanced Encryption Standard (AES) 256 bit key (secret) in Galois/Counter - * Mode (GCM) and synthetic initialization vector (SIV). - * - * AES GCM-SIV is a nonce (initialization vector) misuse-resistant authenticated - * encryption. - * - * @see https://datatracker.ietf.org/doc/html/rfc8452 - */ - AES_GCM_SIV_256; -} diff --git a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/PlainString.java b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/PlainString.java deleted file mode 100644 index 8af0fac45f..0000000000 --- a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/PlainString.java +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -/** - * A wrapper around the string class. - * - * The string class in Java is final and therefore it is impossible to inherited - * from. - * - * @author Jeremias Eppler - */ -public class PlainString extends AbstractBinaryString { - - PlainString(byte[] bytes) { - super(bytes); - } - - PlainString(String string) { - super(string); - } - - @Override - public byte[] getBytes() { - return bytes; - } - - @Override - public BinaryStringEncodingType getType() { - return BinaryStringEncodingType.PLAIN; - } - - @Override - public String toString() { - return new String(bytes); - } - -} diff --git a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/RotationStrategy.java b/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/RotationStrategy.java deleted file mode 100644 index a118d9b87c..0000000000 --- a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/RotationStrategy.java +++ /dev/null @@ -1,199 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; - -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; - -/** - * The rotation strategy helps to rotate the cipher text. - * - * It can be used to rotate (re-encrpyt) by using: - different initialization - * vectors - different secret keys - different algorithms - * - * or a combination of the above. - * - * @author Jeremias Eppler - */ -public class RotationStrategy { - private PersistenceCipher currentCipher; - private PersistenceCipher newCipher; - private boolean performSecretRotation = false; - - private RotationStrategy(PersistenceCipher currentCipher, PersistenceCipher newCipher, boolean performSecretRotation) { - this.currentCipher = currentCipher; - this.newCipher = newCipher; - this.performSecretRotation = performSecretRotation; - } - - /** - * Create a new rotation strategy which only allows to rotate the initialization - * vector. - * - * @param secret - * @param cipher - * @return - * @throws InvalidKeyException - */ - public static RotationStrategy createInitializationVectorOnlyRotationStrategy(BinaryString secret, PersistenceCipherType cipher) - throws InvalidKeyException { - PersistenceCipher currentCipher = PersistenceCipherFactory.create(cipher, secret); - PersistenceCipher newCipher = PersistenceCipherFactory.create(cipher, secret); - - boolean performSecretRotation = false; - - return new RotationStrategy(currentCipher, newCipher, performSecretRotation); - } - - /** - * Create a new rotation strategy which allows to rotate the secret. - * - * This is useful in case of a secret leak. - * - * @param currentSecret - * @param newSecret - * @param cipher - * @return - * @throws InvalidKeyException - */ - public static RotationStrategy createSecretRotationStrategy(BinaryString currentSecret, BinaryString newSecret, PersistenceCipherType cipher) - throws InvalidKeyException { - PersistenceCipher currentCipher = PersistenceCipherFactory.create(cipher, currentSecret); - PersistenceCipher newCipher = PersistenceCipherFactory.create(cipher, newSecret); - - boolean performSecretRotation = true; - - return new RotationStrategy(currentCipher, newCipher, performSecretRotation); - } - - /** - * Create a new rotation strategy which allows to rotate the secret and ciphers. - * - * This is useful if the underling cryptographic cipher or mode of operation is - * deemed as insecure. - * - * For example, this is the case with Data Encryption Standard (DES) and the - * Triple DES variant. - * - * - * @param currentSecret - * @param newSecret - * @param currentCipherType - * @param newCipherType - * @return - * @throws InvalidKeyException - */ - public static RotationStrategy createCipherAndSecretRotationStrategy(BinaryString currentSecret, BinaryString newSecret, - PersistenceCipherType currentCipherType, PersistenceCipherType newCipherType) throws InvalidKeyException { - PersistenceCipher currentCipher = PersistenceCipherFactory.create(currentCipherType, currentSecret); - PersistenceCipher newCipher = PersistenceCipherFactory.create(newCipherType, newSecret); - - boolean performSecretRotation = true; - - return new RotationStrategy(currentCipher, newCipher, performSecretRotation); - } - - /** - * Rotate the encrypted cipher text using the same initialization vector. - * - * The given cipher text is decrypted and encrypted again using the given - * initialization vector the old and the new cipher text. - * - * @param cipherText - * @param initializationVector - * @return - * @throws InvalidKeyException - * @throws IllegalArgumentException - * @throws InvalidAlgorithmParameterException - * @throws IllegalBlockSizeException - * @throws BadPaddingException - */ - public BinaryString rotate(BinaryString cipherText, BinaryString initializationVector) - throws InvalidKeyException, IllegalArgumentException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - return rotate(cipherText, initializationVector, null, cipherText.getType()); - } - - /** - * Rotate the encrypted cipher text using different initialization vectors. - * - * The given cipher text is decrypted and encrypted again using two different - * initialization vectors. - * - * @param cipherText - * @param initializationVector - * @param newIntializationVector - * @return - * @throws InvalidKeyException - * @throws IllegalArgumentException - * @throws InvalidAlgorithmParameterException - * @throws IllegalBlockSizeException - * @throws BadPaddingException - */ - public BinaryString rotate(BinaryString cipherText, BinaryString initializationVector, BinaryString newIntializationVector) - throws InvalidKeyException, IllegalArgumentException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - return rotate(cipherText, initializationVector, newIntializationVector, cipherText.getType()); - } - - /** - * Rotate the encrypted cipher text using different initialization vectors and - * the given encoding type. - * - * The given cipher text is decrypted and encrypted again using two different - * initialization vectors. - * - * The resulting cipher text is returned in the specified - * {@link BinaryStringEncodingType}. - * - * @param cipherText - * @param initializationVector - * @param newIntializationVector - * @param newBinaryStringEncoding - * @return - * @throws InvalidKeyException - * @throws IllegalArgumentException - * @throws InvalidAlgorithmParameterException - * @throws IllegalBlockSizeException - * @throws BadPaddingException - */ - public BinaryString rotate(BinaryString cipherText, BinaryString initializationVector, BinaryString newIntializationVector, - BinaryStringEncodingType newBinaryStringEncoding) - throws InvalidKeyException, IllegalArgumentException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - if (cipherText == null) { - throw new IllegalArgumentException("The ciphertext cannot be null!"); - } - - if (initializationVector == null) { - throw new IllegalArgumentException("The initialization vector (nonce) cannot be null!"); - } - - String plainText = currentCipher.decrypt(cipherText, initializationVector); - - BinaryString newCipherText = null; - - if (newIntializationVector != null) { - newCipherText = newCipher.encrypt(plainText, newIntializationVector); - } else { - newCipherText = newCipher.encrypt(plainText, initializationVector); - } - - return newCipherText; - } - - public PersistenceCipherType getCurrentCipher() { - return currentCipher.getCipherType(); - } - - public PersistenceCipherType getNewCipher() { - return newCipher.getCipherType(); - } - - public boolean isSecretRotationStrategy() { - return performSecretRotation; - } - - public boolean isCipherRotationStrategy() { - return currentCipher.getCipherType() != newCipher.getCipherType(); - } -} diff --git a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/AesGcmSivTest.java b/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/AesGcmSivTest.java deleted file mode 100644 index d011f15ae7..0000000000 --- a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/AesGcmSivTest.java +++ /dev/null @@ -1,377 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -import static org.junit.jupiter.api.Assertions.*; - -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - -import javax.crypto.AEADBadTagException; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; - -import org.junit.jupiter.api.Test; - -public class AesGcmSivTest { - private static final String LONG_TEXT = "Hello world, this is long text with different emojis. Today, I had for breakfast two 🥐, 1 🥑 and some 🥪. That made me happy ☺️!"; - - @Test - void generate_new_initialization_vector() throws InvalidKeyException { - /* prepare */ - BinaryString secret = new Base64String("a".repeat(32)); - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* execute */ - BinaryString initializationVector = cipher.generateNewInitializationVector(); - - /* test */ - assertEquals(AesGcmSiv.IV_LENGTH_IN_BYTES, initializationVector.getBytes().length); - } - - @Test - void create_secret_32_bytes() throws InvalidKeyException { - /* prepare */ - BinaryString secret = new Base64String("a".repeat(32)); - - /* execute */ - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* test */ - assertNotNull(cipher); - assertEquals(PersistenceCipherType.AES_GCM_SIV_256, cipher.getCipherType()); - } - - @Test - void create_secret_16_bytes() throws InvalidKeyException { - /* prepare */ - BinaryString secret = new Base64String("a".repeat(16)); - - /* execute */ - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* test */ - assertNotNull(cipher); - assertEquals(PersistenceCipherType.AES_GCM_SIV_128, cipher.getCipherType()); - } - - @Test - void create_secret_secret_6_bytes_invalid() { - /* prepare */ - BinaryString secret = new Base64String("abcdef"); - - /* execute + test */ - assertThrows(InvalidKeyException.class, () -> { - AesGcmSiv.create(secret); - }); - } - - @Test - void create_secret_secret_31_bytes_invalid() { - /* prepare */ - BinaryString secret = new Base64String("a".repeat(31)); - - /* execute + test */ - assertThrows(InvalidKeyException.class, () -> { - AesGcmSiv.create(secret); - }); - } - - @Test - void encrypt__aes_256() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, - IllegalBlockSizeException, BadPaddingException { - - /* prepare */ - BinaryString secret = new Base64String("w".repeat(32)); - String plaintext = "bca"; - String expectedCiphertext = "1qKKtEpM2ppl4wWrJxJo0MiFdw=="; - BinaryString initializationVector = new Base64String("i".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES)); - - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* execute */ - BinaryString ciphertext = cipher.encrypt(plaintext, initializationVector); - - /* test */ - assertEquals(expectedCiphertext, ciphertext.toString()); - assertEquals(PersistenceCipherType.AES_GCM_SIV_256, cipher.getCipherType()); - } - - @Test - void encrypt__aes_256_initialization_vector_too_short() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, - InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - - /* prepare */ - BinaryString secret = new Base64String("w".repeat(32)); - String plaintext = "bca"; - BinaryString initializationVector = new Base64String("abc".repeat(2)); - - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* execute */ - Exception exception = assertThrows(InvalidAlgorithmParameterException.class, () -> { - cipher.encrypt(plaintext, initializationVector); - }); - - /* test */ - assertEquals("Invalid nonce", exception.getMessage()); - } - - @Test - void encrypt__aes_256_initialization_vector_too_long() throws InvalidKeyException, InvalidAlgorithmParameterException { - - /* prepare */ - BinaryString secret = new Base64String("w".repeat(32)); - String plaintext = "bca"; - BinaryString initializationVector = new Base64String("abc".repeat(50)); - - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* execute */ - Exception exception = assertThrows(InvalidAlgorithmParameterException.class, () -> { - cipher.encrypt(plaintext, initializationVector); - }); - - /* test */ - assertEquals("Invalid nonce", exception.getMessage()); - } - - @Test - void decrypt__aes_256() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, - IllegalBlockSizeException, BadPaddingException { - /* prepare */ - BinaryString secret = new Base64String("w".repeat(32)); - String expectedPlaintext = "bca"; - BinaryString ciphertext = BinaryStringFactory.createFromBase64("1qKKtEpM2ppl4wWrJxJo0MiFdw==", BinaryStringEncodingType.BASE64); - BinaryString initializationVector = new Base64String("i".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES)); - - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* execute */ - String plaintext = cipher.decrypt(ciphertext, initializationVector); - - /* test */ - assertEquals(expectedPlaintext, plaintext); - } - - @Test - void encrypt__aes_128() throws InvalidKeyException, InvalidAlgorithmParameterException { - /* prepare */ - BinaryString secret = new Base64String("a".repeat(16)); - String plaintext = "bca"; - String expectedCiphertext = "yGcKhuWbewS+R4tlegECshiTSQ=="; - BinaryString initializationVector = new Base64String("i".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES)); - - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* execute */ - BinaryString ciphertext = cipher.encrypt(plaintext, initializationVector); - - /* test */ - assertEquals(expectedCiphertext, ciphertext.toString()); - assertEquals(PersistenceCipherType.AES_GCM_SIV_128, cipher.getCipherType()); - } - - @Test - void encrypt__aes_256_hex_format_and_emojis() throws InvalidKeyException, InvalidAlgorithmParameterException { - /* prepare */ - BinaryString secret = new HexString("🥦🥕🥔🫘🥒🫑🌽🍆"); - String plaintext = "Hello 👋, welcome to 🌐."; - String expectedCiphertext = "d09be77ddd8dbac86b69b0f5f554faef740555ac93f12aedfdf62700e4ea3016e03dacc105f32f114791d8e6"; - BinaryString initializationVector = new Base64String("🧅".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES / 4)); - - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* execute */ - BinaryString ciphertext = cipher.encrypt(plaintext, initializationVector, BinaryStringEncodingType.HEX); - - /* test */ - assertEquals(expectedCiphertext, ciphertext.toString()); - assertEquals(PersistenceCipherType.AES_GCM_SIV_256, cipher.getCipherType()); - } - - @Test - void encrypt__aes_128_base64_format_and_emojis() throws InvalidKeyException, InvalidAlgorithmParameterException { - /* prepare */ - BinaryString secret = new Base64String("🍐🍌🍓🍉"); - - String plaintext = "Hello 👋, welcome to 🌐."; - String expectedCiphertext = "Qu7ICJBGMw9dAPPBWx86e5bjOq3YKC+x25n/YkluWZAGdSna08tKaE78pMk="; - BinaryString initializationVector = new Base64String("🧅".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES / 4)); - - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* execute */ - BinaryString ciphertext = cipher.encrypt(plaintext, initializationVector, BinaryStringEncodingType.BASE64); - - /* test */ - assertEquals(expectedCiphertext, ciphertext.toString()); - assertEquals(PersistenceCipherType.AES_GCM_SIV_128, cipher.getCipherType()); - } - - @Test - void decrypt__aes_128() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, - IllegalBlockSizeException, BadPaddingException { - /* prepare */ - BinaryString secret = new Base64String("a".repeat(16)); - String expectedPlaintext = "bca"; - BinaryString cipherText = BinaryStringFactory.createFromBase64("yGcKhuWbewS+R4tlegECshiTSQ==", BinaryStringEncodingType.BASE64); - BinaryString initializationVector = new Base64String("i".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES)); - - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* execute */ - String plaintext = cipher.decrypt(cipherText, initializationVector); - - /* test */ - assertEquals(expectedPlaintext, plaintext); - } - - @Test - void decrypt__aes_128_wrong_cipher_text() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, - IllegalBlockSizeException, BadPaddingException { - /* prepare */ - BinaryString secret = new Base64String("a".repeat(16)); - BinaryString ciphertext = new Base64String("hello world, this is base 64 encoded"); - BinaryString initializationVector = new Base64String("i".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES)); - - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* execute */ - Exception exception = assertThrows(AEADBadTagException.class, () -> { - cipher.decrypt(ciphertext, initializationVector); - }); - - /* test */ - assertEquals("mac check failed", exception.getMessage()); - } - - @Test - void decrypt__aes_256_wrong_cipher_text() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, - IllegalBlockSizeException, BadPaddingException { - - /* prepare */ - BinaryString secret = new Base64String("a".repeat(32)); - BinaryString ciphertext = new Base64String("hello world, this is base 64 encoded"); - BinaryString initializationVector = new Base64String("i".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES)); - - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* execute */ - Exception exception = assertThrows(AEADBadTagException.class, () -> { - cipher.decrypt(ciphertext, initializationVector); - }); - - /* test */ - assertEquals("mac check failed", exception.getMessage()); - } - - @Test - void encrypt__aes_128_long_text_with_emojis() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, - InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - - /* prepare */ - BinaryString secret = new Base64String("a".repeat(16)); - String plaintext = LONG_TEXT; - BinaryString expectedCiphertext = BinaryStringFactory.createFromBase64( - "28/RdEWgYbpbiraiWcSo58+8sfCRRQpSoZiiFqNsYN8tLLVE6AXeQjxh4zazK65G7T0dmFnqrbyx6aRUB+7I6guFXMxqjRij9HdRkae4OalWZVNtCs2+mjBBMNOB5Ke2bgIcYDZbDMRWceBtJnE5PKg7vxrNFgR+8uFw9ejbRVxzGTbkyNeh48QVT9Knk7LpmqQ/eHFTvsvnD0M=", - BinaryStringEncodingType.BASE64); - BinaryString initializationVector = new Base64String("i".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES)); - - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* execute */ - BinaryString ciphertext = cipher.encrypt(plaintext, initializationVector); - - /* test */ - assertEquals(expectedCiphertext, ciphertext); - } - - @Test - void encrypt__aes_256_long_text_with_emojis() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, - InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - - /* prepare */ - BinaryString secret = new Base64String("a".repeat(32)); - String plaintext = LONG_TEXT; - BinaryString expectedCiphertext = BinaryStringFactory.createFromBase64( - "E2RrqhXtKG39okWxxvw3d4NQ+DXr2+Qa78JvdpHS4+FOckRECTkjoX2JfZNKHP3on0sDO1q8uTc+BY9QJkMK+MsWzp8YT4SR0UxWo7uy5SSPMXOLLcQg0vzTOTdgo00vPQy34vogNYO1V/TTzOzzP6Ng0kT9TDsYUWu+v0y3uZw/ujl2X8bP8Nfrp2ZRMrgfpj7NbjQQd8hD5AY=", - BinaryStringEncodingType.BASE64); - BinaryString initializationVector = new Base64String("i".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES)); - - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* execute */ - BinaryString ciphertext = cipher.encrypt(plaintext, initializationVector); - - /* test */ - assertEquals(expectedCiphertext, ciphertext); - } - - @Test - void decrypt__aes_128_long_text_with_emojis() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, - InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - - /* prepare */ - BinaryString secret = new Base64String("a".repeat(16)); - String expectedPlaintext = LONG_TEXT; - BinaryString ciphertext = BinaryStringFactory.createFromBase64( - "28/RdEWgYbpbiraiWcSo58+8sfCRRQpSoZiiFqNsYN8tLLVE6AXeQjxh4zazK65G7T0dmFnqrbyx6aRUB+7I6guFXMxqjRij9HdRkae4OalWZVNtCs2+mjBBMNOB5Ke2bgIcYDZbDMRWceBtJnE5PKg7vxrNFgR+8uFw9ejbRVxzGTbkyNeh48QVT9Knk7LpmqQ/eHFTvsvnD0M=", - BinaryStringEncodingType.BASE64); - BinaryString initializationVector = new Base64String("i".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES)); - - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* execute */ - String plaintext = cipher.decrypt(ciphertext, initializationVector); - - /* test */ - assertEquals(expectedPlaintext, plaintext); - } - - @Test - void decrypt__aes_256_long_text_with_emojis() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, - InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - - /* prepare */ - BinaryString secret = new Base64String("a".repeat(32)); - String expectedPlaintext = LONG_TEXT; - BinaryString ciphertext = BinaryStringFactory.createFromBase64( - "E2RrqhXtKG39okWxxvw3d4NQ+DXr2+Qa78JvdpHS4+FOckRECTkjoX2JfZNKHP3on0sDO1q8uTc+BY9QJkMK+MsWzp8YT4SR0UxWo7uy5SSPMXOLLcQg0vzTOTdgo00vPQy34vogNYO1V/TTzOzzP6Ng0kT9TDsYUWu+v0y3uZw/ujl2X8bP8Nfrp2ZRMrgfpj7NbjQQd8hD5AY=", - BinaryStringEncodingType.BASE64); - BinaryString initializationVector = new Base64String("i".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES)); - - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* execute */ - String plaintext = cipher.decrypt(ciphertext, initializationVector); - - /* test */ - assertEquals(expectedPlaintext, plaintext); - } - - @Test - void getCipherType_aes_256() throws InvalidKeyException { - /* prepare */ - BinaryString secret = new Base64String("a".repeat(32)); - - /* execute */ - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* test */ - assertEquals(PersistenceCipherType.AES_GCM_SIV_256, cipher.getCipherType()); - } - - @Test - void getCiphersType_aes_128() throws InvalidKeyException { - /* prepare */ - BinaryString secret = new Base64String("a".repeat(16)); - - /* execute */ - AesGcmSiv cipher = AesGcmSiv.create(secret); - - /* test */ - assertEquals(PersistenceCipherType.AES_GCM_SIV_128, cipher.getCipherType()); - } -} diff --git a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/Base64StringTest.java b/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/Base64StringTest.java deleted file mode 100644 index 40e6fb3fd1..0000000000 --- a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/Base64StringTest.java +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -public class Base64StringTest { - @Test - void from_string() { - /* prepare */ - String string = "Hello"; - String expectedString = "SGVsbG8="; - - /* execute + test */ - assertEquals(expectedString, new Base64String(string).toString()); - } - - @Test - void from_string_null_throw_illegal_argument_exception() { - /* execute */ - Exception exception = assertThrows(IllegalArgumentException.class, () -> { - new Base64String((String) null); - }); - - /* test */ - assertEquals("String cannot be null.", exception.getMessage()); - } - - @Test - void from_string_unicode() { - String string = "Hello 🦄"; - String expectedString = "SGVsbG8g8J+mhA=="; - - assertEquals(expectedString, new Base64String(string).toString()); - } - - @Test - void from_bytes() { - byte[] bytes = "Hello".getBytes(); - String expectedString = "SGVsbG8="; - - assertEquals(expectedString, new Base64String(bytes).toString()); - } - - @Test - void from_bytes_unicode() { - byte[] bytes = "Hello 🦄".getBytes(); - String expectedString = "SGVsbG8g8J+mhA=="; - - assertEquals(expectedString, new Base64String(bytes).toString()); - } - - @Test - void from_bytes_null_throw_illegal_argument_exception() { - Exception exception = assertThrows(IllegalArgumentException.class, () -> { - new Base64String((byte[]) null); - }); - - assertEquals("Byte array cannot be null.", exception.getMessage()); - } - - @Test - void toString_unicode_test() { - String string = "Hello 🦄"; - String expectedString = "SGVsbG8g8J+mhA=="; - - assertEquals(expectedString, new Base64String(string).toString()); - } - - @Test - void equals_test() { - String string = "I like 🍍"; - - Base64String b64String = new Base64String(string); - Base64String b64String2 = new Base64String(string); - - assertEquals(b64String, b64String2); - assertTrue(b64String.equals(b64String2)); - assertEquals(b64String.hashCode(), b64String2.hashCode()); - } - - @Test - void test_immutablitiy() { - String string = "A 🐺 in a 🐑 skin"; - Base64String b64String = new Base64String(string); - - assertFalse(string == b64String.toString()); - assertFalse(string.getBytes() == b64String.getBytes()); - } -} diff --git a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/BinaryStringFactoryTest.java b/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/BinaryStringFactoryTest.java deleted file mode 100644 index 856f9d6bd3..0000000000 --- a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/BinaryStringFactoryTest.java +++ /dev/null @@ -1,271 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -public class BinaryStringFactoryTest { - @Test - void create_from_string_no_type_given() { - /* prepare */ - String string = "hello"; - - /* execute */ - BinaryString binaryString = BinaryStringFactory.createFromString(string); - - /* test */ - assertNotNull(binaryString); - assertEquals(binaryString.getType(), BinaryStringEncodingType.BASE64); - assertTrue(binaryString instanceof Base64String); - assertEquals("aGVsbG8=", binaryString.toString()); - } - - @Test - void create_from_string_base64_type_given() { - /* prepare */ - String string = "Hello 🌌!"; - - /* execute */ - BinaryString binaryString = BinaryStringFactory.createFromString(string, BinaryStringEncodingType.BASE64); - - /* test */ - assertNotNull(binaryString); - assertEquals(binaryString.getType(), BinaryStringEncodingType.BASE64); - assertTrue(binaryString instanceof Base64String); - assertEquals("SGVsbG8g8J+MjCE=", binaryString.toString()); - } - - @Test - void create_from_base64_no_type_given() { - /* prepare */ - String string = "SGVsbG8g8J+MjCE="; - - /* execute */ - BinaryString binaryString = BinaryStringFactory.createFromBase64(string); - - /* test */ - assertNotNull(binaryString); - assertEquals(binaryString.getType(), BinaryStringEncodingType.BASE64); - assertTrue(binaryString instanceof Base64String); - assertEquals("SGVsbG8g8J+MjCE=", binaryString.toString()); - } - - @Test - void createFromBase64_null_input() { - /* execute */ - Exception exception = assertThrows(IllegalArgumentException.class, () -> { - BinaryStringFactory.createFromBase64(null); - }); - - /* test */ - assertEquals("String cannot be null.", exception.getMessage()); - } - - @Test - void createFromHex_null_input() { - /* execute */ - Exception exception = assertThrows(IllegalArgumentException.class, () -> { - BinaryStringFactory.createFromHex(null); - }); - - /* test */ - assertEquals("String cannot be null.", exception.getMessage()); - } - - @Test - void create_from_string_plain_type_given() { - /* prepare */ - String string = "hello"; - - /* execute */ - BinaryString binaryString = BinaryStringFactory.createFromString(string, BinaryStringEncodingType.PLAIN); - - /* test */ - assertNotNull(binaryString); - assertEquals(binaryString.getType(), BinaryStringEncodingType.PLAIN); - assertTrue(binaryString instanceof PlainString); - assertEquals("hello", binaryString.toString()); - } - - @Test - void create_from_string_hex_type_given() { - /* prepare */ - String string = "hello"; - - /* execute */ - BinaryString binaryString = BinaryStringFactory.createFromString(string, BinaryStringEncodingType.HEX); - - /* test */ - assertNotNull(binaryString); - assertEquals(BinaryStringEncodingType.HEX, binaryString.getType()); - assertTrue(binaryString instanceof HexString); - assertEquals("68656c6c6f", binaryString.toString()); - } - - @Test - void create_from_string_no_type_given_and_input_null() { - /* prepare */ - String string = null; - - /* execute */ - Exception exception = assertThrows(IllegalArgumentException.class, () -> { - BinaryStringFactory.createFromString(string); - }); - - /* test */ - assertEquals("String cannot be null.", exception.getMessage()); - } - - @Test - void create_from_bytes_no_type_given() { - /* prepare */ - byte[] bytes = new byte[] { 104, 101, 108, 108, 111 }; - - /* execute */ - BinaryString binaryString = BinaryStringFactory.createFromBytes(bytes); - - /* test */ - assertNotNull(binaryString); - assertEquals(BinaryStringEncodingType.BASE64, binaryString.getType()); - assertTrue(binaryString instanceof Base64String); - assertEquals("aGVsbG8=", binaryString.toString()); - } - - @Test - void create_from_bytes_base64_type_given() { - /* prepare */ - byte[] bytes = new byte[] { 104, 101, 108, 108, 111 }; - - /* execute */ - BinaryString binaryString = BinaryStringFactory.createFromBytes(bytes, BinaryStringEncodingType.BASE64); - - /* test */ - assertNotNull(binaryString); - assertEquals(BinaryStringEncodingType.BASE64, binaryString.getType()); - assertTrue(binaryString instanceof Base64String); - assertEquals("aGVsbG8=", binaryString.toString()); - } - - @Test - void create_from_bytes_plain_type_given() { - /* prepare */ - byte[] bytes = new byte[] { 104, 101, 108, 108, 111 }; - - /* execute */ - BinaryString binaryString = BinaryStringFactory.createFromBytes(bytes, BinaryStringEncodingType.PLAIN); - - /* test */ - assertNotNull(binaryString); - assertEquals(BinaryStringEncodingType.PLAIN, binaryString.getType()); - assertTrue(binaryString instanceof PlainString); - assertEquals("hello", binaryString.toString()); - } - - @Test - void create_from_bytes_hex_type_given() { - /* prepare */ - byte[] bytes = new byte[] { 104, 101, 108, 108, 111 }; - - /* execute */ - BinaryString binaryString = BinaryStringFactory.createFromBytes(bytes, BinaryStringEncodingType.HEX); - - /* test */ - assertNotNull(binaryString); - assertEquals(BinaryStringEncodingType.HEX, binaryString.getType()); - assertTrue(binaryString instanceof HexString); - assertEquals("68656c6c6f", binaryString.toString()); - } - - @Test - void create_from_bytes_no_type_given_and_input_null() { - /* prepare */ - byte[] bytes = null; - - /* execute */ - Exception exception = assertThrows(IllegalArgumentException.class, () -> { - BinaryStringFactory.createFromBytes(bytes); - }); - - /* test */ - assertEquals("String cannot be null.", exception.getMessage()); - } - - @Test - void createFromHex_from_string_plain_type_given() { - /* prepare */ - String string = "68656c6c6f"; - - /* execute */ - BinaryString binaryString = BinaryStringFactory.createFromHex(string, BinaryStringEncodingType.PLAIN); - - /* test */ - assertNotNull(binaryString); - assertEquals(BinaryStringEncodingType.PLAIN, binaryString.getType()); - assertTrue(binaryString instanceof PlainString); - assertEquals("hello", binaryString.toString()); - } - - @Test - void createFromHex_base64_type_given() { - /* prepare */ - String string = "68656c6c6f"; - - /* execute */ - BinaryString binaryString = BinaryStringFactory.createFromHex(string, BinaryStringEncodingType.BASE64); - - /* test */ - assertNotNull(binaryString); - assertEquals(BinaryStringEncodingType.BASE64, binaryString.getType()); - assertTrue(binaryString instanceof Base64String); - assertEquals("aGVsbG8=", binaryString.toString()); - } - - @Test - void createFromHex_no_type_given() { - /* prepare */ - String string = "68656c6c6f"; - - /* execute */ - BinaryString binaryString = BinaryStringFactory.createFromHex(string); - - /* test */ - assertNotNull(binaryString); - assertEquals(BinaryStringEncodingType.BASE64, binaryString.getType()); - assertTrue(binaryString instanceof Base64String); - assertEquals("aGVsbG8=", binaryString.toString()); - } - - @Test - void createFromBase64_plain_type_given() { - /* prepare */ - String string = "aGVsbG8="; - - /* execute */ - BinaryString binaryString = BinaryStringFactory.createFromBase64(string, BinaryStringEncodingType.PLAIN); - - /* test */ - assertNotNull(binaryString); - assertEquals(BinaryStringEncodingType.PLAIN, binaryString.getType()); - assertTrue(binaryString instanceof PlainString); - assertEquals("hello", binaryString.toString()); - } - - @Test - void createFromBase64_hex_type_given() { - /* prepare */ - String string = "aGVsbG8="; - - /* execute */ - BinaryString binaryString = BinaryStringFactory.createFromBase64(string, BinaryStringEncodingType.HEX); - - /* test */ - assertNotNull(binaryString); - assertEquals(BinaryStringEncodingType.HEX, binaryString.getType()); - assertTrue(binaryString instanceof HexString); - assertEquals("68656c6c6f", binaryString.toString()); - } -} diff --git a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/HexStringTest.java b/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/HexStringTest.java deleted file mode 100644 index 71e15831e4..0000000000 --- a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/HexStringTest.java +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -public class HexStringTest { - @Test - void from_string() { - /* prepare */ - String string = "Hello"; - String expectedString = "48656c6c6f"; - - /* execute + test */ - assertEquals(expectedString, new HexString(string).toString()); - } - - @Test - void from_string_null_throw_illegal_argument_exception() { - /* execute */ - Exception exception = assertThrows(IllegalArgumentException.class, () -> { - new HexString((String) null); - }); - - /* test */ - assertEquals("String cannot be null.", exception.getMessage()); - } - - @Test - void from_string_unicode() { - /* prepare */ - String string = "Hello 🦄"; - String expectedString = "48656c6c6f20f09fa684"; - - /* execute + test */ - assertEquals(expectedString, new HexString(string).toString()); - } - - @Test - void from_bytes() { - /* prepare */ - byte[] bytes = "Hello".getBytes(); - String expectedString = "48656c6c6f"; - - /* execute + test */ - assertEquals(expectedString, new HexString(bytes).toString()); - } - - @Test - void from_bytes_unicode() { - /* prepare */ - byte[] bytes = "Hello 🦄".getBytes(); - String expectedString = "48656c6c6f20f09fa684"; - - /* execute + test */ - assertEquals(expectedString, new HexString(bytes).toString()); - } - - @Test - void from_bytes_null_throw_illegal_argument_exception() { - /* execute */ - Exception exception = assertThrows(IllegalArgumentException.class, () -> { - new HexString((byte[]) null); - }); - - /* test */ - assertEquals("Byte array cannot be null.", exception.getMessage()); - } - - @Test - void toString_unicode_test() { - /* prepare */ - String string = "Hello 🦄"; - String expectedString = "48656c6c6f20f09fa684"; - - /* execute + test */ - assertEquals(expectedString, new HexString(string).toString()); - } - - @Test - void equals_test() { - /* prepare */ - String string = "I like 🍍"; - - /* execute */ - HexString hexString = new HexString(string); - HexString hexString2 = new HexString(string); - - /* test */ - assertEquals(hexString, hexString2); - assertTrue(hexString.equals(hexString2)); - assertEquals(hexString.hashCode(), hexString2.hashCode()); - } - - @Test - void test_immutablitiy() { - /* prepare */ - String string = "A 🐺 in a 🐑 skin"; - - /* execute */ - HexString hexString = new HexString(string); - - /* test */ - assertFalse(string == hexString.toString()); - assertFalse(string.getBytes() == hexString.getBytes()); - } -} diff --git a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/NoneCipherTest.java b/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/NoneCipherTest.java deleted file mode 100644 index 1369365bf5..0000000000 --- a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/NoneCipherTest.java +++ /dev/null @@ -1,116 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; - -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; - -import org.junit.jupiter.api.Test; - -public class NoneCipherTest { - private BinaryString initializationVector = BinaryStringFactory.createFromString("Hello"); - - @Test - void encrypt_null_iv() throws InvalidKeyException, InvalidAlgorithmParameterException { - /* prepare */ - String plaintext = "This is plaintext"; - PersistenceCipher cipher = NoneCipher.create(null); - - /* execute */ - BinaryString ciphertext = cipher.encrypt(plaintext, null); - - /* test */ - assertEquals(plaintext, ciphertext.toString()); - } - - @Test - void encrypt_with_iv() throws InvalidKeyException, InvalidAlgorithmParameterException { - /* prepare */ - String plaintext = "This is plaintext"; - PersistenceCipher cipher = NoneCipher.create(null); - - /* execute */ - BinaryString ciphertext = cipher.encrypt(plaintext, initializationVector); - - /* test */ - assertEquals(plaintext, ciphertext.toString()); - } - - @Test - void decrypt_null_iv() throws InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - /* prepare */ - String expectedPlaintext = "This is plaintext"; - BinaryString ciphertext = new PlainString(expectedPlaintext); - PersistenceCipher cipher = NoneCipher.create(null); - - /* execute */ - String plaintext = cipher.decrypt(ciphertext, null); - - /* test */ - assertEquals(expectedPlaintext, plaintext); - } - - @Test - void decrypt_with_iv() throws InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - /* prepare */ - String expectedPlaintext = "This is plaintext with a 🐎!"; - BinaryString ciphertext = new PlainString(expectedPlaintext); - PersistenceCipher cipher = NoneCipher.create(null); - - /* execute */ - String plaintext = cipher.decrypt(ciphertext, initializationVector); - - /* test */ - assertEquals(expectedPlaintext, plaintext); - } - - @Test - void getCipher_null() throws InvalidKeyException { - /* prepare */ - PersistenceCipher cipher = NoneCipher.create(null); - - /* execute + test */ - assertEquals(PersistenceCipherType.NONE, cipher.getCipherType()); - } - - @Test - void getCipher_string() throws InvalidKeyException { - /* prepare */ - PersistenceCipher cipher = NoneCipher.create(new Base64String("Hello")); - - /* execute + test */ - assertEquals(PersistenceCipherType.NONE, cipher.getCipherType()); - } - - @Test - void generateNewInitializationVector_default_type() throws InvalidKeyException { - /* prepare */ - PersistenceCipher cipher = NoneCipher.create(new Base64String("Hello")); - - /* execute */ - BinaryString initializationVector = cipher.generateNewInitializationVector(); - - /* test */ - assertEquals(PersistenceCipherType.NONE, cipher.getCipherType()); - assertEquals(BinaryStringEncodingType.PLAIN, initializationVector.getType()); - assertEquals("", initializationVector.toString()); - } - - @Test - void generateNewInitializationVector_type_provided() throws InvalidKeyException { - /* prepare */ - PersistenceCipher cipher = NoneCipher.create(new Base64String("Hello")); - - /* execute */ - BinaryString initializationVector = cipher.generateNewInitializationVector(BinaryStringEncodingType.HEX); - - /* test */ - assertEquals(PersistenceCipherType.NONE, cipher.getCipherType()); - assertEquals(BinaryStringEncodingType.HEX, initializationVector.getType()); - assertEquals("", initializationVector.toString()); - } -} diff --git a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/PersistenceCipherFactoryTest.java b/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/PersistenceCipherFactoryTest.java deleted file mode 100644 index 96e5978829..0000000000 --- a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/PersistenceCipherFactoryTest.java +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.security.InvalidKeyException; - -import org.junit.jupiter.api.Test; - -public class PersistenceCipherFactoryTest { - @Test - void create_none_cipher() throws InvalidKeyException { - /* prepare */ - PersistenceCipherType cipherType = PersistenceCipherType.NONE; - Base64String secret = new Base64String("topSecret"); - - /* execute */ - PersistenceCipher cipher = PersistenceCipherFactory.create(cipherType, secret); - - /* test */ - assertEquals(cipher.getCipherType(), cipherType); - } - - @Test - void create_aes_gcm_siv_256() throws InvalidKeyException { - /* prepare */ - PersistenceCipherType cipherType = PersistenceCipherType.AES_GCM_SIV_256; - BinaryString secret = new PlainString("a".repeat(32)); - - /* execute */ - PersistenceCipher cipher = PersistenceCipherFactory.create(cipherType, secret); - - /* test */ - assertEquals(cipher.getCipherType(), cipherType); - } - - @Test - void create_aes_gcm_siv_128() throws InvalidKeyException { - /* prepare */ - PersistenceCipherType cipherType = PersistenceCipherType.AES_GCM_SIV_128; - BinaryString secret = new HexString("a".repeat(16)); - - /* execute */ - PersistenceCipher cipher = PersistenceCipherFactory.create(cipherType, secret); - - /* test */ - assertEquals(cipher.getCipherType(), cipherType); - } - - @Test - void create_initializationVector_aes_gcm_siv_256() throws InvalidKeyException { - /* prepare */ - PersistenceCipherType cipherType = PersistenceCipherType.AES_GCM_SIV_256; - BinaryString secret = new PlainString("a".repeat(32)); - PersistenceCipher cipher = PersistenceCipherFactory.create(cipherType, secret); - - /* execute */ - BinaryString initializationVector = cipher.generateNewInitializationVector(); - - /* test */ - assertEquals(cipher.getCipherType(), cipherType); - assertNotNull(initializationVector); - assertEquals(AesGcmSiv.IV_LENGTH_IN_BYTES, initializationVector.getBytes().length); - } - - @Test - void create_initializationVector_aes_gcm_siv_128() throws InvalidKeyException { - /* prepare */ - PersistenceCipherType cipherType = PersistenceCipherType.AES_GCM_SIV_128; - BinaryString secret = new PlainString("a".repeat(16)); - PersistenceCipher cipher = PersistenceCipherFactory.create(cipherType, secret); - - /* execute */ - BinaryString initializationVector = cipher.generateNewInitializationVector(); - - /* test */ - assertEquals(cipher.getCipherType(), cipherType); - assertNotNull(initializationVector); - assertEquals(AesGcmSiv.IV_LENGTH_IN_BYTES, initializationVector.getBytes().length); - } - - @Test - void create_initializationVector_none_cipher() throws InvalidKeyException { - /* prepare */ - PersistenceCipherType cipherType = PersistenceCipherType.NONE; - BinaryString secret = new PlainString("a".repeat(3)); - PersistenceCipher cipher = PersistenceCipherFactory.create(cipherType, secret); - - /* execute */ - BinaryString initializationVector = cipher.generateNewInitializationVector(); - - /* test */ - assertEquals(cipher.getCipherType(), cipherType); - assertNotNull(initializationVector); - assertEquals("", initializationVector.toString()); - } -} diff --git a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/PlainStringTest.java b/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/PlainStringTest.java deleted file mode 100644 index b9b66d26f1..0000000000 --- a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/PlainStringTest.java +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -public class PlainStringTest { - @Test - void from_string() { - /* prepare */ - String string = "Hello"; - - /* execute + test */ - assertEquals(string, new PlainString(string).toString()); - } - - @Test - void from_string_null_throw_illegal_argument_exception() { - /* execute */ - Exception exception = assertThrows(IllegalArgumentException.class, () -> { - new PlainString((String) null); - }); - - /* test */ - assertEquals("String cannot be null.", exception.getMessage()); - } - - @Test - void from_string_unicode() { - /* prepare */ - String string = "Hello 🦄"; - - /* execute + test */ - assertEquals(string, new PlainString(string).toString()); - } - - @Test - void from_bytes() { - /* prepare */ - byte[] bytes = "Hello".getBytes(); - - /* execute + test */ - assertEquals(new String(bytes), new PlainString(bytes).toString()); - } - - @Test - void from_bytes_unicode() { - /* prepare */ - byte[] bytes = "Hello 🦄".getBytes(); - - /* execute + test */ - assertEquals(new String(bytes), new PlainString(bytes).toString()); - } - - @Test - void from_bytes_null_throw_illegal_argument_exception() { - Exception exception = assertThrows(IllegalArgumentException.class, () -> { - new PlainString((byte[]) null); - }); - - /* execute + test */ - assertEquals("Byte array cannot be null.", exception.getMessage()); - } - - @Test - void toString_unicode_test() { - /* prepare */ - String string = "Hello 🦄"; - - /* execute + test */ - assertEquals(string, new PlainString(string).toString()); - } - - @Test - void equals_test() { - /* prepare */ - String string = "I like 🍍"; - - /* execute */ - PlainString plainString1 = new PlainString(string); - PlainString plainString2 = new PlainString(string); - - /* test */ - assertEquals(plainString1, plainString2); - assertTrue(plainString1.equals(plainString2)); - assertEquals(plainString1.hashCode(), plainString2.hashCode()); - } - - @Test - void test_immutablitiy() { - /* prepare */ - String string = "A 🐺 in a 🐑 skin"; - - /* execute */ - PlainString plainString = new PlainString(string); - - /* test */ - assertFalse(string == plainString.toString()); - assertFalse(string.getBytes() == plainString.getBytes()); - } -} diff --git a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/RotationTest.java b/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/RotationTest.java deleted file mode 100644 index 3b656487d8..0000000000 --- a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/security/persistence/RotationTest.java +++ /dev/null @@ -1,300 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.commons.core.security.persistence; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; - -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; - -import org.junit.jupiter.api.Test; - -public class RotationTest { - @Test - void secret_rotation_cipher_none() - throws InvalidKeyException, IllegalArgumentException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - /* prepare */ - BinaryString currentSecret = new PlainString("abc"); - BinaryString newSecret = new PlainString("bca"); - BinaryString cipherText = new PlainString("hello"); - BinaryString initializationVector = new PlainString("iv"); - - PersistenceCipherType cipherType = PersistenceCipherType.NONE; - RotationStrategy rotation = RotationStrategy.createSecretRotationStrategy(currentSecret, newSecret, cipherType); - - /* execute */ - BinaryString newCipherText = rotation.rotate(cipherText, initializationVector); - - /* test */ - assertNotNull(rotation); - assertEquals(cipherText, newCipherText); - assertTrue(rotation.isSecretRotationStrategy()); - assertFalse(rotation.isCipherRotationStrategy()); - assertEquals(PersistenceCipherType.NONE, rotation.getCurrentCipher()); - assertEquals(PersistenceCipherType.NONE, rotation.getNewCipher()); - } - - @Test - void secret_rotation_cipher_aes_gcm_siv_128() - throws InvalidKeyException, IllegalArgumentException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - /* prepare */ - String expectedPlainText = "Hello, I am ☺️ 4 you."; - BinaryString expectedCipherText = BinaryStringFactory.createFromBase64("8gWa4YPRlshBZCml8a0xvAJ1Y1mNU9iovclpvOhwVj4XiiaZvWKHWkU=", - BinaryStringEncodingType.BASE64); - BinaryString currentSecret = new PlainString("a".repeat(16)); - BinaryString newSecret = new PlainString("z".repeat(16)); - BinaryString cipherText = BinaryStringFactory.createFromBase64("DuwfqoAJrzZiK3u5v0XEnARPOjLugpobvWCxfTV6Y1FkAOECII/J8RU=", - BinaryStringEncodingType.BASE64); - BinaryString initializationVector = new PlainString("i".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES)); - - PersistenceCipherType cipherType = PersistenceCipherType.AES_GCM_SIV_128; - RotationStrategy rotation = RotationStrategy.createSecretRotationStrategy(currentSecret, newSecret, cipherType); - - /* execute */ - BinaryString newCipherText = rotation.rotate(cipherText, initializationVector); - - /* test */ - assertNotNull(rotation); - assertEquals(expectedCipherText, newCipherText); - assertTrue(rotation.isSecretRotationStrategy()); - assertFalse(rotation.isCipherRotationStrategy()); - assertEquals(PersistenceCipherType.AES_GCM_SIV_128, rotation.getCurrentCipher()); - assertEquals(PersistenceCipherType.AES_GCM_SIV_128, rotation.getNewCipher()); - - PersistenceCipher cipher = PersistenceCipherFactory.create(cipherType, newSecret); - String plainText = cipher.decrypt(newCipherText, initializationVector); - - assertEquals(expectedPlainText, plainText); - } - - @Test - void secret_rotation_cipher_aes_gcm_siv_256() - throws InvalidKeyException, IllegalArgumentException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - /* prepare */ - String expectedPlainText = "Hello, I am ☺️ 4 you."; - BinaryString expectedCipherText = BinaryStringFactory.createFromBase64("y5/I7wBmKwqKKazHrRnj2j6v9uuySiHJ9MK3pUvWjkUIC/3jDSFJLPI=", - BinaryStringEncodingType.BASE64); - BinaryString currentSecret = new PlainString("a".repeat(32)); - BinaryString newSecret = new PlainString("z".repeat(32)); - BinaryString cipherText = BinaryStringFactory.createFromBase64("KSIAj+JAD95o77GF91GQShbuh0dyuIMdDjhX1VQFi7DW2KSutD0uOt8=", - BinaryStringEncodingType.BASE64); - BinaryString initializationVector = new HexString("i".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES)); - - PersistenceCipherType cipherType = PersistenceCipherType.AES_GCM_SIV_256; - RotationStrategy rotation = RotationStrategy.createSecretRotationStrategy(currentSecret, newSecret, cipherType); - - /* execute */ - BinaryString newCipherText = rotation.rotate(cipherText, initializationVector); - - /* test */ - assertNotNull(rotation); - assertEquals(expectedCipherText, newCipherText); - assertTrue(rotation.isSecretRotationStrategy()); - assertFalse(rotation.isCipherRotationStrategy()); - assertEquals(PersistenceCipherType.AES_GCM_SIV_256, rotation.getCurrentCipher()); - assertEquals(PersistenceCipherType.AES_GCM_SIV_256, rotation.getNewCipher()); - - PersistenceCipher cipher = PersistenceCipherFactory.create(cipherType, newSecret); - String plainText = cipher.decrypt(newCipherText, initializationVector); - - assertEquals(expectedPlainText, plainText); - } - - @Test - void cipher_and_secret_rotation_cipher_none_to_cipher_aes_gcm_siv_128() - throws InvalidKeyException, IllegalArgumentException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - /* prepare */ - String expectedPlainText = "Hello, I am ☺️ 4 you."; - BinaryString expectedCipherText = BinaryStringFactory.createFromBase64("DuwfqoAJrzZiK3u5v0XEnARPOjLugpobvWCxfTV6Y1FkAOECII/J8RU=", - BinaryStringEncodingType.BASE64); - BinaryString currentSecret = new PlainString("abc"); - BinaryString newSecret = new PlainString("a".repeat(16)); - BinaryString cipherText = new PlainString(expectedPlainText); - BinaryString initializationVector = new PlainString("i".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES)); - - PersistenceCipherType currentCipherType = PersistenceCipherType.NONE; - PersistenceCipherType newCipherType = PersistenceCipherType.AES_GCM_SIV_128; - RotationStrategy rotation = RotationStrategy.createCipherAndSecretRotationStrategy(currentSecret, newSecret, currentCipherType, newCipherType); - - /* execute */ - BinaryString newCipherText = rotation.rotate(cipherText, initializationVector); - - /* test */ - assertNotNull(rotation); - assertEquals(expectedCipherText, newCipherText); - assertTrue(rotation.isSecretRotationStrategy()); - assertTrue(rotation.isCipherRotationStrategy()); - assertEquals(PersistenceCipherType.NONE, rotation.getCurrentCipher()); - assertEquals(PersistenceCipherType.AES_GCM_SIV_128, rotation.getNewCipher()); - - PersistenceCipher cipher = PersistenceCipherFactory.create(newCipherType, newSecret); - String plainText = cipher.decrypt(newCipherText, initializationVector); - - assertEquals(expectedPlainText, plainText); - } - - @Test - void cipher_and_secret_rotation_cipher_aes_gcm_siv_128_to_cipher_aes_gcm_siv_256() - throws InvalidKeyException, IllegalArgumentException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - /* prepare */ - String expectedPlainText = "Hello 👋, welcome to 🌐."; - - BinaryString expectedCipherText = BinaryStringFactory - .createFromHex("d09be77ddd8dbac86b69b0f5f554faef740555ac93f12aedfdf62700e4ea3016e03dacc105f32f114791d8e6", BinaryStringEncodingType.BASE64); - BinaryString currentSecret = new Base64String("🍐🍌🍓🍉"); - BinaryString newSecret = new HexString("🥦🥕🥔🫘🥒🫑🌽🍆"); - BinaryString cipherText = BinaryStringFactory.createFromBase64("Qu7ICJBGMw9dAPPBWx86e5bjOq3YKC+x25n/YkluWZAGdSna08tKaE78pMk=", - BinaryStringEncodingType.BASE64); - BinaryString initializationVector = new PlainString("🧅".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES / 4)); - - PersistenceCipherType currentCipherType = PersistenceCipherType.AES_GCM_SIV_128; - PersistenceCipherType newCipherType = PersistenceCipherType.AES_GCM_SIV_256; - RotationStrategy rotation = RotationStrategy.createCipherAndSecretRotationStrategy(currentSecret, newSecret, currentCipherType, newCipherType); - - /* execute */ - BinaryString newCipherText = rotation.rotate(cipherText, initializationVector); - - /* test */ - assertNotNull(rotation); - assertEquals(expectedCipherText, newCipherText); - assertEquals(PersistenceCipherType.AES_GCM_SIV_128, rotation.getCurrentCipher()); - assertEquals(PersistenceCipherType.AES_GCM_SIV_256, rotation.getNewCipher()); - assertTrue(rotation.isSecretRotationStrategy()); - assertTrue(rotation.isCipherRotationStrategy()); - - PersistenceCipher cipher = PersistenceCipherFactory.create(newCipherType, newSecret); - String plainText = cipher.decrypt(newCipherText, initializationVector); - - assertEquals(expectedPlainText, plainText); - } - - @Test - void cipher_and_secret_rotation_aes_gcm_siv_256_to_none() - throws InvalidKeyException, IllegalArgumentException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - /* prepare */ - String expectedPlainText = "Hello 👋, welcome to 🌐."; - - BinaryString expectedCipherText = BinaryStringFactory.createFromString(expectedPlainText, BinaryStringEncodingType.PLAIN); - BinaryString currentSecret = new HexString("🥦🥕🥔🫘🥒🫑🌽🍆"); - BinaryString newSecret = new HexString("abc"); - BinaryString cipherText = BinaryStringFactory.createFromHex("d09be77ddd8dbac86b69b0f5f554faef740555ac93f12aedfdf62700e4ea3016e03dacc105f32f114791d8e6", - BinaryStringEncodingType.HEX); - BinaryString initializationVector = new Base64String("🧅".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES / 4)); - - PersistenceCipherType currentCipherType = PersistenceCipherType.AES_GCM_SIV_256; - PersistenceCipherType newCipherType = PersistenceCipherType.NONE; - RotationStrategy rotation = RotationStrategy.createCipherAndSecretRotationStrategy(currentSecret, newSecret, currentCipherType, newCipherType); - - /* execute */ - BinaryString newCipherText = rotation.rotate(cipherText, initializationVector); - - /* test */ - assertNotNull(rotation); - assertEquals(expectedCipherText, newCipherText); - assertEquals(PersistenceCipherType.AES_GCM_SIV_256, rotation.getCurrentCipher()); - assertEquals(PersistenceCipherType.NONE, rotation.getNewCipher()); - assertTrue(rotation.isSecretRotationStrategy()); - assertTrue(rotation.isCipherRotationStrategy()); - - PersistenceCipher cipher = PersistenceCipherFactory.create(newCipherType, newSecret); - String plainText = cipher.decrypt(newCipherText, initializationVector); - - assertEquals(expectedPlainText, plainText); - } - - @Test - void initializationVector_rotation_cipher_none() - throws InvalidKeyException, IllegalArgumentException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - /* prepare */ - BinaryString secret = new PlainString("abc"); - BinaryString cipherText = new PlainString("hello"); - BinaryString initializationVector = new PlainString("vi"); - BinaryString newInitializationVector = new PlainString("iv"); - - PersistenceCipherType cipherType = PersistenceCipherType.NONE; - RotationStrategy rotation = RotationStrategy.createInitializationVectorOnlyRotationStrategy(secret, cipherType); - - /* execute */ - BinaryString newCipherText = rotation.rotate(cipherText, initializationVector, newInitializationVector); - - /* test */ - assertNotNull(rotation); - assertEquals(cipherText, newCipherText); - assertFalse(rotation.isSecretRotationStrategy()); - assertFalse(rotation.isCipherRotationStrategy()); - assertEquals(PersistenceCipherType.NONE, rotation.getCurrentCipher()); - assertEquals(PersistenceCipherType.NONE, rotation.getNewCipher()); - } - - @Test - void initializationVector_rotation_aes_gcm_siv_128() - throws InvalidKeyException, IllegalArgumentException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - /* prepare */ - String expectedPlainText = "The quick brown fox jumps over the lazy dog."; - - BinaryString expectedCipherText = BinaryStringFactory - .createFromBase64("roRrChR9D/VIaebHZgxGG3tYuCR+yXjPleQr4bDZ7B5qsVzC7EnXTi3rT3qDv09t62W/W4V9CIfQRlWs"); - BinaryString secret = new PlainString("d".repeat(16)); - BinaryString cipherText = BinaryStringFactory.createFromBase64("h3Pz6Kue6V4CUYtkZdtWml0vmXkEXjSpXyud98Z3NJjNGk+qFYJmpLCoyO+qBxWKQfxbmnmup9+vjYJQ"); - BinaryString initializationVector = new HexString("vi".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES / 2)); - BinaryString newInitializationVector = new Base64String("iv".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES / 2)); - - PersistenceCipherType cipherType = PersistenceCipherType.AES_GCM_SIV_128; - RotationStrategy rotation = RotationStrategy.createInitializationVectorOnlyRotationStrategy(secret, cipherType); - - /* execute */ - BinaryString newCipherText = rotation.rotate(cipherText, initializationVector, newInitializationVector); - - /* test */ - assertNotNull(rotation); - assertEquals(expectedCipherText, newCipherText); - assertFalse(rotation.isSecretRotationStrategy()); - assertFalse(rotation.isCipherRotationStrategy()); - assertEquals(PersistenceCipherType.AES_GCM_SIV_128, rotation.getCurrentCipher()); - assertEquals(PersistenceCipherType.AES_GCM_SIV_128, rotation.getNewCipher()); - - PersistenceCipher cipher = PersistenceCipherFactory.create(cipherType, secret); - String plainText = cipher.decrypt(newCipherText, newInitializationVector); - - assertEquals(expectedPlainText, plainText); - } - - @Test - void initializationVector_rotation_aes_gcm_siv_256() - throws InvalidKeyException, IllegalArgumentException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - /* prepare */ - String expectedPlainText = "Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich"; - - BinaryString expectedCipherText = BinaryStringFactory - .createFromBase64("0eG35+7x+nr4MbsgRsTqOEjRReF6JzM2thWe+3zHleta1cCXzyCxQJ5Jaxeq7TnYDDg3nR/RcQdc00UlqEJw3WODaffeETDJ0bk5LfP19FWr/g=="); - BinaryString secret = new PlainString("ThisIsAStringWith32Characters!?!"); - BinaryString cipherText = BinaryStringFactory - .createFromBase64("AfVfrXdv8JImZ9qrpO7vOJRwveOaYRJNsQaeJiuDg5nX3ZT13EwH7CXlZa8IBNvhrqUvEn3w0L0J+JjYVYPxh80CqCpgjbMl4a8Ql5eyqKQIZQ=="); - BinaryString initializationVector = new HexString("vi".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES / 2)); - BinaryString newInitializationVector = new Base64String("iv".repeat(AesGcmSiv.IV_LENGTH_IN_BYTES / 2)); - - PersistenceCipherType cipherType = PersistenceCipherType.AES_GCM_SIV_256; - RotationStrategy rotation = RotationStrategy.createInitializationVectorOnlyRotationStrategy(secret, cipherType); - - /* execute */ - BinaryString newCipherText = rotation.rotate(cipherText, initializationVector, newInitializationVector); - - /* test */ - assertNotNull(rotation); - assertEquals(expectedCipherText, newCipherText); - assertFalse(rotation.isSecretRotationStrategy()); - assertFalse(rotation.isCipherRotationStrategy()); - assertEquals(PersistenceCipherType.AES_GCM_SIV_256, rotation.getCurrentCipher()); - assertEquals(PersistenceCipherType.AES_GCM_SIV_256, rotation.getNewCipher()); - - PersistenceCipher cipher = PersistenceCipherFactory.create(cipherType, secret); - String plainText = cipher.decrypt(newCipherText, newInitializationVector); - - assertEquals(expectedPlainText, plainText); - } -} diff --git a/sechub-commons-encryption/README.adoc b/sechub-commons-encryption/README.adoc new file mode 100644 index 0000000000..8846a51f5b --- /dev/null +++ b/sechub-commons-encryption/README.adoc @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +== About +`sechub-commons-encryption` is a library without any dependency to Spring or SecHub. + +It has only a dependency to bouncy castle library to provide security. + diff --git a/sechub-commons-encryption/build.gradle b/sechub-commons-encryption/build.gradle new file mode 100644 index 0000000000..ef6805e98c --- /dev/null +++ b/sechub-commons-encryption/build.gradle @@ -0,0 +1,11 @@ + // SPDX-License-Identifier: MIT +dependencies { + + implementation library.bouncy_castle_bcprov_jdk8 + + testImplementation spring_boot_dependency.junit_jupiter + testImplementation spring_boot_dependency.mockito_core + testImplementation spring_boot_dependency.assertj_core + +} + \ No newline at end of file diff --git a/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/AesGcmSivCipher.java b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/AesGcmSivCipher.java new file mode 100644 index 0000000000..cd7e309cda --- /dev/null +++ b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/AesGcmSivCipher.java @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.Security; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +/** + * Providing access to AES-GCM-SIV + * + * AES-GCM-SIV is a nonce misuse-resistant authenticated encryption algorithm. + * + * For more information refer to + * RFC 8452 + * + * @author Jeremias Eppler + * @author Albert Tregnaghi + */ +class AesGcmSivCipher implements PersistentCipher { + + private static BouncyCastleProvider cryptoProvider; + + public static final int AUTHENTICATION_TAG_LENGTH_IN_BITS = 16 * 8; // 16 bytes (128 bits) + + /** + * The recommended initialization vector (iv) for AES-GCM-SIV is 12 bytes or 96 + * bits. + * + * For an explanation have a look at: - + * https://datatracker.ietf.org/doc/html/rfc8452#section-4 - + * https://crypto.stackexchange.com/questions/41601/aes-gcm-recommended-iv-size-why-12-bytes + */ + static final int IV_LENGTH_IN_BYTES = 12; + + private static final String ALGORITHM = "AES/GCM-SIV/NoPadding"; + + static { + cryptoProvider = new BouncyCastleProvider(); + + Security.addProvider(cryptoProvider); + } + + private SecretKey secretKey; + + private PersistentCipherType type; + + AesGcmSivCipher(SecretKey secretKey, PersistentCipherType type) { + this.secretKey = secretKey; + this.type = type; + } + + @Override + public byte[] encrypt(byte[] origin, InitializationVector initVector) { + + try { + Cipher cipher = Cipher.getInstance(ALGORITHM, cryptoProvider); + + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(AUTHENTICATION_TAG_LENGTH_IN_BITS, initVector.getInitializationBytes()); + + cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec); + + byte[] encrypted = cipher.doFinal(origin); + + return encrypted; + + } catch (NoSuchAlgorithmException | NoSuchPaddingException providerException) { + throw new IllegalStateException("Encryption not possible, please check the provider", providerException); + } catch (BadPaddingException | IllegalBlockSizeException paddingBlockException) { + throw new IllegalStateException("Should not occur. AES in GCM-SIV mode does not require padding.", paddingBlockException); + } catch (InvalidKeyException e) { + throw new IllegalStateException("Key not valid", e); + } catch (InvalidAlgorithmParameterException e) { + throw new IllegalStateException("Invalid algorithm parameters", e); + } + + } + + @Override + public byte[] decrypt(byte[] encryptedData, InitializationVector initVector) { + Cipher cipher; + try { + cipher = Cipher.getInstance(ALGORITHM, cryptoProvider); + + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(AUTHENTICATION_TAG_LENGTH_IN_BITS, initVector.getInitializationBytes()); + + cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec); + + byte[] plaintextBytes = cipher.doFinal(encryptedData); + return plaintextBytes; + } catch (NoSuchAlgorithmException | NoSuchPaddingException providerException) { + throw new IllegalStateException("Decryption not possible, please check the provider", providerException); + } catch (InvalidKeyException e) { + throw new IllegalStateException("Key not valid", e); + } catch (InvalidAlgorithmParameterException e) { + throw new IllegalStateException("Invalid algorithm parameters", e); + } catch (IllegalBlockSizeException e) { + throw new IllegalStateException(e); + } catch (BadPaddingException e) { + throw new IllegalStateException(e); + } + } + + @Override + public InitializationVector createNewInitializationVector() { + + byte[] initializationVector = new byte[IV_LENGTH_IN_BYTES]; + + SecureRandom random = new SecureRandom(); + random.nextBytes(initializationVector); + + return new InitializationVector(initializationVector); + } + + @Override + public PersistentCipherType getType() { + return type; + } + + @Override + public String toString() { + return type == null ? "Unknown " + getClass().getSimpleName() + " type!" : type.name(); + } + +} diff --git a/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/ByteArrayTransformer.java b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/ByteArrayTransformer.java new file mode 100644 index 0000000000..73e19ef125 --- /dev/null +++ b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/ByteArrayTransformer.java @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +/** + * Byte array transformer interface. Implementations are able to transform + * encrypted data (which we always handle as bytes) into an object type and to + * transform the object type to a byte array. + * + * @author Albert Tregnaghi + * + * @param + */ +public interface ByteArrayTransformer { + + /** + * Transform given bytes to target object. If bytes array is null, the object + * will be null + * + * @param bytes + * @return created object + */ + public T transformFromBytes(byte[] bytes); + + /** + * Transforms given object to byte array. If object is null, + * null will be returned + * + * @param object the object to transform (can be null) + * @return byte array, never null + */ + public byte[] transformToBytes(T object); + +} diff --git a/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/DefaultSecretKeyProvider.java b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/DefaultSecretKeyProvider.java new file mode 100644 index 0000000000..09f8125647 --- /dev/null +++ b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/DefaultSecretKeyProvider.java @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +/** + * Default implementation of a secret key provider. + * + * @author Albert Tregnaghi + * + */ +public class DefaultSecretKeyProvider implements SecretKeyProvider { + + private int lengthInBits; + private SecretKey secretKey; + + public DefaultSecretKeyProvider(byte[] rawSecret, PersistentCipherType cipherType) { + if (rawSecret == null) { + throw new IllegalArgumentException("secret may not be null"); + } + if (rawSecret.length == 0) { + throw new IllegalArgumentException("secret bytes array may not be empty"); + } + if (cipherType == null) { + throw new IllegalArgumentException("cipher type not defined"); + } + String secretKeyAlgorithm = cipherType.getSecretKeyAlgorithm(); + if (secretKeyAlgorithm == null || secretKeyAlgorithm.isBlank()) { + throw new IllegalArgumentException("cipher type: " + cipherType + " does not provide an algorithm for secret keys!"); + } + + lengthInBits = rawSecret.length * 8; + + secretKey = new SecretKeySpec(rawSecret, 0, rawSecret.length, secretKeyAlgorithm); + } + + @Override + public SecretKey getSecretKey() { + return secretKey; + } + + @Override + public int getLengthOfSecretInBits() { + return lengthInBits; + } + +} diff --git a/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/EncryptionConstants.java b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/EncryptionConstants.java new file mode 100644 index 0000000000..8f22ba7655 --- /dev/null +++ b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/EncryptionConstants.java @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +import java.nio.charset.Charset; + +public class EncryptionConstants { + + public static final Charset UTF8_CHARSET_ENCODING = Charset.forName("UTF-8"); +} diff --git a/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/EncryptionResult.java b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/EncryptionResult.java new file mode 100644 index 0000000000..00cb98f84b --- /dev/null +++ b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/EncryptionResult.java @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +/** + * Represents the result of an encryption. Contains encrypted data but also + * initial vector. + * + * @author Albert Tregnaghi + * + */ +public class EncryptionResult { + + private byte[] encryptedData; + + private InitializationVector initialVector; + + public EncryptionResult(byte[] encryptedData, InitializationVector initialVector) { + if (initialVector == null) { + throw new IllegalArgumentException("initial vector may not be null!"); + } + this.encryptedData = encryptedData; + this.initialVector = initialVector; + } + + /** + * @return encrypted data or null + */ + public byte[] getEncryptedData() { + return encryptedData; + } + + /** + * @return initial vector, never null + */ + public InitializationVector getInitialVector() { + return initialVector; + } + +} \ No newline at end of file diff --git a/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/EncryptionRotationSetup.java b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/EncryptionRotationSetup.java new file mode 100644 index 0000000000..92e385bda2 --- /dev/null +++ b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/EncryptionRotationSetup.java @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +public class EncryptionRotationSetup { + + public static RotationSetupBuilder builder() { + return new RotationSetupBuilder(); + } + + /* private, because MUST be created by builder - which ensures setup is valid */ + private EncryptionRotationSetup() { + + } + + private PersistentCipher oldCipher; + private InitializationVector oldInitialVector; + private PersistentCipher newCipher; + private InitializationVector newInitialVector; + + public PersistentCipher getOldCipher() { + return oldCipher; + } + + public InitializationVector getOldInitialVector() { + return oldInitialVector; + } + + public PersistentCipher getNewCipher() { + return newCipher; + } + + public InitializationVector getNewInitialVector() { + return newInitialVector; + } + + public static class RotationSetupBuilder { + + private PersistentCipher oldCipher; + private InitializationVector oldInitialVector; + private PersistentCipher newCipher; + private InitializationVector newInitialVector; + + private RotationSetupBuilder() { + } + + public RotationSetupBuilder oldInitialVector(InitializationVector oldInitialVector) { + this.oldInitialVector = oldInitialVector; + return this; + } + + public RotationSetupBuilder newInitialVector(InitializationVector newInitialVector) { + this.newInitialVector = newInitialVector; + return this; + } + + public RotationSetupBuilder oldCipher(PersistentCipher oldCipher) { + this.oldCipher = oldCipher; + return this; + } + + public RotationSetupBuilder newCipher(PersistentCipher newCipher) { + this.newCipher = newCipher; + return this; + } + + /** + * Builds a new rotation data object. If the new initial vector is not set, the + * old initial vector will be used If the new cipher is not set, the old cipher + * will be used. But if none of them ( no new initial vector and no new cipher) + * is defined an {@link IllegalArgumentException} will be thrown (because setup + * provides no rotation at all) + * + * @return new rotation data object + * @throws IllegalArgumentException when no new cipher and no new initial vector + * defined + * @throws IllegalArgumentException when no old cipher defined + * @throws IllegalArgumentException when no old initial vector defined + */ + public EncryptionRotationSetup build() { + if (oldCipher == null) { + throw new IllegalArgumentException("old cipher must be defined in builder before build is called!"); + } + if (oldInitialVector == null) { + throw new IllegalArgumentException("old initial vector must be defined in builder before build is called!"); + } + + if (newInitialVector == null && newCipher == null) { + throw new IllegalArgumentException("no new cipher or a new initial vector given - rotation is impossible in this case!"); + } + + EncryptionRotationSetup data = new EncryptionRotationSetup(); + data.oldCipher = this.oldCipher; + data.oldInitialVector = this.oldInitialVector; + + if (newCipher == null) { + data.newCipher = this.oldCipher; + } else { + data.newCipher = this.newCipher; + } + + if (newInitialVector == null) { + data.newInitialVector = this.oldInitialVector; + } else { + data.newInitialVector = this.newInitialVector; + } + + return data; + } + } +} diff --git a/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/EncryptionRotator.java b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/EncryptionRotator.java new file mode 100644 index 0000000000..0d38baaca5 --- /dev/null +++ b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/EncryptionRotator.java @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +public class EncryptionRotator { + + /** + * Rotates encryption for existing encrypted data. + * + * @param encryptedData data which is encrypted and shall be rotated + * @param setup rotation setup to use, may not be null + * @return new encrypted data + */ + public byte[] rotate(byte[] encryptedData, EncryptionRotationSetup setup) { + if (setup == null) { + throw new IllegalArgumentException("NO rotation setup defined"); + } + // no further checks necessary - is don by rotation setup builder which is the + // only way to create such an object + + byte[] unencryptedData = setup.getOldCipher().decrypt(encryptedData, setup.getOldInitialVector()); + byte[] newEncryptedData = setup.getNewCipher().encrypt(unencryptedData, setup.getNewInitialVector()); + + return newEncryptedData; + } + +} diff --git a/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/EncryptionSupport.java b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/EncryptionSupport.java new file mode 100644 index 0000000000..668b58839b --- /dev/null +++ b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/EncryptionSupport.java @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +/** + * A class to simplify encryption (and also decryption) + * + * @author Albert Tregnaghi + * + */ +public class EncryptionSupport { + + private StringByteArrayTransformer stringTransformer = new StringByteArrayTransformer(); + + /** + * Encrypt given string + * + * @param string text to encrypt + * @param cipher cipher to use for encryption + * @return {@link EncryptionResult}, never null + */ + public EncryptionResult encryptString(String string, PersistentCipher cipher) { + return encrypt(string, cipher, stringTransformer); + } + + /** + * Encrypt given object. Will create new initial vector automatically. + * + * @param target object type + * @param object object to encrypt + * @param cipher cipher to use for encryption + * @param transformer transformer used to transform object into a byte array + * @return {@link EncryptionResult}, never null + */ + public EncryptionResult encrypt(T object, PersistentCipher cipher, ByteArrayTransformer transformer) { + if (transformer == null) { + throw new IllegalArgumentException("transformer may not be null!"); + } + byte[] transformToBytes = transformer.transformToBytes(object); + + InitializationVector initialVector = cipher.createNewInitializationVector(); + + byte[] encrypted = cipher.encrypt(transformToBytes, initialVector); + + EncryptionResult result = new EncryptionResult(encrypted, initialVector); + return result; + } + + /** + * Decrypts given encrypted byte array and automatically transform to a string + * by using a {@link StringByteArrayTransformer}. + * + * @param encryptedData + * @param cipher + * @param initialVector + * @return string or null + */ + public String decryptString(byte[] encryptedData, PersistentCipher cipher, InitializationVector initialVector) { + return decrypt(encryptedData, cipher, initialVector, stringTransformer); + } + + /** + * Decrypts given encrypted byte array via given byte array transformer instance + * + * @param Target object type + * @param encryptedData byte array containing encrypted dta + * @param cipher cipher to use + * @param initialVector initial vector to use + * @param transformer transformer which will be used to transform byte array + * result from cipher instance to target object + * @return target object or null + */ + public T decrypt(byte[] encryptedData, PersistentCipher cipher, InitializationVector initialVector, ByteArrayTransformer transformer) { + if (transformer == null) { + throw new IllegalArgumentException("transformer may not be null!"); + } + byte[] unencrypted = cipher.decrypt(encryptedData, initialVector); + + return transformer.transformFromBytes(unencrypted); + } + +} diff --git a/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/InitializationVector.java b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/InitializationVector.java new file mode 100644 index 0000000000..0591f4b409 --- /dev/null +++ b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/InitializationVector.java @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +import java.util.Arrays; + +public class InitializationVector { + + private byte[] initializationBytes; + + public InitializationVector(byte[] bytes) { + this.initializationBytes = bytes; + } + + public byte[] getInitializationBytes() { + return initializationBytes; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "#" + hashCode(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(initializationBytes); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + InitializationVector other = (InitializationVector) obj; + return Arrays.equals(initializationBytes, other.initializationBytes); + } +} \ No newline at end of file diff --git a/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/NoneCipher.java b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/NoneCipher.java new file mode 100644 index 0000000000..9de25eb6dc --- /dev/null +++ b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/NoneCipher.java @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +class NoneCipher implements PersistentCipher { + + private static final InitializationVector INITIAL_VECTOR = new InitializationVector("none".getBytes(EncryptionConstants.UTF8_CHARSET_ENCODING)); + + @Override + public byte[] encrypt(byte[] plainData, InitializationVector initVector) { + return plainData; + } + + @Override + public byte[] decrypt(byte[] encryptedData, InitializationVector initVector) { + return encryptedData; + } + + @Override + public InitializationVector createNewInitializationVector() { + /* + * We always use the "same" initial vector which is always "none". Means it is + * also clear in database that this is not a real initial vector. The NoneCipher + * implementation does need an initial vector at all... but having a database + * column and having this method inside the interface contract, it is necessary + * to provide a value. + */ + return INITIAL_VECTOR; + } + + @Override + public PersistentCipherType getType() { + return PersistentCipherType.NONE; + } + + @Override + public String toString() { + return getType().name(); + } +} diff --git a/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/PersistentCipher.java b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/PersistentCipher.java new file mode 100644 index 0000000000..bc24374309 --- /dev/null +++ b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/PersistentCipher.java @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +public interface PersistentCipher { + + /** + * Encrypts plain data with given initial vector + * + * @param data the origin (not encrypted) data + * @param initialVector an initial vector + * @return encrypted data as byte array + */ + public byte[] encrypt(byte[] data, InitializationVector initialVector); + + /** + * Decrypts given encrypted data with initial vector + * + * @param encryptedData the encrypted data + * @param initialVector an initial vector to use for decryption + * @return decrypted data as byte array + */ + public byte[] decrypt(byte[] encryptedData, InitializationVector initialVector); + + /** + * Creates a new initialization vector which can be used for encryption. Remark: + * The bytes of the initialization vector must be stored together with the + * encrypted data. Otherwise it is not possible to retain the origin data even + * when the secret key is known! + * + * @return initialization vector which provides initialization bytes, never + * null + */ + public InitializationVector createNewInitializationVector(); + + /** + * @return the type of the cipher + */ + public PersistentCipherType getType(); +} diff --git a/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/PersistentCipherFactory.java b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/PersistentCipherFactory.java new file mode 100644 index 0000000000..89d8fb68b2 --- /dev/null +++ b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/PersistentCipherFactory.java @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +public class PersistentCipherFactory { + + public PersistentCipher createCipher(SecretKeyProvider secretProvider, PersistentCipherType type) { + + if (secretProvider == null && !PersistentCipherType.NONE.equals(type)) { + throw new IllegalArgumentException("secret key provider must be defined for all cipher types (except NONE)"); + } + + if (type == null) { + throw new IllegalArgumentException("cipher type must be defined!"); + } + + switch (type) { + case AES_GCM_SIV_128: + assertKeyHasBits(secretProvider, type, 128); + return new AesGcmSivCipher(secretProvider.getSecretKey(), type); + case AES_GCM_SIV_256: + assertKeyHasBits(secretProvider, type, 256); + return new AesGcmSivCipher(secretProvider.getSecretKey(), type); + case NONE: + return new NoneCipher(); + default: + throw new IllegalStateException("There is no implementation for %s".formatted(type)); + } + } + + private static void assertKeyHasBits(SecretKeyProvider secretProvider, PersistentCipherType type, int bitsWanted) { + int amountOfBits = secretProvider.getLengthOfSecretInBits(); + if (amountOfBits != bitsWanted) { + throw new IllegalArgumentException("The type %s does only accept %s bits for secret key, but returned secret key by provider had %s bit!" + .formatted(type, bitsWanted, amountOfBits)); + } + } + +} diff --git a/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/PersistentCipherType.java b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/PersistentCipherType.java new file mode 100644 index 0000000000..c9ddd68f82 --- /dev/null +++ b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/PersistentCipherType.java @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +public enum PersistentCipherType { + + NONE(null), + + AES_GCM_SIV_128("AES"), + + AES_GCM_SIV_256("AES"), + + ; + + private String secretKeyAlgorithm; + + /** + * Creates a new cipher type + * + * @param secretKeyAlgorithm the algorithm which shall be used for secret key + * creation + */ + private PersistentCipherType(String secretKeyAlgorithm) { + this.secretKeyAlgorithm = secretKeyAlgorithm; + } + + String getSecretKeyAlgorithm() { + return secretKeyAlgorithm; + } + +} diff --git a/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/SecretKeyProvider.java b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/SecretKeyProvider.java new file mode 100644 index 0000000000..395b6f8ff1 --- /dev/null +++ b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/SecretKeyProvider.java @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +import javax.crypto.SecretKey; + +public interface SecretKeyProvider { + + public int getLengthOfSecretInBits(); + + public SecretKey getSecretKey(); +} diff --git a/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/StringByteArrayTransformer.java b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/StringByteArrayTransformer.java new file mode 100644 index 0000000000..43d85fc347 --- /dev/null +++ b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/StringByteArrayTransformer.java @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +/** + * A specialized byte transformer which transform String to byte array a vice + * versa with same character encoding (UTF-8). + * + * @author Albert Tregnaghi + * + */ +public class StringByteArrayTransformer implements ByteArrayTransformer { + + @Override + public String transformFromBytes(byte[] bytes) { + if (bytes == null) { + return null; + } + return new String(bytes, EncryptionConstants.UTF8_CHARSET_ENCODING); + } + + @Override + public byte[] transformToBytes(String string) { + if (string == null) { + return null; + } + return string.getBytes(EncryptionConstants.UTF8_CHARSET_ENCODING); + } + +} diff --git a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/package-info.java b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/package-info.java similarity index 93% rename from sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/package-info.java rename to sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/package-info.java index a666fada63..b6913f3696 100644 --- a/sechub-commons-core/src/main/java/com/mercedesbenz/sechub/commons/core/security/persistence/package-info.java +++ b/sechub-commons-encryption/src/main/java/com/mercedesbenz/sechub/commons/encryption/package-info.java @@ -2,9 +2,6 @@ /** * This package contains classes to protect data at-rest. * - * The package is named persistence, because the persistence layer is used to - * help store data in a database or other data storage systems. - * * The requirements for the cryptographic algorithms for data at rest are: * *
    @@ -77,4 +74,4 @@ * * @author Jeremias Eppler */ -package com.mercedesbenz.sechub.commons.core.security.persistence; \ No newline at end of file +package com.mercedesbenz.sechub.commons.encryption; \ No newline at end of file diff --git a/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/AesGcmSivCipherTest.java b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/AesGcmSivCipherTest.java new file mode 100644 index 0000000000..c8e6370c2b --- /dev/null +++ b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/AesGcmSivCipherTest.java @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +import static org.junit.jupiter.api.Assertions.*; + +import javax.crypto.spec.SecretKeySpec; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class AesGcmSivCipherTest { + + private SecretKeySpec aes256secretKey; + private SecretKeySpec aes128secretKey; + + @BeforeEach + void beforeEach() { + /* prepare */ + byte[] aes256pwd = "a".repeat(32).getBytes(); + aes256secretKey = new SecretKeySpec(aes256pwd, "AES"); + + byte[] aes128pwd = "a".repeat(16).getBytes(); + aes128secretKey = new SecretKeySpec(aes128pwd, "AES"); + } + + @Test + void aes_gcm_siv_256_encryption_and_decryption_works_in_general() { + + /* prepare */ + AesGcmSivCipher cipherToTest = new AesGcmSivCipher(aes256secretKey, PersistentCipherType.AES_GCM_SIV_256); + + InitializationVector initVector = cipherToTest.createNewInitializationVector(); + byte[] initVectorInBytes = initVector.getInitializationBytes(); + byte[] dataToEncrypt = "i am the plain text :-)".getBytes(); + + /* execute */ + byte[] encryptedBytes = cipherToTest.encrypt(dataToEncrypt, initVector); + + /* test */ + AesGcmSivCipher cipherFromOutside = new AesGcmSivCipher(aes256secretKey, PersistentCipherType.AES_GCM_SIV_256); + byte[] decrypted = cipherFromOutside.decrypt(encryptedBytes, new InitializationVector(initVectorInBytes)); + + assertEquals(new String(dataToEncrypt), new String(decrypted)); + assertNotEquals(new String(encryptedBytes), new String(dataToEncrypt)); + + } + + @Test + void aes_gcm_siv_256_initialization_cipher_has_expected_length() { + + /* prepare */ + AesGcmSivCipher cipherToTest = new AesGcmSivCipher(aes256secretKey, PersistentCipherType.AES_GCM_SIV_256); + + /* execute */ + InitializationVector initVector = cipherToTest.createNewInitializationVector(); + + /* test */ + assertEquals(AesGcmSivCipher.IV_LENGTH_IN_BYTES, initVector.getInitializationBytes().length); + + } + + @Test + void aes_gcm_siv_128_initialization_cipher_has_expected_length() { + + /* prepare */ + AesGcmSivCipher cipherToTest = new AesGcmSivCipher(aes128secretKey, PersistentCipherType.AES_GCM_SIV_128); + + /* execute */ + InitializationVector initVector = cipherToTest.createNewInitializationVector(); + + /* test */ + assertEquals(AesGcmSivCipher.IV_LENGTH_IN_BYTES, initVector.getInitializationBytes().length); + + } + + @Test + void aes_gcm_siv_128_encryption_and_decryption_works_in_general() { + + /* prepare */ + AesGcmSivCipher cipherToTest = new AesGcmSivCipher(aes128secretKey, PersistentCipherType.AES_GCM_SIV_128); + + InitializationVector initVector = cipherToTest.createNewInitializationVector(); + byte[] initVectorInBytes = initVector.getInitializationBytes(); + byte[] dataToEncrypt = "i am the plain text :-)".getBytes(); + + /* execute */ + byte[] encryptedBytes = cipherToTest.encrypt(dataToEncrypt, initVector); + + /* test */ + AesGcmSivCipher cipherFromOutside = new AesGcmSivCipher(aes128secretKey, PersistentCipherType.AES_GCM_SIV_128); + byte[] decrypted = cipherFromOutside.decrypt(encryptedBytes, new InitializationVector(initVectorInBytes)); + + assertEquals(new String(dataToEncrypt), new String(decrypted)); + assertNotEquals(new String(encryptedBytes), new String(dataToEncrypt)); + + } + +} diff --git a/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/DefaultSecretKeyProviderTest.java b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/DefaultSecretKeyProviderTest.java new file mode 100644 index 0000000000..5df56054ad --- /dev/null +++ b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/DefaultSecretKeyProviderTest.java @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.random.RandomGenerator; +import java.util.random.RandomGeneratorFactory; + +import javax.crypto.SecretKey; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EmptySource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +class DefaultSecretKeyProviderTest { + + private static RandomGenerator randomGenerator; + + @BeforeAll + static void beforeAll() { + randomGenerator = RandomGeneratorFactory.getDefault().create(); + } + + @ParameterizedTest + @NullSource + @EmptySource + void illegal_arguments_detected(byte[] secret) { + + assertThrows(IllegalArgumentException.class, () -> new DefaultSecretKeyProvider(secret, PersistentCipherType.AES_GCM_SIV_128)); + assertThrows(IllegalArgumentException.class, () -> new DefaultSecretKeyProvider(secret, PersistentCipherType.AES_GCM_SIV_256)); + } + + @ParameterizedTest + @ValueSource(ints = { 1, 16, 32, 64 }) + void secretKey_in_bytes_is_correct_created(int secretByteLength) { + + /* prepare */ + byte[] rawTestSecret = createSecretInBytes(secretByteLength); + + /* execute */ + DefaultSecretKeyProvider providerToTest = new DefaultSecretKeyProvider(rawTestSecret, PersistentCipherType.AES_GCM_SIV_256); + + /* test */ + assertEquals(secretByteLength * 8, providerToTest.getLengthOfSecretInBits()); + + SecretKey secretKey = providerToTest.getSecretKey(); + assertNotNull(secretKey); + + byte[] encoded = secretKey.getEncoded(); + assertThat(encoded).isEqualTo(rawTestSecret); + } + + private byte[] createSecretInBytes(int secretKeyInBytes) { + byte[] randomSecret = new byte[secretKeyInBytes]; + randomGenerator.nextBytes(randomSecret); + return randomSecret; + } + +} diff --git a/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/EncryptionRotationSetupTest.java b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/EncryptionRotationSetupTest.java new file mode 100644 index 0000000000..9b4ba0b143 --- /dev/null +++ b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/EncryptionRotationSetupTest.java @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Rotation setup test will test also the builder because the setup cannot be + * created without the builder and the builder has logic inside on build time + * which must be tested as well. + * + * @author Albert Tregnaghi + * + */ +class EncryptionRotationSetupTest { + + private PersistentCipher cipher1; + private InitializationVector initialVector1; + + private PersistentCipher cipher2; + private InitializationVector initialVector2; + + @BeforeEach + void beforeEach() { + cipher1 = mock(PersistentCipher.class); + cipher2 = mock(PersistentCipher.class); + + initialVector1 = mock(InitializationVector.class); + initialVector2 = mock(InitializationVector.class); + } + + @Test + void setup_build_without_arguments_throws_exception() { + /* @formatter:off */ + assertThatThrownBy( + ()-> + EncryptionRotationSetup.builder(). + build()). + + isInstanceOf(IllegalArgumentException.class). + hasMessageStartingWith("old cipher must be defined"); + /* @formatter:on */ + } + + @Test + void setup_build_without_old_cipher_throws_exception() { + /* @formatter:off */ + assertThatThrownBy( + ()-> + EncryptionRotationSetup.builder(). + oldInitialVector(initialVector1). + build()). + + isInstanceOf(IllegalArgumentException.class). + hasMessageStartingWith("old cipher must be defined"); + /* @formatter:on */ + } + + @Test + void setup_build_without_old_initialvector_throws_exception() { + /* @formatter:off */ + assertThatThrownBy( + ()-> + EncryptionRotationSetup.builder(). + oldCipher(cipher1). + build()). + + isInstanceOf(IllegalArgumentException.class). + hasMessageStartingWith("old initial vector must be defined"); + /* @formatter:on */ + } + + @Test + void setup_build_throws_exception_when_only_old_cipher_and_old_initial_vector() { + /* @formatter:off */ + assertThatThrownBy( + ()-> + EncryptionRotationSetup.builder(). + oldCipher(cipher1). + oldInitialVector(initialVector1). + build()). + + isInstanceOf(IllegalArgumentException.class). + hasMessageStartingWith("no new cipher or a new initial vector given"); + /* @formatter:on */ + } + + @Test + void setup_build_throws_exception_when_only_new_cipher_and_new_initial_vector() { + /* @formatter:off */ + assertThatThrownBy( + ()-> + EncryptionRotationSetup.builder(). + newCipher(cipher1). + newInitialVector(initialVector1). + build()). + + isInstanceOf(IllegalArgumentException.class). + hasMessageStartingWith("old cipher must be defined"); + /* @formatter:on */ + } + + @Test + void setup_build_throws_exception_when_old_initial_vector_is_missing() { + /* @formatter:off */ + assertThatThrownBy( + ()-> + EncryptionRotationSetup.builder(). + newCipher(cipher1). + newInitialVector(initialVector1). + oldCipher(cipher2). + build()). + + isInstanceOf(IllegalArgumentException.class). + hasMessageStartingWith("old initial vector must be defined"); + /* @formatter:on */ + } + + @Test + void setup_build_throws_exception_when_old_cipher_is_missing() { + /* @formatter:off */ + assertThatThrownBy( + ()-> + EncryptionRotationSetup.builder(). + newCipher(cipher1). + newInitialVector(initialVector1). + oldInitialVector(initialVector2). + build()). + + isInstanceOf(IllegalArgumentException.class). + hasMessageStartingWith("old cipher must be defined"); + /* @formatter:on */ + } + + @Test + void setup_build_works_for_password_rotation_when_no_new_initial_vector_is_set() { + /* @formatter:off */ + + /* execute */ + EncryptionRotationSetup setup = EncryptionRotationSetup.builder(). + oldCipher(cipher1). + newCipher(cipher2). + oldInitialVector(initialVector1). + build(); + + + /* test */ + assertThat(setup).isNotNull(); + assertThat(setup.getOldCipher()).isEqualTo(cipher1); + assertThat(setup.getNewCipher()).isEqualTo(cipher2); + assertThat(setup.getOldInitialVector()).isEqualTo(initialVector1); + // no new initial vector set, builder will setup to old one automatically + assertThat(setup.getNewInitialVector()).isEqualTo(initialVector1); + + /* @formatter:on */ + } + + @Test + void setup_build_works_for_initial_vector_rotation_when_no_new_cipher_is_set() { + /* @formatter:off */ + + /* execute */ + EncryptionRotationSetup setup = EncryptionRotationSetup.builder(). + oldCipher(cipher1). + oldInitialVector(initialVector1). + newInitialVector(initialVector2). + build(); + + + /* test */ + assertThat(setup).isNotNull(); + assertThat(setup.getOldInitialVector()).isEqualTo(initialVector1); + assertThat(setup.getNewInitialVector()).isEqualTo(initialVector2); + assertThat(setup.getOldCipher()).isEqualTo(cipher1); + + // no new cipher set, builder will setup to old one automatically + assertThat(setup.getNewCipher()).isEqualTo(cipher1); + /* @formatter:on */ + } + +} diff --git a/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/EncryptionRotatorTest.java b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/EncryptionRotatorTest.java new file mode 100644 index 0000000000..0feacdcb5f --- /dev/null +++ b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/EncryptionRotatorTest.java @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class EncryptionRotatorTest { + + private EncryptionRotator rotatorToTest; + private EncryptionRotationSetup rotationSetup; + private PersistentCipher oldCipher; + private PersistentCipher newCipher; + private InitializationVector oldInitialVector; + private InitializationVector newInitialVector; + private byte[] plainTextAsBytes; + private byte[] newEncryptedData; + private byte[] oldEncryptedData; + + @BeforeEach + void beforeEach() { + plainTextAsBytes = "testdata".getBytes(); + oldEncryptedData = "old-encrypted".getBytes(); + newEncryptedData = "new-encrypted".getBytes(); + + rotatorToTest = new EncryptionRotator(); + rotationSetup = mock(EncryptionRotationSetup.class); + + oldCipher = mock(PersistentCipher.class); + newCipher = mock(PersistentCipher.class); + + oldInitialVector = mock(InitializationVector.class); + newInitialVector = mock(InitializationVector.class); + + when(rotationSetup.getOldCipher()).thenReturn(oldCipher); + when(rotationSetup.getNewCipher()).thenReturn(newCipher); + when(rotationSetup.getOldInitialVector()).thenReturn(oldInitialVector); + when(rotationSetup.getNewInitialVector()).thenReturn(newInitialVector); + + when(oldCipher.decrypt(oldEncryptedData, oldInitialVector)).thenReturn(plainTextAsBytes); + when(newCipher.encrypt(plainTextAsBytes, newInitialVector)).thenReturn(newEncryptedData); + } + + @Test + void roation_uses_old_cipher_and_initvector_to_decrypt() { + + /* execute */ + rotatorToTest.rotate(oldEncryptedData, rotationSetup); + + /* test */ + verify(oldCipher).decrypt(oldEncryptedData, oldInitialVector); + + } + + @Test + void roation_uses_new_cipher_and_new_initvector_to_encrypt() { + + /* execute */ + rotatorToTest.rotate(oldEncryptedData, rotationSetup); + + /* test */ + verify(newCipher).encrypt(plainTextAsBytes, newInitialVector); + + } + + @Test + void roation_result_is_new_encrypted_data() { + + /* execute */ + byte[] result = rotatorToTest.rotate(oldEncryptedData, rotationSetup); + + /* test */ + assertThat(result).isEqualTo(newEncryptedData); + + } + +} diff --git a/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/FullEncryptionRotationTest.java b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/FullEncryptionRotationTest.java new file mode 100644 index 0000000000..2215b8a54c --- /dev/null +++ b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/FullEncryptionRotationTest.java @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.util.stream.Stream; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +/** + * This class tests not only the rotator, but also other parts of the encryption + * library. It uses real ciphers which are generated by the cipher factory etc. + * etc. + * + * So it is not a normal unit tests for the rotator but more an integration test + * of the encryption library! + * + * @author Albert Tregnaghi + * + */ +class FullEncryptionRotationTest { + + private static PersistentCipher cipher1Aes256; + private static PersistentCipher cipher2Aes256; + private static PersistentCipher cipher3Aes128; + private static PersistentCipher cipher4Aes128; + private static PersistentCipher cipher5None; + private static PersistentCipher cipher6None; + private static InitializationVector initialVector1aes256; + private static InitializationVector initialVector2aes256; + private static InitializationVector initialVector3None; + private static InitializationVector initialVector3aes128; + private EncryptionRotator rotatorToTest; + + @BeforeAll + static void beforeAll() { + PersistentCipherFactory factory = new PersistentCipherFactory(); + PersistentCipherType aes256CipherType = PersistentCipherType.AES_GCM_SIV_256; + cipher1Aes256 = factory.createCipher(new DefaultSecretKeyProvider("x".repeat(32).getBytes(), aes256CipherType), aes256CipherType); + cipher2Aes256 = factory.createCipher(new DefaultSecretKeyProvider("y".repeat(32).getBytes(), aes256CipherType), aes256CipherType); + + PersistentCipherType aes128CipherType = PersistentCipherType.AES_GCM_SIV_128; + cipher3Aes128 = factory.createCipher(new DefaultSecretKeyProvider("x".repeat(16).getBytes(), aes128CipherType), aes128CipherType); + cipher4Aes128 = factory.createCipher(new DefaultSecretKeyProvider("y".repeat(16).getBytes(), aes128CipherType), aes128CipherType); + + cipher5None = factory.createCipher(null, PersistentCipherType.NONE); + cipher6None = factory.createCipher(null, PersistentCipherType.NONE); + + initialVector1aes256 = cipher1Aes256.createNewInitializationVector(); + initialVector2aes256 = cipher2Aes256.createNewInitializationVector(); + initialVector3aes128 = cipher2Aes256.createNewInitializationVector(); + initialVector3None = cipher5None.createNewInitializationVector(); + + } + + @BeforeEach + void beforeEach() { + rotatorToTest = new EncryptionRotator(); + } + + @Test + void secret_rotation_cipher_none() + throws InvalidKeyException, IllegalArgumentException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + /* prepare */ + byte[] rawPlainTextBytes = "hello".getBytes(EncryptionConstants.UTF8_CHARSET_ENCODING); + + NoneCipher noneCipher1 = new NoneCipher(); + InitializationVector vector1 = noneCipher1.createNewInitializationVector(); + NoneCipher noneCipher2 = new NoneCipher(); + + /* @formatter:off */ + EncryptionRotationSetup setup = EncryptionRotationSetup.builder(). + oldCipher(noneCipher1). + oldInitialVector(vector1). + newCipher(noneCipher2). + build(); + + /* @formatter:on */ + + /* execute */ + byte[] result = rotatorToTest.rotate(rawPlainTextBytes, setup); + + /* test */ + assertArrayEquals(rawPlainTextBytes, result); + } + + @ParameterizedTest + @ArgumentsSource(DualCipherSameInitVectorArgumentsProvider.class) + void cipher_rotation_same_initial_vector(String testInfo, PersistentCipher cipher1, PersistentCipher cipher2, InitializationVector initVector) { + /* prepare */ + String textToEncrypt = "Hello, I am ☺️ 4 you."; + byte[] rawBytesToEncrypt = textToEncrypt.getBytes(EncryptionConstants.UTF8_CHARSET_ENCODING); + + byte[] encryptedWithCipher1 = cipher1.encrypt(rawBytesToEncrypt, initVector); + + /* execute */ + /* @formatter:off */ + EncryptionRotationSetup setup = EncryptionRotationSetup.builder(). + oldCipher(cipher1). + oldInitialVector(initVector). + newCipher(cipher2). + build(); + + /* @formatter:on */ + byte[] encryptedAfterRotation = rotatorToTest.rotate(encryptedWithCipher1, setup); + + // sanity check for this test: the encryption may not be same... + assertThat(encryptedAfterRotation).isNotEqualTo(encryptedWithCipher1); + + /* test */ + byte[] decryptedWithCipher2AfterRotation = cipher2.decrypt(encryptedAfterRotation, initVector); + + assertThat(rawBytesToEncrypt).isEqualTo(decryptedWithCipher2AfterRotation); + + } + + @ParameterizedTest + @ArgumentsSource(DualCipherDifferentInitVectorArgumentsProvider.class) + void cipher_rotation_different_initial_vector_and_different_cipher(String testInfo, PersistentCipher cipher1, PersistentCipher cipher2, + InitializationVector initVector1, InitializationVector initVector2) { + /* prepare */ + String textToEncrypt = "Hello, I am ☺️ 4 you."; + byte[] rawBytesToEncrypt = textToEncrypt.getBytes(EncryptionConstants.UTF8_CHARSET_ENCODING); + + byte[] encryptedWithCipher1 = cipher1.encrypt(rawBytesToEncrypt, initVector1); + + /* execute */ + /* @formatter:off */ + EncryptionRotationSetup setup = EncryptionRotationSetup.builder(). + oldCipher(cipher1). + oldInitialVector(initVector1). + newCipher(cipher2). + newInitialVector(initVector2). + build(); + + /* @formatter:on */ + byte[] encryptedAfterRotation = rotatorToTest.rotate(encryptedWithCipher1, setup); + + // sanity check for this test: the encryption may not be same... + assertThat(encryptedAfterRotation).isNotEqualTo(encryptedWithCipher1); + + /* test */ + byte[] decryptedWithCipher2AfterRotation = cipher2.decrypt(encryptedAfterRotation, initVector2); + + assertThat(rawBytesToEncrypt).isEqualTo(decryptedWithCipher2AfterRotation); + + } + + private static class DualCipherSameInitVectorArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + /* @formatter:off */ + + return Stream.of( + Arguments.of("cipher1(aes256)->2(aes256)", cipher1Aes256, cipher2Aes256, initialVector1aes256), + Arguments.of("cipher1(aes256)->3(aes128)", cipher1Aes256, cipher3Aes128, initialVector1aes256), + Arguments.of("cipher2(aes256)->1(aes256)", cipher2Aes256, cipher1Aes256, initialVector2aes256), + Arguments.of("cipher3(aes128)->1(aes256)", cipher3Aes128, cipher1Aes256, initialVector3aes128), + Arguments.of("cipher3(aes128)->4(aes128)", cipher3Aes128, cipher4Aes128, initialVector3aes128), + Arguments.of("cipher2(aes256)->5(none)", cipher2Aes256, cipher5None, initialVector1aes256), + Arguments.of("cipher5(none)->1(aes256)", cipher5None, cipher1Aes256, initialVector1aes256) + + ); + /* @formatter:on */ + } + } + + private static class DualCipherDifferentInitVectorArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + /* @formatter:off */ + + return Stream.of( + Arguments.of("cipher1(aes256)->2 (aes256)", cipher1Aes256, cipher2Aes256, initialVector1aes256, initialVector2aes256), + Arguments.of("cipher5(none)->1 (aes256)", cipher5None, cipher2Aes256, initialVector3None, initialVector2aes256), + Arguments.of("cipher2(aes256)->5 (none)", cipher2Aes256, cipher5None, initialVector2aes256, initialVector3None), + Arguments.of("cipher6(none)->4(aes128)", cipher6None, cipher4Aes128, initialVector3None, initialVector2aes256) + + ); + /* @formatter:on */ + } + } + +} diff --git a/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/InitializationVectorTest.java b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/InitializationVectorTest.java new file mode 100644 index 0000000000..8960ac71ea --- /dev/null +++ b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/InitializationVectorTest.java @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class InitializationVectorTest { + + @Test + void null_bytes() throws Exception { + + /* execute */ + InitializationVector initialVectorToTest = new InitializationVector(null); + + /* test */ + assertThat(initialVectorToTest.getInitializationBytes()).isNull(); + + } + + @Test + void filled_bytes() throws Exception { + + /* execute */ + InitializationVector initialVectorToTest = new InitializationVector("filled-bytes...".getBytes()); + + /* test */ + assertThat(initialVectorToTest.getInitializationBytes()).isEqualTo("filled-bytes...".getBytes()); + + } + +} diff --git a/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/NoneCipherTest.java b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/NoneCipherTest.java new file mode 100644 index 0000000000..d532307447 --- /dev/null +++ b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/NoneCipherTest.java @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class NoneCipherTest { + + @Test + void none_cipher_encryption_and_decryption_works_but_encrypted_data_is_origin() { + + /* prepare */ + NoneCipher cipherToTest = new NoneCipher(); + + InitializationVector initVector = cipherToTest.createNewInitializationVector(); + byte[] initVectorInBytes = initVector.getInitializationBytes(); + byte[] dataToEncrypt = "i am the plain text :-)".getBytes(); + + /* execute */ + byte[] encryptedBytes = cipherToTest.encrypt(dataToEncrypt, initVector); + + /* test */ + NoneCipher cipherFromOutside = new NoneCipher(); + byte[] decrypted = cipherFromOutside.decrypt(encryptedBytes, new InitializationVector(initVectorInBytes)); + + assertEquals(new String(dataToEncrypt), new String(decrypted)); + assertEquals(new String(encryptedBytes), new String(dataToEncrypt)); + + } + +} diff --git a/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/PersistentCipherFactoryTest.java b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/PersistentCipherFactoryTest.java new file mode 100644 index 0000000000..3e8a611e5a --- /dev/null +++ b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/PersistentCipherFactoryTest.java @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PersistentCipherFactoryTest { + + private PersistentCipherFactory factoryToTest; + + @BeforeEach + void beforeEach() { + factoryToTest = new PersistentCipherFactory(); + } + + @Test + void type_aes_gcm_siv_256_can_be_created__with_secret_of_32Byte() { + /* prepare */ + SecretKeyProvider secretKeyProvider = mock(SecretKeyProvider.class); + when(secretKeyProvider.getLengthOfSecretInBits()).thenReturn(256); + + /* execute */ + PersistentCipher cipher = factoryToTest.createCipher(secretKeyProvider, PersistentCipherType.AES_GCM_SIV_256); + + /* test */ + assertTrue(cipher instanceof AesGcmSivCipher); + assertEquals(PersistentCipherType.AES_GCM_SIV_256, cipher.getType()); + + } + + @Test + void type_aes_gcm_siv_1289_can_be_created__with_secret_of128bit() { + /* prepare */ + SecretKeyProvider secretKeyProvider = mock(SecretKeyProvider.class); + when(secretKeyProvider.getLengthOfSecretInBits()).thenReturn(128); + + /* execute */ + PersistentCipher cipher = factoryToTest.createCipher(secretKeyProvider, PersistentCipherType.AES_GCM_SIV_128); + + /* test */ + assertTrue(cipher instanceof AesGcmSivCipher); + assertEquals(PersistentCipherType.AES_GCM_SIV_128, cipher.getType()); + + } + + @ParameterizedTest + @ValueSource(ints = { 0, 1, 16, 128, 512 }) + void type_aes_gcm_siv_256_can_NOT_be_created__with_secret_having_wrong_amunt_of_bits(int amountOfBits) { + /* prepare */ + SecretKeyProvider secretKeyProvider = mock(SecretKeyProvider.class); + when(secretKeyProvider.getLengthOfSecretInBits()).thenReturn(amountOfBits); + + /* execute */ + assertThrows(IllegalArgumentException.class, () -> factoryToTest.createCipher(secretKeyProvider, PersistentCipherType.AES_GCM_SIV_256)); + + } + + @ParameterizedTest + @ValueSource(ints = { 0, 1, 16, 64, 256 }) + void type_aes_gcm_siv_128_can_NOT_be_created__with_secret_having_wrong_amunt_of_bits(int amountOfBits) { + + /* prepare */ + SecretKeyProvider secretKeyProvider = mock(SecretKeyProvider.class); + when(secretKeyProvider.getLengthOfSecretInBits()).thenReturn(amountOfBits); + + /* execute */ + assertThrows(IllegalArgumentException.class, () -> factoryToTest.createCipher(secretKeyProvider, PersistentCipherType.AES_GCM_SIV_128)); + + } + + @Test + void type_none_can_be_created() { + + /* execute */ + PersistentCipher cipher = factoryToTest.createCipher(null, PersistentCipherType.NONE); + + /* test */ + assertTrue(cipher instanceof NoneCipher); + assertEquals(PersistentCipherType.NONE, cipher.getType()); + + } + +} diff --git a/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/StringByteArrayTransformerTest.java b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/StringByteArrayTransformerTest.java new file mode 100644 index 0000000000..786a69b899 --- /dev/null +++ b/sechub-commons-encryption/src/test/java/com/mercedesbenz/sechub/commons/encryption/StringByteArrayTransformerTest.java @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.encryption; + +import static org.assertj.core.api.Assertions.*; + +import java.nio.charset.Charset; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EmptySource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +class StringByteArrayTransformerTest { + + private StringByteArrayTransformer transformerToTest; + + @BeforeEach + void beforeEach() { + transformerToTest = new StringByteArrayTransformer(); + } + + /* @formatter:off */ + @ParameterizedTest + @NullSource + @EmptySource + @ValueSource(strings= { + "hello world", + "🍐🍌🍓🍉", + "Hello 🦄", + "🥦🥕🥔🫘🥒🫑🌽🍆", + "Äpfel sind grün" + }) + /* @formatter:on */ + void transformFromBytes_bytes_are_transformed_back_via_utf_8(String plainText) { + /* prepare */ + byte[] bytes = plainText == null ? null : plainText.getBytes(Charset.forName("UTF-8")); + + /* execute */ + String result = transformerToTest.transformFromBytes(bytes); + + /* test */ + assertThat(result).isEqualTo(plainText); + } + + /* @formatter:off */ + @ParameterizedTest + @NullSource + @EmptySource + @ValueSource(strings= { + "hello world", + "🍐🍌🍓🍉", + "Hello 🦄", + "🥦🥕🥔🫘🥒🫑🌽🍆", + "Äpfel sind grün" + }) + /* @formatter:on */ + void transformToBytes(String plainText) { + /* prepare */ + + /* execute */ + Object result = transformerToTest.transformToBytes(plainText); + + /* test */ + byte[] expectedBytes = plainText == null ? null : plainText.getBytes(Charset.forName("UTF-8")); + assertThat(result).isEqualTo(expectedBytes); + } + +} diff --git a/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/data/PDSJobStatus.java b/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/data/PDSJobStatus.java index b3fb1d0ae2..a453f13e39 100644 --- a/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/data/PDSJobStatus.java +++ b/sechub-commons-pds/src/main/java/com/mercedesbenz/sechub/commons/pds/data/PDSJobStatus.java @@ -3,23 +3,95 @@ import java.util.UUID; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +/** + * This class represents the schedule job status which can be obtained by REST + * + */ +@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(Include.NON_NULL) public class PDSJobStatus { - public UUID jobUUID; + public static final String PROPERTY_JOBUUID = "jobUUID"; + public static final String PROPERTY_OWNER = "owner"; + public static final String PROPERTY_CREATED = "created"; + public static final String PROPERTY_STARTED = "started"; + public static final String PROPERTY_ENDED = "ended"; + public static final String PROPERTY_STATE = "state"; + public static final String PROPERTY_ENCRYPTION_OUT_OF_SYNCH = "encryptionOutOfSync"; + + private UUID jobUUID; + + private String owner; + + private String created; + private String started; + private String ended; - public String owner; + private boolean encryptionOutOfSync; - public String created; - public String started; - public String ended; + private PDSJobStatusState state; + + public UUID getJobUUID() { + return jobUUID; + } + + public void setJobUUID(UUID jobUUID) { + this.jobUUID = jobUUID; + } - public PDSJobStatusState state; + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getCreated() { + return created; + } + + public void setCreated(String created) { + this.created = created; + } + + public String getStarted() { + return started; + } + + public void setStarted(String started) { + this.started = started; + } + + public String getEnded() { + return ended; + } + + public void setEnded(String ended) { + this.ended = ended; + } + + public PDSJobStatusState getState() { + return state; + } + + public void setState(PDSJobStatusState state) { + this.state = state; + } + + public boolean isEncryptionOutOfSync() { + return encryptionOutOfSync; + } - @Override - public String toString() { - return "PDSJobStatus [" + (state != null ? "state=" + state + ", " : "") + (jobUUID != null ? "jobUUID=" + jobUUID + ", " : "") - + (owner != null ? "owner=" + owner + ", " : "") + (created != null ? "created=" + created + ", " : "") - + (started != null ? "started=" + started + ", " : "") + (ended != null ? "ended=" + ended : "") + "]"; + public void setEncryptionOutOfSync(boolean encryptionOutOfSync) { + this.encryptionOutOfSync = encryptionOutOfSync; } } diff --git a/sechub-developertools/build.gradle b/sechub-developertools/build.gradle index e56563b48a..787b032dee 100644 --- a/sechub-developertools/build.gradle +++ b/sechub-developertools/build.gradle @@ -15,6 +15,7 @@ dependencies { implementation project(':sechub-scan') implementation project(':sechub-commons-pds') + implementation project(':sechub-commons-encryption') implementation project(':sechub-wrapper-checkmarx') implementation library.apache_commons_io diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/DeveloperAdministration.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/DeveloperAdministration.java index 195c06fbcd..e54020c4a8 100644 --- a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/DeveloperAdministration.java +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/DeveloperAdministration.java @@ -44,6 +44,8 @@ import com.mercedesbenz.sechub.integrationtest.internal.TestRestHelper; import com.mercedesbenz.sechub.integrationtest.internal.TestRestHelper.RestHelperTarget; import com.mercedesbenz.sechub.sharedkernel.ProductIdentifier; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionData; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionStatus; import com.mercedesbenz.sechub.sharedkernel.project.ProjectAccessLevel; import com.mercedesbenz.sechub.test.PDSTestURLBuilder; import com.mercedesbenz.sechub.test.SecHubTestURLBuilder; @@ -801,4 +803,12 @@ public String fetchProjectJobInfoForUser(String projectId, int pageSize, int pag return TestJSONHelper.get().createJSON(listPage, true); } + public String rotateEncryption(SecHubEncryptionData data) { + return asTestUser().rotateEncryption(data); + } + + public SecHubEncryptionStatus fetchEncryptionStatus() { + return asTestUser().fetchEncryptionStatus(); + } + } diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/ComboxSelectionDialogUI.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/ComboxSelectionDialogUI.java index 6c78a2baac..949ddce683 100644 --- a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/ComboxSelectionDialogUI.java +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/ComboxSelectionDialogUI.java @@ -15,10 +15,10 @@ import javax.swing.JPanel; import javax.swing.border.EmptyBorder; -public class ComboxSelectionDialogUI { +public class ComboxSelectionDialogUI { private JLabel label; - private JComboBox combobox; + private JComboBox combobox; private JButton cancelButton; private JButton okButton; @@ -27,7 +27,7 @@ public class ComboxSelectionDialogUI { private JDialog dialog; - public ComboxSelectionDialogUI(JFrame parentFrame, String title, String labelText, List comboboxValues, String initialValue) { + public ComboxSelectionDialogUI(JFrame parentFrame, String title, String labelText, List comboboxValues, T initialValue) { dialog = new JDialog(parentFrame, title, true); JPanel content = new JPanel(new BorderLayout()); @@ -45,12 +45,12 @@ public ComboxSelectionDialogUI(JFrame parentFrame, String title, String labelTex } - private JPanel createComboBoxPanel(String labelText, List values, String initialValue) { + private JPanel createComboBoxPanel(String labelText, List values, T initialValue) { JPanel comboboxPanel = new JPanel(new BorderLayout()); label = new JLabel(labelText); - DefaultComboBoxModel model = new DefaultComboBoxModel<>(); - for (String value : values) { + DefaultComboBoxModel model = new DefaultComboBoxModel<>(); + for (T value : values) { model.addElement(value); } @@ -83,8 +83,9 @@ public void showDialog() { combobox.requestFocusInWindow(); } - public String getSelectionFromCombobox() { - String inputValue = (String) combobox.getSelectedItem(); + public T getSelectionFromCombobox() { + @SuppressWarnings("unchecked") + T inputValue = (T) combobox.getSelectedItem(); return inputValue; } diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/CommandUI.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/CommandUI.java index 37cacc1600..11a85dc139 100644 --- a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/CommandUI.java +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/CommandUI.java @@ -32,6 +32,10 @@ import com.mercedesbenz.sechub.developertools.admin.ui.action.config.ListExecutionProfilesAction; import com.mercedesbenz.sechub.developertools.admin.ui.action.config.ListExecutorConfigurationsAction; import com.mercedesbenz.sechub.developertools.admin.ui.action.developerbatchops.DeveloperBatchCreateCheckmarxTestSetupAction; +import com.mercedesbenz.sechub.developertools.admin.ui.action.encryption.FetchSecHubEncryptionStatusAction; +import com.mercedesbenz.sechub.developertools.admin.ui.action.encryption.RotateSecHubEncryptionAction; +import com.mercedesbenz.sechub.developertools.admin.ui.action.encryption.SecretKeyGeneratorAction; +import com.mercedesbenz.sechub.developertools.admin.ui.action.encryption.TestDecryptToStringAction; import com.mercedesbenz.sechub.developertools.admin.ui.action.integrationtestserver.FetchMockMailsAction; import com.mercedesbenz.sechub.developertools.admin.ui.action.job.CancelJobAction; import com.mercedesbenz.sechub.developertools.admin.ui.action.job.DownloadFullscanDataForJobAction; @@ -206,6 +210,7 @@ private void createMainMenu() { createConfigMenu(); createPDSMenu(); createSecHubClientMenu(); + createEncryptionMenu(); } public void createEditMenu() { @@ -247,6 +252,17 @@ public void createConfigMenu() { menu.add(new ConfigureAutoCleanupAction(context)); } + public void createEncryptionMenu() { + JMenu menu = new JMenu("Encryption"); + menuBar.add(menu); + menu.add(new FetchSecHubEncryptionStatusAction(context)); + menu.addSeparator(); + menu.add(new SecretKeyGeneratorAction(context)); + menu.add(new TestDecryptToStringAction(context)); + menu.addSeparator(); + menu.add(new RotateSecHubEncryptionAction(context)); + } + private ShowProductExecutorTemplatesDialogAction register(ShowProductExecutorTemplatesDialogAction action) { showProductExecutorTemplatesDialogActions.add(action); return action; diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/DialogUI.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/DialogUI.java index 724ee8146e..8d6a7bbf8a 100644 --- a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/DialogUI.java +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/DialogUI.java @@ -61,7 +61,18 @@ public void warn(String message) { * @return file or null */ public File selectFile(String initialPath) { - return selectFile(initialPath, null); + return selectFile(initialPath, null, null); + } + + /* + * Selects file by file chooser + * + * @param initialPath the initial file path or null + * + * @return file or null + */ + public File selectFile(String initialPath, String title) { + return selectFile(initialPath, null, title); } /** @@ -73,6 +84,11 @@ public File selectFile(String initialPath) { * @return file or null */ public File selectFile(String initialPath, javax.swing.filechooser.FileFilter fileFilter) { + return selectFile(initialPath, fileFilter, null); + } + + public File selectFile(String initialPath, javax.swing.filechooser.FileFilter fileFilter, String title) { + fileChooser.setDialogTitle(title); fileChooser.setFileFilter(fileFilter); if (initialPath != null) { File file = new File(initialPath); @@ -139,8 +155,8 @@ public Optional getUserInput(String message, String defaultValue) { * @param identifier * @return */ - public Optional getUserInputFromCombobox(String message, String title, List comboboxValues, String initialValue) { - ComboxSelectionDialogUI dialog = new ComboxSelectionDialogUI(frame, title, message, comboboxValues, initialValue); + public Optional getUserInputFromCombobox(String message, String title, List comboboxValues, T initialValue) { + ComboxSelectionDialogUI dialog = new ComboxSelectionDialogUI<>(frame, title, message, comboboxValues, initialValue); dialog.showDialog(); if (!dialog.isOkPressed()) { diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/AbstractUIAction.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/AbstractUIAction.java index 59025f4851..96f46903df 100644 --- a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/AbstractUIAction.java +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/AbstractUIAction.java @@ -3,6 +3,7 @@ import java.awt.event.ActionEvent; import java.net.URL; +import java.util.Arrays; import java.util.Optional; import javax.swing.AbstractAction; @@ -214,6 +215,17 @@ protected Optional getUserPassword(String message, InputCacheIdentifier return x; } + /** + * Shows an input dialog for selecting one of given types + * + * @param title + * @param text + * @return + */ + protected Optional getUserInputFromCombobox(String title, T initialValue, String message, @SuppressWarnings({ "unchecked" }) T... identifier) { + return getContext().getDialogUI().getUserInputFromCombobox(message, title, Arrays.asList(identifier), initialValue); + } + /** * Shows an input dialog for user (multi line) and sets given text as content * diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/encryption/FetchSecHubEncryptionStatusAction.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/encryption/FetchSecHubEncryptionStatusAction.java new file mode 100644 index 0000000000..4e4c3b6393 --- /dev/null +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/encryption/FetchSecHubEncryptionStatusAction.java @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.developertools.admin.ui.action.encryption; + +import java.awt.event.ActionEvent; + +import com.mercedesbenz.sechub.developertools.admin.ui.UIContext; +import com.mercedesbenz.sechub.developertools.admin.ui.action.AbstractUIAction; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionStatus; + +public class FetchSecHubEncryptionStatusAction extends AbstractUIAction { + private static final long serialVersionUID = 1L; + + public FetchSecHubEncryptionStatusAction(UIContext context) { + super("Fetch SecHub encryption status", context); + } + + @Override + public void execute(ActionEvent e) { + + SecHubEncryptionStatus status = getContext().getAdministration().fetchEncryptionStatus(); + outputAsTextOnSuccess(status.toFormattedJSON()); + } + +} \ No newline at end of file diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/encryption/RotateSecHubEncryptionAction.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/encryption/RotateSecHubEncryptionAction.java new file mode 100644 index 0000000000..0843b17d61 --- /dev/null +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/encryption/RotateSecHubEncryptionAction.java @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.developertools.admin.ui.action.encryption; + +import java.awt.event.ActionEvent; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mercedesbenz.sechub.developertools.admin.ui.UIContext; +import com.mercedesbenz.sechub.developertools.admin.ui.action.AbstractUIAction; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherAlgorithm; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherPasswordSourceType; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionData; + +public class RotateSecHubEncryptionAction extends AbstractUIAction { + private static final long serialVersionUID = 1L; + + private static final Logger LOG = LoggerFactory.getLogger(RotateSecHubEncryptionAction.class); + + public RotateSecHubEncryptionAction(UIContext context) { + super("Rotate SecHub encryption", context); + } + + @Override + public void execute(ActionEvent e) { + + Optional optSelectedAlgorithm = getUserInputFromCombobox("Select algorithm to use for encryption", + SecHubCipherAlgorithm.AES_GCM_SIV_256, "Select algorithm", SecHubCipherAlgorithm.values()); + if (!optSelectedAlgorithm.isPresent()) { + return; + } + String sourceData = null; + SecHubCipherPasswordSourceType sourceType; + SecHubCipherAlgorithm selectedAlgorithm = optSelectedAlgorithm.get(); + switch (selectedAlgorithm) { + case NONE: + sourceType = SecHubCipherPasswordSourceType.NONE; + break; + case AES_GCM_SIV_128: + case AES_GCM_SIV_256: + default: + sourceType = SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE; + Optional optEnvironmentVariableName = getUserInput("Please enter environment name for password"); + if (!optEnvironmentVariableName.isPresent()) { + return; + } + sourceData = optEnvironmentVariableName.get(); + break; + + } + String confirmMessage = """ + You want to start encryption rotation with: + + Cipher algorithm : '%s' + Password source + - type: '%s' + - data: '%s' + + This will update encryption pool on all SecHub servers inside cluster + and will also start automated re-encryption of jobs which are in state + CANCELED or ENDED. + + Are you sure ? + """.formatted(selectedAlgorithm, sourceType, sourceData); + if (!confirm(confirmMessage)) { + return; + } + LOG.info("start encryption rotation with algorithm: {}, password souce type: {}, password source data: {}", selectedAlgorithm, sourceType, sourceData); + + SecHubEncryptionData data = new SecHubEncryptionData(); + data.setAlgorithm(selectedAlgorithm); + data.setPasswordSourceType(sourceType); + data.setPasswordSourceData(sourceData); + + String infoMessage = getContext().getAdministration().rotateEncryption(data); + outputAsTextOnSuccess(infoMessage); + } + + @Override + protected boolean canConfirmationBeOverridenBySetup() { + return false; + } + +} \ No newline at end of file diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/encryption/SecretKeyGeneratorAction.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/encryption/SecretKeyGeneratorAction.java new file mode 100644 index 0000000000..23ecd26841 --- /dev/null +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/encryption/SecretKeyGeneratorAction.java @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.developertools.admin.ui.action.encryption; + +import java.awt.event.ActionEvent; +import java.util.Base64; +import java.util.Base64.Encoder; +import java.util.random.RandomGenerator; +import java.util.random.RandomGeneratorFactory; + +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherType; +import com.mercedesbenz.sechub.developertools.admin.ui.UIContext; +import com.mercedesbenz.sechub.developertools.admin.ui.action.AbstractUIAction; + +public class SecretKeyGeneratorAction extends AbstractUIAction { + private static final long serialVersionUID = 1L; + + private RandomGenerator randomGenerator; + + public SecretKeyGeneratorAction(UIContext context) { + super("Secret key generator", context); + randomGenerator = RandomGeneratorFactory.of("SecureRandom").create(); + } + + @Override + public void execute(ActionEvent e) { + + byte[] random256Bit = new byte[32]; + byte[] random128Bit = new byte[16]; + + randomGenerator.nextBytes(random256Bit); + randomGenerator.nextBytes(random128Bit); + + Encoder encoder = Base64.getEncoder(); + String random256BitBase64encoded = encoder.encodeToString(random256Bit); + String random128BitBase64encoded = encoder.encodeToString(random128Bit); + + String output = """ + Generated random keys + (base64 encoded - ready to use as environment variables for encryption ) + + 256 bit: %s + usable for + - %s + + 128 bit: %s + usable for + - %s + + + """.formatted(random256BitBase64encoded, PersistentCipherType.AES_GCM_SIV_256, random128BitBase64encoded, PersistentCipherType.AES_GCM_SIV_128); + + outputAsTextOnSuccess(output); + } + +} diff --git a/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/encryption/TestDecryptToStringAction.java b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/encryption/TestDecryptToStringAction.java new file mode 100644 index 0000000000..fbd26d6798 --- /dev/null +++ b/sechub-developertools/src/main/java/com/mercedesbenz/sechub/developertools/admin/ui/action/encryption/TestDecryptToStringAction.java @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.developertools.admin.ui.action.encryption; + +import java.awt.event.ActionEvent; +import java.io.File; +import java.nio.file.Files; +import java.util.Base64; +import java.util.Optional; + +import com.mercedesbenz.sechub.commons.encryption.DefaultSecretKeyProvider; +import com.mercedesbenz.sechub.commons.encryption.EncryptionSupport; +import com.mercedesbenz.sechub.commons.encryption.InitializationVector; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipher; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherFactory; +import com.mercedesbenz.sechub.commons.encryption.SecretKeyProvider; +import com.mercedesbenz.sechub.developertools.admin.ui.UIContext; +import com.mercedesbenz.sechub.developertools.admin.ui.action.AbstractUIAction; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherAlgorithm; + +public class TestDecryptToStringAction extends AbstractUIAction { + private static final long serialVersionUID = 1L; + + private static final PersistentCipherFactory cipherFactory = new PersistentCipherFactory(); + private static final EncryptionSupport encryptionSupport = new EncryptionSupport(); + + public TestDecryptToStringAction(UIContext context) { + super("Test decrypt to string", context); + } + + @Override + public void execute(ActionEvent e) { + + /* -------algorithm--------------------------- */ + Optional optSelectedAlgorithm = getUserInputFromCombobox("Select algorithm to use for encryption", + SecHubCipherAlgorithm.AES_GCM_SIV_256, "Select algorithm", SecHubCipherAlgorithm.values()); + if (!optSelectedAlgorithm.isPresent()) { + return; + } + SecHubCipherAlgorithm selectedAlgorithm = optSelectedAlgorithm.get(); + + /* ------secret key---------------------------- */ + Optional optBase64testSecretKey = getUserPassword("Base64 test secret key", null); + if (optBase64testSecretKey.isEmpty()) { + return; + } + String base64 = optBase64testSecretKey.get(); + byte[] decoded = Base64.getDecoder().decode(base64.getBytes()); + SecretKeyProvider secretKeyProvider = null; + + switch (selectedAlgorithm) { + case NONE: + secretKeyProvider = null; + break; + default: + secretKeyProvider = new DefaultSecretKeyProvider(decoded, selectedAlgorithm.getType()); + } + + output("secret key provider:" + secretKeyProvider); + + /* ------create and use cipher---------------------- */ + PersistentCipher cipher = cipherFactory.createCipher(secretKeyProvider, selectedAlgorithm.getType()); + + /* ------select files------------------------------- */ + String userHomeFolder = System.getProperty("user.home"); + File encryptedDataFile = getContext().getDialogUI().selectFile(userHomeFolder, "Please select encrypted data file"); + if (encryptedDataFile == null) { + return; + } + File initialVectorFile = getContext().getDialogUI().selectFile(userHomeFolder, "Please select initial vector file"); + if (initialVectorFile == null) { + return; + } + + /* ------decrypt------------------------------- */ + try { + byte[] dataBytes = Files.readAllBytes(encryptedDataFile.toPath()); + byte[] vectorBytes = Files.readAllBytes(initialVectorFile.toPath()); + + String decryptedAsString = encryptionSupport.decryptString(dataBytes, cipher, new InitializationVector(vectorBytes)); + File file = new File(encryptedDataFile.getParentFile(), encryptedDataFile.getName() + ".txt"); + Files.writeString(file.toPath(), decryptedAsString); + + output("Decrypted file written to " + file); + + } catch (Exception ex) { + error(ex.getMessage()); + ex.printStackTrace(); + } + + } + +} diff --git a/sechub-doc/src/docs/asciidoc/diagrams/diagram_encryption_pds.puml b/sechub-doc/src/docs/asciidoc/diagrams/diagram_encryption_pds.puml new file mode 100644 index 0000000000..b8885341cb --- /dev/null +++ b/sechub-doc/src/docs/asciidoc/diagrams/diagram_encryption_pds.puml @@ -0,0 +1,44 @@ +' SPDX-License-Identifier: MIT +@startuml + +'Hide empty parts: +hide empty fields +hide empty methods + +'You can find more examles at https://plantuml.com/class-diagram + +skinparam linetype ortho +'skinparam linetype polyline +package com.mercedesbenz.sechub.commons.encryption as common_encrypt { + + class PersistentCipherFactory + class PersistentCipher + class EncryptionSupport +} + +package com.mercedesbenz.sechub.pds as pds_root{ + + class PDSStartupAssertEnvironmentVariablesUsed { + } + + package encryption as pds_encryption{ + class PDSEncryptionConfiguration #aliceblue ##darkblue + class PDSEncryptionService #aliceblue ##darkblue { + + } + } + package job as pds_job{ + class PDSCreateJobService + class PDSJobConfigurationAccess + } +} +PDSEncryptionService ..> PDSEncryptionConfiguration +PersistentCipherFactory --> PersistentCipher + +PDSEncryptionService ..> PersistentCipherFactory +PDSEncryptionService ..> EncryptionSupport +PDSEncryptionService --> PersistentCipher + + +PDSJobConfigurationAccess --> PDSEncryptionService +PDSCreateJobService --> PDSEncryptionService \ No newline at end of file diff --git a/sechub-doc/src/docs/asciidoc/diagrams/diagram_encryption_sechub_config.puml b/sechub-doc/src/docs/asciidoc/diagrams/diagram_encryption_sechub_config.puml new file mode 100644 index 0000000000..3e8cce1cd7 --- /dev/null +++ b/sechub-doc/src/docs/asciidoc/diagrams/diagram_encryption_sechub_config.puml @@ -0,0 +1,96 @@ +' SPDX-License-Identifier: MIT +@startuml + +'Hide empty parts: +hide empty fields +hide empty methods + +'You can find more examles at https://plantuml.com/class-diagram + +'skinparam linetype ortho +'skinparam linetype polyline + +database START_ENCRYPTION_ROTATION as rotateEvent #darkorange { +} +note right of rotateEvent +asynchronous +end note +database SCHEDULE_ENCRYPTION_POOL_INITIALIZED as poolInitEvent #limegreen { +} +note right of poolInitEvent +asynchronous +end note + +package com.mercedesbenz.sechub.domain.administration { + package encryption as adm_encryption{ + class EncryptionAdministrationRestController + class AdministrationEncryptionRotationService + } +} + +package com.mercedesbenz.sechub.domain.schedule{ + + package job as schedule_job { + class ScheduleSecHubJob { + byte[] getEncryptedConfiguration() + } + + class SecHubJobFactory { + + } + + class SecHubConfigurationModelAccess { + resolveUnencryptedConfiguration(ScheduleSecHubJob job) + } + + class ScheduleSecHubJobEncryptionUpdateService #limegreen ##green { + updateEncryptedDataIfNecessary() + } + + } + + package encryption as schedule_encryption { + + class ScheduleRefreshEncryptionServiceSetupTriggerService #aliceblue ##darkblue { + triggerEncryptionSetupRefresh() + } + + class ScheduleEncryptionService #aliceblue ##darkblue { + applicationStarted() + refreshEncryptionPoolAndLatestIdIfNecessary() + + encryptWithLatestCipher(String plainText) + String decryptToString(byte[] encrypted, Long encryptionPoolId, InitializationVector initialVector) + ScheduleEncryptionResult rotateEncryption(byte[] data, Long oldCipherPoolId, InitializationVector oldInitialVector) + } + + class ScheduleCipherPoolData #darkorange { + } + + class ScheduleEncryptionPool #aliceblue ##darkblue { + PersistentCipher getCipherForPoolId(Long poolId) + } + + class ScheduleEncryptionRotationService #darkorange { + startEncryptionRotation() + } + } + +EncryptionAdministrationRestController -> AdministrationEncryptionRotationService +AdministrationEncryptionRotationService --> rotateEvent +rotateEvent -[#darkorange]-> ScheduleEncryptionRotationService + +ScheduleEncryptionRotationService -[#darkorange]-> ScheduleCipherPoolData : (A1) create new + +ScheduleRefreshEncryptionServiceSetupTriggerService -[#darkblue]-> ScheduleEncryptionService: (B1) trigger refresh +ScheduleEncryptionRotationService -[#darkorange]-> ScheduleEncryptionService: (A2) trigger refresh +ScheduleEncryptionPool <-[#darkblue]- ScheduleEncryptionService: (A3,B2): refesh pool + +ScheduleEncryptionService -[#darkblue]-> poolInitEvent: (A4,B3): trigger update +poolInitEvent -[#limegreen]-> ScheduleSecHubJobEncryptionUpdateService: (A5,B4): trigger update + +SecHubConfigurationModelAccess --> ScheduleSecHubJob +SecHubJobFactory ..> ScheduleSecHubJob: create +ScheduleSecHubJobEncryptionUpdateService -[#limegreen]-> ScheduleSecHubJob: updates + +@enduml diff --git a/sechub-doc/src/docs/asciidoc/diagrams/diagram_encryption_sechub_use_of_commons.puml b/sechub-doc/src/docs/asciidoc/diagrams/diagram_encryption_sechub_use_of_commons.puml new file mode 100644 index 0000000000..e49711130c --- /dev/null +++ b/sechub-doc/src/docs/asciidoc/diagrams/diagram_encryption_sechub_use_of_commons.puml @@ -0,0 +1,31 @@ +' SPDX-License-Identifier: MIT +@startuml + +'Hide empty parts: +hide empty fields +hide empty methods + +'You can find more examles at https://plantuml.com/class-diagram + +skinparam linetype ortho +'skinparam linetype polyline +package com.mercedesbenz.sechub.commons.encryption as common_encrypt { + + class PersistentCipherFactory + class PersistentCipher + class EncryptionSupport +} + +package com.mercedesbenz.sechub.domain.schedule.encryption as schedule_encrypt{ + class ScheduleEncryptionService #aliceblue ##darkblue + class ScheduleEncryptionPool #aliceblue ##darkblue { + PersistentCipher getCipherForPoolId(Long poolId) + } +} + +PersistentCipherFactory --> PersistentCipher + +ScheduleEncryptionService ..> PersistentCipherFactory +ScheduleEncryptionService -[#darkblue]> ScheduleEncryptionPool +ScheduleEncryptionService ..> EncryptionSupport +ScheduleEncryptionPool --> PersistentCipher \ No newline at end of file diff --git a/sechub-doc/src/docs/asciidoc/diagrams/diagram_pds_events_storage.puml b/sechub-doc/src/docs/asciidoc/diagrams/diagram_pds_events_storage.puml index 8e77a84faa..4b02555452 100644 --- a/sechub-doc/src/docs/asciidoc/diagrams/diagram_pds_events_storage.puml +++ b/sechub-doc/src/docs/asciidoc/diagrams/diagram_pds_events_storage.puml @@ -72,7 +72,7 @@ note bottom of eventFile Remark: Currently not implemented, but if an event type shall supports multiple - files in fture the name pattern shall be: + files in future the name pattern shall be: "${eventTypeName}[${nr}].json" end note diff --git a/sechub-doc/src/docs/asciidoc/documents/architecture/08_concepts.adoc b/sechub-doc/src/docs/asciidoc/documents/architecture/08_concepts.adoc index 7dbf99a8ff..ee26c07351 100644 --- a/sechub-doc/src/docs/asciidoc/documents/architecture/08_concepts.adoc +++ b/sechub-doc/src/docs/asciidoc/documents/architecture/08_concepts.adoc @@ -43,4 +43,7 @@ include::../shared/concepts/concept_analytic.adoc[] === Statistics include::../shared/concepts/concept_statistic.adoc[] +=== Data encryption +include::../shared/concepts/concept_sechub_data_encryption.adoc[] + diff --git a/sechub-doc/src/docs/asciidoc/documents/code2doc/usecases/user/upload_binaries_description.adoc b/sechub-doc/src/docs/asciidoc/documents/code2doc/usecases/user/upload_binaries_description.adoc index 20848289c4..15121b7b03 100644 --- a/sechub-doc/src/docs/asciidoc/documents/code2doc/usecases/user/upload_binaries_description.adoc +++ b/sechub-doc/src/docs/asciidoc/documents/code2doc/usecases/user/upload_binaries_description.adoc @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT [[sechub-doclink-uc-user-uploads-binaries-for-job]] -A user wants to upload binearies for a former <>. +A user wants to upload binaries for a former <>. The binaries must be inside a valid tar file. \ No newline at end of file diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_auto_clean.adoc b/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_auto_clean.adoc index 4613ec8959..ebab254553 100644 --- a/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_auto_clean.adoc +++ b/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_auto_clean.adoc @@ -3,6 +3,8 @@ === Auto cleanup To prevent full hard drives there is an option to automatically remove old data. +It also cleans up old encryption settings when it comes to <>. + [NOTE] ==== See also diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_pds.adoc b/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_pds.adoc index 7b27ec39ce..d4cb3ddfe1 100644 --- a/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_pds.adoc +++ b/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_pds.adoc @@ -18,7 +18,7 @@ need to provide a `ProductDelegationServer` (in short form `PDS`). - a spring boot application which uses a network DB for providing cluster possibility - a complete standalone application without runtime dependencies to {sechub} server (or its shared kernel) - provides REST access -- a very simple priviledge model with just two users (`tech user` + `admin user`), +- a very simple privilege model with just two users (`tech user` + `admin user`), basic auth via `TLS`, credentials are simply defined by environment entries on startup - provides jobs, queing, monitoring etc. - can execute single files (e.g. a bash script), where job parameters are @@ -32,6 +32,16 @@ need to provide a `ProductDelegationServer` (in short form `PDS`). ==== Big picture plantuml::diagrams/diagram_concept_product_delgation_server_bigpicture.puml[] +==== Encryption +include::concept_pds_data_encryption.adoc[] + +[[concept-pds-auto-cleanup]] +==== Auto cleanup +The PDS provides an auto cleanup mechanism which will remove old PDS jobs automatically. + +The default configuration is set to 2 days. +Administrators can change the default configuration via REST. + [[pds-storage-and-sharing]] ==== Storage and sharing @@ -211,7 +221,7 @@ The Adapter will always be the same, but filled with other necessary parameters. NOTE: So there will be no need to write any adapter or executor when using PDS! -=== HowTo integrate a new product via PDS +==== HowTo integrate a new product via PDS Having new security product XYZ but being a command line tool, we @@ -247,6 +257,13 @@ Having new security product XYZ but being a command line tool, we - test via {sechub} client by starting a new {sechub} job which shall use the product and verify results +[CAUTION] +==== +Output and error stream of a PDS launcher script are stored in {pds} database as plain text! +Means: NEVER log any sensitive data in launcher scripts! + +If you want to give hints for debugging etc. you have to mask the information in log output. +==== diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_pds_data_encryption.adoc b/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_pds_data_encryption.adoc new file mode 100644 index 0000000000..7187325098 --- /dev/null +++ b/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_pds_data_encryption.adoc @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +[[section-shared-concepts-pds-data-encryption]] +In {pds} we can have also some sensitive data we want to be encrypted. For example: The remote data +section inside the sechub job configuration contains credentials to fetch data. +Such sensitive information shall be always encrypted. + +===== General +We want + +. Simple encryption rotation approach + +In contrast to {sechub}, the data in the {pds} is only temporary and is not made available for a +longer period of time. Subsequent access to encrypted data is also no longer necessary, +but only while a SecHub job is running. + + + + +This means we simply accept the situation that a PDS restart with new encryption setup could +lead to a situation where a former created job is no longer executable by PDS. + +When the encryption changes for a job between its creation and when it begins running, the job will +be marked automatically as failed and having encryption out of sync. +The PDS product executor at {sechub} side will take care of such a situation and will restart +a new PDS job (which will then be encrypted correctly again). + +. Full automated + +There is no need for manual interaction - means it is not necessary to create any cron jobs or +something else to convert non encrypted data to encrypted data or to rotate a password or to +use a new encryption method. + +. Data protection /Privacy policy +- Even for administrators it shall not be possible to fetch the information directly + + _(of course a person who knows the encryption password and has access to the database will always + be able to calculate values - but we separate here between administration and operation inside + this concept, so protection is fully possible)_ +- The data must not be accidentally made available in decrypted form - for example through a REST + call in which the data object is passed along unencrypted. + +. Easy encryption administration + - It shall be possible for an administrator to configure a new cipher entry at deployment time + +. Secure storage of encryption passwords + + - Encryption passwords are always provided via environment entries, we store always + the environment variable name to use inside the database but never plain values! + +===== PDS startup +A {pds} server only knows the encryption defined inside two variables: + +- `PDS_ENCRYPTION_SECRET_KEY` + + contains the base64 encoded secret key used for encryption +- `PDS_ENCRYPTION_ALGORITHM` + + contains the information about the used encryption algorithm. Can be + + `NONE`, `AES_GCM_SIV_128` or `AES_GCM_SIV_256` . + + +This setup will be used inside the complete instance as long as it is running. +There is no pooling of different encryptions (in constrast to {sechub}, where pooling feature exists). + +[IMPORTANT] +==== +If the secret key is not a base 64 value the server startup will fail! +==== + + +===== Administration +[[section-shared-concepts-pds-data-encryption-rotation]] +====== Encryption rotation +There is no complete rotation of encryption - old data will have no encryption update. + +But an administrator is able to do re-deployment of the PDS cluster +and using other secret or algorithm. + +This will + +- use new encryption setup for all new PDS jobs +- keep existing encrypted data as is +- can lead to a rare race condition when {sechub} has created the job with old PDS instance and + new PDS instance tries to run the PDS job (the access to the encrypted data is no longer possible) + +[TIP] +==== +Via <> the old data will automatically disappear. +If an encryption cleanup for PDS via auto cleanup is too late (e.g. credentials were leaked and +an update is really urgent) , it is still possible to just delete +via SQL all jobs at database which have a timestamp older then the newest deployment time (or +just all). +==== + +====== Encryption status +There is no direct possibility to check encryption status. But the job contains a creation time stamp +and can be mapped to the startup of containers if this would become necessary. + +====== Cleanup old encrypted data +<> automatically removes old information. +This means that old encrypted information (with older encryption settings) automatically +disappears after a certain period of time. + +Since no other encryption data is persisted except in the PDS job, nothing else needs to be cleaned up. + +===== Diagrams +plantuml::diagrams/diagram_encryption_pds.puml[format=svg, title="title"] + diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_sechub_data_encryption.adoc b/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_sechub_data_encryption.adoc new file mode 100644 index 0000000000..c34ee3a8d1 --- /dev/null +++ b/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_sechub_data_encryption.adoc @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: MIT +[[section-shared-concepts-sechub-data-encryption]] +In {sechub} we have some sensitive data we want to be encrypted. For example: Inside remote data +section the configuration contains credentials to fetch data. These sensitive information shall +be always encrypted. + +==== General +We want + +. Data consistency + +- It shall not be possible that we loose data by encryption in any way. + + It must be ensured that the servers are always able to read and write data. + +. Full automated cipher rotation + +There is no need for manual interaction - means it is not necessary to create any cron jobs or +something else to convert non encrypted data to encrypted data or to rotate a password or to +use a new encryption method. + +. Data protection /Privacy policy +- Even for administrators it shall not be possible to fetch the information directly + + _(of course a person who knows the encryption password and has access to the database will always + be able to calculate values - but we separate here between administration and operation inside + this concept, so protection is fully possible)_ +- The data must not be accidentally made available in decrypted form - for example through a REST + call in which the data object is passed along unencrypted. + +. Easy encryption administration + - It shall be possible for an administrator to configure a new cipher entry via REST + +. Secure storage of encryption passwords + + - Encryption passwords are always provided via environment entries, we store always + the environment variable name to use inside the database but never plain values! + +==== Server startup +A {sechub} server will stop on startup phase when one of the entries inside the cipher pool cannot +be handled by this server. + +This ensures that every new started server is able to handle all of them / is always readable. + +==== Administration +[[section-shared-concepts-sechub-data-encryption-rotation]] +===== Encryption rotation + +An administrator is able to start encryption rotation via REST. This will + +- use new encryption setup for all new data +- automatically convert existing encrypted data with new encryption setup in background + +===== Encryption status +An administrator is able to fetch encryption status from {sechub} server. All domains which are +doing data encryption add their current status information into result. + +===== Cleanup old encryption setup +<> automatically removes old information. +This means that old encrypted information that cannot be updated for some reason may eventually +disappear and old encryption configurations are then no longer needed and can be removed. + +To fully automate this, after the respective Auto Cleanup, the domains are always checked for encryption configurations that are no longer used and these are then automatically removed (except for the most recent encryption configuration). + + +[IMPORTANT] +==== +If you have setup auto cleanup to 0 days, the auto cleanup is disabled completely and +unused encryption setup will also not be removed. +==== + +==== Scheduler +Inside the schedule domain, the sensitive information is the sechub job configuration. + +===== Database +====== Table +We store the cipher information inside table: `SCHEDULE_CIPHER_POOL_DATA`. + +[NOTE] +==== +Why in schedule domain and only there? Because it is the responsible domain for the storage. All other +domains may NEVER persist this information (for `PDS` the configuration will be sent from SecHub +and stored at `PDS` side encrypted as well) +==== + +Here an an overview of the table (names can be different in database): + +[options="header"] +|=== +|id |algorithm | password_source_type |password_source_data| encoding |test_text | test_initial_vector| test_encrypted | creation_timestamp |created_from +//----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +|0 |NO_ENCRYPTION | PLAIN_TEXT | | PLAIN |no-encryption | | no-encryption | 2024-06-24_10:00:01 | null +|1 |AES_GCM_SIV_128| ENVIRONMENT_VARIABLE |SECHUB_CRYPTO_P0 | PLAIN |SecHub | easdfa313334 | 53d$125666eeffeded | 2024-06-24_10:05:34 | Admin1 +|2 |AES_GCM_SIV_256| ENVIRONMENT_VARIABLE |SECHUB_CRYPTO_P1 | PLAIN |Apfel | fxadsfeec33s | 13x313412124$rfewd | 2024-06-24_11:02:14 | Admin2 +|=== + + +*algorithm* + +Algorithm to use in encryption - currently we provide: + +- NONE (means not encrypted!) +- AES_GCM_SIV_128 +- AES_GCM_SIV_256 + +*password_source_type* + +Currently supported password source types are + +. ENVIRONMENT_VARIABLE + + Here we provide environment variables, the password source data is the name of the environment variable +. NONE + + No password - only allowed for `NONE` algorithm + + +We separated source type and source data to be able to provide additional source - e.g. a password fault for the future. + +*password_source_data* + +Depends on the source + +- If source is `env` than this is the name of the environment variable which holds the secret + +====== Usage inside rows + +Inside the encrypted rows we will persist the *pool id* together with an *initial vector* + +*initial vector* +Some algorithm like `AES_GCM_SIV` do need an initial vector to encrypt secure. The value here is +auto generated by SecHub and is dependent on the algorithm. + +SecHub will always auto generate a dedicate value when it comes to encryption and the vector +will be stored together with the encrypted data. If the initial vector is changed, the row cannot +be decrypted, even when the secret key is known! + +===== Constraints on scheduling +The only situation we need to access the encrypted job configuration is the point, when +it comes to job execution. At all other situations it does not matter if the configuration +can be decrypted or not. + +This means that it may not be possible that an scheduler instance executes a job which is +not supported by the current encryption pool! + +==== Handling server updates +===== {sechub} server 1.x to 2.x +Old server versions do not have the encryption field inside the scheduler job table or the cipher pool table. + +Our SQL migration scripts will initialize scheduler cipher pool table on creation time with a +`NONE` entry (pool id = 0). This is encryption setup (meaning no encryption) will be added +to all existing jobs. + +We want to have zero downtime and rolling updates with k8s and SecHub. To provide this, +it must be ensured, that there is no old server running which creates new jobs with +plain text configurations while update is running. To prevent such a situation +the column name inside `schedule_sechub_job` have been renamed from `configuration` to `unencrypted_configuration`. +If there appears any race conditions, old servers would no longer be able to write data and a +SQL error would happen. + +==== Handling server downgrade +===== {sechub} server 2.x to 1.x +For a downgrade from {sechub} server V2.x to V1.x it is necessary to ensure, that all data is +encrypted with `NONE` cipher type (can be done by encryption rotation). When ensured that everything +is "encrypted" with this cipher type, the old server version can be deployed/used and migration +is automatically done as usual. + +==== Handling sensitive data at runtime +JVM crash dumps contain string information. Classes containing sensitive information shall +store such information inside sealed objects. + +==== Handling metadata from job configuration +The {secHub} configuration is encrypted, because it can contain sensitive data. E.g. when defining a remote data +section. + +There exists a REST endpoint which gives users the possiblity to fetch job information, together with +the meta data defined inside the {sechub} configuration. + +To obtain this information, the configuration will be decrypted temporary at runtime and the meta +data are resolved and returned. + +Because meta data shall not contain any sensitive information, this will not be audit logged. + + +==== Diagrams +===== Usage of encryption commons +plantuml::diagrams/diagram_encryption_sechub_use_of_commons.puml[] + +===== Encryption rotation overview +plantuml::diagrams/diagram_encryption_sechub_config.puml[title='a reduced view of the steps done on encryption rotation'] diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/configuration/sechub_config.adoc b/sechub-doc/src/docs/asciidoc/documents/shared/configuration/sechub_config.adoc index 4ee7891ef4..f6a3c376d7 100644 --- a/sechub-doc/src/docs/asciidoc/documents/shared/configuration/sechub_config.adoc +++ b/sechub-doc/src/docs/asciidoc/documents/shared/configuration/sechub_config.adoc @@ -480,6 +480,15 @@ include::sechub_config_example20_remote_data_with_credentials_binaries_licensesc ==== MetaData The {sechub} configuration file can have optional meta data. +[IMPORTANT] +==== +The {sechub} configuration is stored encrypted in database and access is restricted, even +for administrators. But the meta data can be fetched by users of the project or administrators +without additional audit logging. + +Because of this you should never store sensitive information inside the meta data! +==== + ===== Labels With labels a user is able to add additional information to the scan configuration which end up in the report as key value pairs. diff --git a/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/kubernetes/KubernetesTemplateFilesGenerator.java b/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/kubernetes/KubernetesTemplateFilesGenerator.java index fbed326b81..d8a1bd45c5 100644 --- a/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/kubernetes/KubernetesTemplateFilesGenerator.java +++ b/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/kubernetes/KubernetesTemplateFilesGenerator.java @@ -59,10 +59,10 @@ public void generate(KubernetesFiles result, List list) throw list.add(newSecret("sechub.server.ssl.keystore.file", "ssl", "The server ssl certificate file", "server-certificate.p12")); /* - * config (normally unnecessary because automatical generated, but we want .json - * as file ending, so here necessary + * configuration (normally unnecessary because automatical generated, but we + * want .json as file ending, so here necessary */ - list.add(newSecret("sechub.scan.config.initial", "config", "The initial scan configuration", "sechub_scan_config_initial.json")); + list.add(newSecret("sechub.scan.config.initial", "configuration", "The initial scan configuration", "sechub_scan_config_initial.json")); Collections.sort(list); generateDeploymentFilePart(result, list); diff --git a/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/spring/SystemPropertiesDescriptionGenerator.java b/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/spring/SystemPropertiesDescriptionGenerator.java index 44af665883..aa05dbef87 100644 --- a/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/spring/SystemPropertiesDescriptionGenerator.java +++ b/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/spring/SystemPropertiesDescriptionGenerator.java @@ -74,7 +74,7 @@ protected String generate(List list, SecureEnvironmentVariabl protected void appendStringContent(StringBuilder sb, Map> rowMap) { for (Map.Entry> entries : rowMap.entrySet()) { SortedSet table = entries.getValue(); - sb.append("[[section-gen-config-scope-").append(entries.getKey()).append("]]\n"); + sb.append("[[section-gen-configuration-scope-").append(entries.getKey()).append("]]\n"); sb.append("[options=\"header\",cols=\"1,1,1\"]\n"); sb.append(".").append(buildTitle(entries.getKey())); sb.append("\n|===\n"); diff --git a/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/util/RestDocFactory.java b/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/util/RestDocFactory.java index 30453a6b36..f88b7da5ad 100644 --- a/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/util/RestDocFactory.java +++ b/sechub-doc/src/main/java/com/mercedesbenz/sechub/docgen/util/RestDocFactory.java @@ -120,4 +120,5 @@ public static String extractTag(String apiEndpoint) { return tag; } + } \ No newline at end of file diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/ExampleFilesValidTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/ExampleFilesValidTest.java index 8570efd7fd..0f9233c1c9 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/ExampleFilesValidTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/ExampleFilesValidTest.java @@ -92,7 +92,7 @@ void every_sechub_config_webscan_file_is_valid_and_has_a_target_uri(ExampleFile /* test */ Optional webScanOpt = config.getWebScan(); - assertTrue(webScanOpt.isPresent(), "Webscan config does exist for file: " + file.getPath()); + assertTrue(webScanOpt.isPresent(), "Webscan configuration does exist for file: " + file.getPath()); SecHubWebScanConfiguration webScan = webScanOpt.get(); assertNotNull(webScan.getUrl(), "No URI set in file: " + file.getPath()); diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/DownloadsFullScanDataForJobRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/DownloadsFullScanDataForJobRestDocTest.java index 1f47195d20..d6c4772377 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/DownloadsFullScanDataForJobRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/DownloadsFullScanDataForJobRestDocTest.java @@ -79,8 +79,7 @@ public void before() { d.metaData = "{}"; data.allScanData.add(d); - String config = "{}"; - ProjectScanLog log = new ProjectScanLog("theProject", jobUUID, "spartakus", config); + ProjectScanLog log = new ProjectScanLog("theProject", jobUUID, "spartakus"); data.allScanLogs.add(log); when(fullScanDataService.getFullScanData(jobUUID)).thenReturn(data); diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/EncryptionAdministrationRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/EncryptionAdministrationRestControllerRestDocTest.java new file mode 100644 index 0000000000..9f1179d55c --- /dev/null +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/EncryptionAdministrationRestControllerRestDocTest.java @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.restdoc; + +import static com.mercedesbenz.sechub.docgen.util.RestDocFactory.*; +import static com.mercedesbenz.sechub.restdoc.RestDocumentation.*; +import static com.mercedesbenz.sechub.test.SecHubTestURLBuilder.*; +import static org.mockito.Mockito.*; +import static org.springframework.restdocs.headers.HeaderDocumentation.*; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.lang.annotation.Annotation; +import java.time.LocalDateTime; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import com.mercedesbenz.sechub.commons.model.job.ExecutionState; +import com.mercedesbenz.sechub.domain.administration.encryption.AdministrationEncryptionRotationService; +import com.mercedesbenz.sechub.domain.administration.encryption.AdministrationEncryptionStatusService; +import com.mercedesbenz.sechub.domain.administration.encryption.EncryptionAdministrationRestController; +import com.mercedesbenz.sechub.domain.administration.job.JobAdministrationRestController; +import com.mercedesbenz.sechub.sharedkernel.Profiles; +import com.mercedesbenz.sechub.sharedkernel.RoleConstants; +import com.mercedesbenz.sechub.sharedkernel.configuration.AbstractSecHubAPISecurityConfiguration; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherAlgorithm; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherPasswordSourceType; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubDomainEncryptionData; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubDomainEncryptionStatus; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionData; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionDataValidator; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionStatus; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubPasswordSource; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; +import com.mercedesbenz.sechub.sharedkernel.usecases.encryption.UseCaseAdminFetchesEncryptionStatus; +import com.mercedesbenz.sechub.sharedkernel.usecases.encryption.UseCaseAdminStartsEncryptionRotation; +import com.mercedesbenz.sechub.test.ExampleConstants; +import com.mercedesbenz.sechub.test.TestIsNecessaryForDocumentation; +import com.mercedesbenz.sechub.test.TestPortProvider; + +@RunWith(SpringRunner.class) +@WebMvcTest(JobAdministrationRestController.class) +@ContextConfiguration(classes = { EncryptionAdministrationRestController.class, SecHubEncryptionDataValidator.class, + EncryptionAdministrationRestControllerRestDocTest.SimpleTestConfiguration.class }) +@WithMockUser(roles = RoleConstants.ROLE_SUPERADMIN) +@ActiveProfiles({ Profiles.TEST, Profiles.ADMIN_ACCESS }) +@AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443) +public class EncryptionAdministrationRestControllerRestDocTest implements TestIsNecessaryForDocumentation { + + private static final int PORT_USED = TestPortProvider.DEFAULT_INSTANCE.getRestDocTestPort(); + + @Autowired + private MockMvc mockMvc; + + @MockBean + AdministrationEncryptionRotationService encryptionRotationService; + + @MockBean + AdministrationEncryptionStatusService encryptionStatusService; + + @Before + public void before() { + + } + + @Test + @UseCaseRestDoc(useCase = UseCaseAdminStartsEncryptionRotation.class) + public void restdoc_admin_starts_encryption_rotation() throws Exception { + + /* prepare */ + + SecHubEncryptionData data = new SecHubEncryptionData(); + data.setAlgorithm(SecHubCipherAlgorithm.AES_GCM_SIV_256); + data.setPasswordSourceType(SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE); + data.setPasswordSourceData("SECRET_1"); + + String apiEndpoint = https(PORT_USED).buildAdminStartsEncryptionRotation(); + Class useCase = UseCaseAdminStartsEncryptionRotation.class; + + /* execute + test @formatter:off */ + + this.mockMvc.perform( + post(apiEndpoint). + contentType(MediaType.APPLICATION_JSON_VALUE). + content(data.toFormattedJSON()). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(extractTag(apiEndpoint)). + and(). + document( + requestHeaders( + + ) + )); + + /* @formatter:on */ + } + + @Test + @UseCaseRestDoc(useCase = UseCaseAdminFetchesEncryptionStatus.class) + public void restdoc_admin_fetches_encryption_status() throws Exception { + + /* prepare */ + + SecHubEncryptionStatus status = createEncryptionStatusExample(); + + when(encryptionStatusService.fetchStatus()).thenReturn(status); + + String apiEndpoint = https(PORT_USED).buildAdminFetchesEncryptionStatus(); + Class useCase = UseCaseAdminFetchesEncryptionStatus.class; + + /* execute + test @formatter:off */ + String domains = SecHubEncryptionStatus.PROPERTY_DOMAINS+"[]."; + String domainData = domains+SecHubDomainEncryptionStatus.PROPERTY_DATA+"[]."; + + this.mockMvc.perform( + get(apiEndpoint). + contentType(MediaType.APPLICATION_JSON_VALUE). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(extractTag(apiEndpoint)). + responseSchema(OpenApiSchema.ENCRYPTION_STATUS.getSchema()). + and(). + document( + requestHeaders( + + ), + responseFields( + fieldWithPath(SecHubEncryptionStatus.PROPERTY_TYPE).description("The type description of the json content"), + fieldWithPath(domains+SecHubDomainEncryptionStatus.PROPERTY_NAME).description("Name of the domain which will provide this encryption data elements"), + fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_ID).description("Unique identifier"), + fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_ALGORITHM).description("Algorithm used for encryption"), + fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_PASSWORDSOURCE+"."+ SecHubPasswordSource.PROPERTY_TYPE).description("Type of password source. Can be "+List.of(SecHubCipherPasswordSourceType.values())), + fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_PASSWORDSOURCE+"."+ SecHubPasswordSource.PROPERTY_DATA).description("Data for password source. If type is "+SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE+" then it is the the name of the environment variable."), + fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_USAGE).description("Map containing information about usage of this encryption"), + fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_USAGE+".*").description("Key value data"), + fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_CREATED).description("Creation timestamp"), + fieldWithPath(domainData+SecHubDomainEncryptionData.PROPERTY_CREATED_FROM).description("User id of admin who created the encryption entry") + ) + )); + + /* @formatter:on */ + } + + private SecHubEncryptionStatus createEncryptionStatusExample() { + SecHubEncryptionStatus status = new SecHubEncryptionStatus(); + SecHubDomainEncryptionStatus scheduleDomainEncryptionStatus = new SecHubDomainEncryptionStatus(); + scheduleDomainEncryptionStatus.setName("schedule"); + + // create some example domain encryption data like in really + SecHubDomainEncryptionData scheduleDomainEncryptionData = new SecHubDomainEncryptionData(); + scheduleDomainEncryptionData.setAlgorithm(SecHubCipherAlgorithm.AES_GCM_SIV_256); + scheduleDomainEncryptionData.setCreated(LocalDateTime.of(2024, 8, 1, 9, 26)); + scheduleDomainEncryptionData.setCreatedFrom("admin-username"); + scheduleDomainEncryptionData.setId("1"); + scheduleDomainEncryptionData.getPasswordSource().setType(SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE); + scheduleDomainEncryptionData.getPasswordSource().setData("SECRET_1"); + + long value = 1; + for (ExecutionState state : ExecutionState.values()) { + scheduleDomainEncryptionData.getUsage().put("job.state." + state.name().toLowerCase(), value++); + } + + scheduleDomainEncryptionStatus.getData().add(scheduleDomainEncryptionData); + status.getDomains().add(scheduleDomainEncryptionStatus); + return status; + } + + @EnableAutoConfiguration + public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { + + } + +} diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/JobAdministrationRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/JobAdministrationRestControllerRestDocTest.java index 2c9b197ab5..29d03d5115 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/JobAdministrationRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/JobAdministrationRestControllerRestDocTest.java @@ -81,7 +81,6 @@ public void before() { info.setStatus(JobStatus.RUNNING); info.setProjectId("project-name"); - info.setConfiguration("{ config data }"); info.setOwner("owner-userid"); info.setSince(LocalDateTime.now()); @@ -119,8 +118,7 @@ public void restdoc_list_all_running_jobs() throws Exception { fieldWithPath(inArray(JobInformation.PROPERTY_PROJECT_ID)).description("The name of the project the job is running for"), fieldWithPath(inArray(JobInformation.PROPERTY_OWNER)).description("Owner of the job - means user which triggered it"), fieldWithPath(inArray(JobInformation.PROPERTY_STATUS)).description("A status information "), - fieldWithPath(inArray(JobInformation.PROPERTY_SINCE)).description("Timestamp since when job has been started"), - fieldWithPath(inArray(JobInformation.PROPERTY_CONFIGURATION)).description("Configuration used for this job") + fieldWithPath(inArray(JobInformation.PROPERTY_SINCE)).description("Timestamp since when job has been started") ) )); @@ -227,7 +225,7 @@ public void restdoc_restart_job_hard() throws Exception { } // see - // https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#documenting-your-api-request-response-payloads-fields-json + // https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#documenting-your-api-request-response-payloads-fields-json private static String inArray(String field) { return "[]." + field; } diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/OpenApiSchema.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/OpenApiSchema.java index 38c95ee49d..df032df5e6 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/OpenApiSchema.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/OpenApiSchema.java @@ -64,6 +64,8 @@ enum OpenApiSchema { PROJECT_JOB_LIST("ProjectJobList"), + ENCRYPTION_STATUS("EncryptionStatus"), + ; private final Schema schema; diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProductExecutionProfileRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProductExecutionProfileRestControllerRestDocTest.java index 7c2b34c16c..a9f5c9fe6d 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProductExecutionProfileRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProductExecutionProfileRestControllerRestDocTest.java @@ -336,7 +336,7 @@ public void restdoc_admin_fetches_profile() throws Exception { fieldWithPath(PROPERTY_ENABLED).description("Enabled state of profile, default is false").optional(), fieldWithPath(PROPERTY_CONFIGURATIONS+"[]."+ProductExecutorConfig.PROPERTY_UUID).description("uuid of configuration"), fieldWithPath(PROPERTY_CONFIGURATIONS+"[]."+ProductExecutorConfig.PROPERTY_NAME).description("name of configuration"), - fieldWithPath(PROPERTY_CONFIGURATIONS+"[]."+ProductExecutorConfig.PROPERTY_ENABLED).description("enabled state of this config"), + fieldWithPath(PROPERTY_CONFIGURATIONS+"[]."+ProductExecutorConfig.PROPERTY_ENABLED).description("enabled state of this configuration"), fieldWithPath(PROPERTY_CONFIGURATIONS+"[]."+ProductExecutorConfig.PROPERTY_PRODUCTIDENTIFIER).description("executed product"), fieldWithPath(PROPERTY_CONFIGURATIONS+"[]."+ProductExecutorConfig.PROPERTY_EXECUTORVERSION).description("executor version"), fieldWithPath(PROPERTY_CONFIGURATIONS+"[]."+ProductExecutorConfig.PROPERTY_SETUP+"."+ProductExecutorConfigSetup.PROPERTY_BASEURL).ignored(), diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProductExecutorConfigRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProductExecutorConfigRestControllerRestDocTest.java index 277856f357..a86fe82ad8 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProductExecutorConfigRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/ProductExecutorConfigRestControllerRestDocTest.java @@ -121,7 +121,7 @@ public void restdoc_admin_creates_executor_config() throws Exception { TestExecutorConfig configFromUser = new TestExecutorConfig(); configFromUser.enabled = false; - configFromUser.name = "PDS gosec config 1"; + configFromUser.name = "PDS gosec configuration 1"; configFromUser.productIdentifier = ProductIdentifier.PDS_CODESCAN.name(); configFromUser.executorVersion = 1; configFromUser.setup.baseURL = "https://productXYZ.example.com"; diff --git a/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/AsPDSUser.java b/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/AsPDSUser.java index 730e92c6a9..5a4329491d 100644 --- a/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/AsPDSUser.java +++ b/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/AsPDSUser.java @@ -54,7 +54,7 @@ public String getJobStatus(UUID jobUUID) { public PDSJobStatusState getJobStatusState(UUID jobUUID) { PDSJobStatus pdsJobStatus = getJobStatusObject(jobUUID); - return pdsJobStatus.state; + return pdsJobStatus.getState(); } public PDSJobStatus getJobStatusObject(UUID jobUUID) { diff --git a/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/AsUser.java b/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/AsUser.java index abf3f931a0..0a53267ff3 100644 --- a/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/AsUser.java +++ b/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/AsUser.java @@ -44,6 +44,8 @@ import com.mercedesbenz.sechub.integrationtest.internal.TestAutoCleanupData; import com.mercedesbenz.sechub.integrationtest.internal.TestJSONHelper; import com.mercedesbenz.sechub.integrationtest.internal.TestRestHelper; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionData; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionStatus; import com.mercedesbenz.sechub.sharedkernel.project.ProjectAccessLevel; import com.mercedesbenz.sechub.test.SecHubTestURLBuilder; import com.mercedesbenz.sechub.test.TestUtil; @@ -1307,4 +1309,16 @@ public String tryToCreateJobByJson(TestProject project, String sechubConfigAsStr } + public String rotateEncryption(SecHubEncryptionData data) { + + String url = getUrlBuilder().buildAdminStartsEncryptionRotation(); + return getRestHelper().postJson(url, data.toFormattedJSON()); + } + + public SecHubEncryptionStatus fetchEncryptionStatus() { + String url = getUrlBuilder().buildAdminFetchesEncryptionStatus(); + String json = getRestHelper().getJSON(url); + return SecHubEncryptionStatus.fromString(json); + } + } diff --git a/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/AssertEncryptionStatus.java b/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/AssertEncryptionStatus.java new file mode 100644 index 0000000000..fb805429a9 --- /dev/null +++ b/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/AssertEncryptionStatus.java @@ -0,0 +1,87 @@ +package com.mercedesbenz.sechub.integrationtest.api; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubDomainEncryptionStatus; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionStatus; + +public class AssertEncryptionStatus { + + private SecHubEncryptionStatus status; + + private static final Logger LOG = LoggerFactory.getLogger(AssertEncryptionStatus.class); + + public static AssertEncryptionStatus assertEncryptionStatus(SecHubEncryptionStatus status) { + return new AssertEncryptionStatus(status); + } + + private AssertEncryptionStatus(SecHubEncryptionStatus status) { + if (status == null) { + throw new AssertionError("Encryption status was null!"); + } + this.status = status; + } + + public AssertDomainEncryptionStatus domain(String domainName) { + + List domains = status.getDomains(); + for (SecHubDomainEncryptionStatus domain : domains) { + if (domain.getName().equalsIgnoreCase(domainName)) { + return new AssertDomainEncryptionStatus(domain); + } + } + throw new AssertionError("Encryption status was null!"); + } + + public class AssertDomainEncryptionStatus { + + private SecHubDomainEncryptionStatus domain; + + private AssertDomainEncryptionStatus(SecHubDomainEncryptionStatus domain) { + this.domain = domain; + } + + /** + * Return to parent assertion level + * + * @return encryption status assert object + */ + public AssertEncryptionStatus encryptionStatus() { + return AssertEncryptionStatus.this; + } + + public AssertDomainEncryptionStatus hasData() { + if (domain.getData().isEmpty()) { + throw new AssertionError("No data available for domain: " + domain.getName()); + } + return this; + } + + public AssertDomainEncryptionStatus hasData(int expectedAmountOfData) { + int amount = domain.getData().size(); + + if (amount != expectedAmountOfData) { + + dump(); + + throw new AssertionError("Not expected amount of data available for domain: " + domain.getName() + ", expected was: " + expectedAmountOfData + + ", found: " + amount); + } + return this; + } + + public int getDataSize() { + return domain.getData().size(); + } + + } + + public AssertEncryptionStatus dump() { + LOG.info("Dump encrpytion status object:\n{}", status.toFormattedJSON()); + return this; + } + +} diff --git a/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/IntegrationTestSetup.java b/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/IntegrationTestSetup.java index 5bcf660474..6ac1591814 100644 --- a/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/IntegrationTestSetup.java +++ b/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/IntegrationTestSetup.java @@ -219,6 +219,8 @@ public void evaluate() throws Throwable { LOG.error("#"); LOG.error("#########################################################################"); LOG.error("# Wasnt able to prepare scenario:{}", scenario.getName()); + LOG.error("# Reason: {}", e.getMessage()); + LOG.error("# (for more details look in unit test stack trace output)"); LOG.error("#########################################################################"); LOG.error("Last url :" + TestRestHelper.getLastUrl()); LOG.error("Last data:" + TestRestHelper.getLastData()); diff --git a/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/TestAPI.java b/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/TestAPI.java index 0c30a3faed..92f61a8f96 100644 --- a/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/TestAPI.java +++ b/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/api/TestAPI.java @@ -51,6 +51,7 @@ import com.mercedesbenz.sechub.integrationtest.internal.TestJSONHelper; import com.mercedesbenz.sechub.integrationtest.internal.TestRestHelper; import com.mercedesbenz.sechub.integrationtest.internal.autoclean.TestAutoCleanJsonDeleteCount; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionStatus; import com.mercedesbenz.sechub.sharedkernel.logging.SecurityLogData; import com.mercedesbenz.sechub.sharedkernel.messaging.IntegrationTestEventHistory; import com.mercedesbenz.sechub.test.ExampleConstants; @@ -118,6 +119,14 @@ public static AssertUserJobInfo assertUserJobInfo(TestSecHubJobInfoForUserListPa return AssertUserJobInfo.assertInfo(page); } + public static AssertEncryptionStatus assertEncryptionStatus() { + return assertEncryptionStatus(as(SUPER_ADMIN).fetchEncryptionStatus()); + } + + public static AssertEncryptionStatus assertEncryptionStatus(SecHubEncryptionStatus status) { + return AssertEncryptionStatus.assertEncryptionStatus(status); + } + /** * Asserts given report json - it will try to find report elements * @@ -1255,6 +1264,18 @@ public boolean runAndReturnTrueWhenSuccesfulImpl() throws Exception { }); } + /** + * Starts cipher pool cleanup for scheduler domain directly for test scenario. + * Normally this is done by auto cleanup mechanism only, but with this method it + * is also possible to trigger the cleanup inside integration tests. + */ + public static void startScheduleCipherPoolDataCleanup() { + resetAutoCleanupDays(0); + + String url = getURLBuilder().buildIntegrationTestStartScheduleCipherPoolDataCleanup(); + getSuperAdminRestHelper().put(url); + } + /** * Will ensure complete auto cleanup inspector is reset and that auto cleanup is * set to "wantedFormerDays days" in configuration and also in every domain auto @@ -1339,6 +1360,12 @@ public static List fetchAutoCleanupInspectionDelet } + public static Long fetchScheduleEncryptionPoolIdForJob(UUID jobUUID) { + String url = getURLBuilder().buildIntegrationTestFetchScheduleEncryptionPoolIdForSecHubJob(jobUUID); + String result = getSuperAdminRestHelper().getStringFromURL(url); + return Long.valueOf(result); + } + public static FullScanData fetchFullScanData(UUID sechubJobUIUD) { String url = getURLBuilder().buildIntegrationTestFetchFullScandata(sechubJobUIUD); @@ -1631,4 +1658,5 @@ private static UUID ensureConfigHasUUID(TestExecutorConfig executorConfig, TestE } return executorConfig.uuid; } + } diff --git a/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/internal/PersistentScenarioTestDataProvider.java b/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/internal/PersistentScenarioTestDataProvider.java index edb76065db..c31d5cf217 100644 --- a/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/internal/PersistentScenarioTestDataProvider.java +++ b/sechub-integrationtest/src/main/java/com/mercedesbenz/sechub/integrationtest/internal/PersistentScenarioTestDataProvider.java @@ -7,6 +7,9 @@ import java.io.IOException; import java.util.Properties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Provides persistent test data for integration tests - e.g. growing ids for * test scenarios. @@ -16,6 +19,8 @@ */ public class PersistentScenarioTestDataProvider { + private static final Logger LOG = LoggerFactory.getLogger(PersistentScenarioTestDataProvider.class); + private static final String BASE_SECHUB_INTEGRATIONTEST_DATA_KEY = "sechub.integrationtest.data."; private static final String SECHUB_INTEGRATIONTEST_DATA_GROWINGID = BASE_SECHUB_INTEGRATIONTEST_DATA_KEY + "growingid"; private int grow; @@ -43,25 +48,52 @@ public PersistentScenarioTestDataProvider(GrowingScenario scenario) { } private void ensurePropertyFileExists() { - file.getParentFile().mkdirs(); + LOG.trace("Ensure test scenario property file exists: {}", file); properties = new Properties(); if (file.exists()) { + LOG.trace("File exists: {}", file); - try (FileInputStream fis = new FileInputStream(file)) { - properties.load(fis); - String d = properties.getProperty(SECHUB_INTEGRATIONTEST_DATA_GROWINGID); - if (d == null) { - grow = 0; - } else { - grow = Integer.parseInt(d); + boolean loaded = false; + int tryCount = 0; + Exception lastException = null; + while (!loaded && tryCount < 5) { + tryCount++; + + LOG.trace("Start load of properties file: {} per stream. Try count:{}", file, tryCount); + try (FileInputStream fis = new FileInputStream(file)) { + properties.load(fis); + LOG.trace("Properties loaded: {}, contains: {}", file.getName(), properties); + String d = properties.getProperty(SECHUB_INTEGRATIONTEST_DATA_GROWINGID); + LOG.trace("Properties: {}, growing id value: {}", file.getName(), d); + if (d == null) { + grow = 0; + } else { + grow = Integer.parseInt(d); + } + LOG.trace("Properties: {}, grow set to: {}", file.getName(), grow); + loaded = true; + } catch (Exception e) { + lastException = e; + LOG.trace("Properties load failed, will wait shortly and retry some time", e); + try { + Thread.sleep(500); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } } - } catch (Exception e) { + } + if (!loaded) { + LOG.trace("Properties load failed, will no longer retry, but delete file: {}", file); + this.file.delete(); - throw new IllegalStateException("cannot read growid file: " + file.getAbsolutePath() + ", so deleted as fallback", e); + throw new IllegalStateException("Cannot read growid file: " + file.getAbsolutePath() + ", so deleted as fallback", lastException); } } if (!file.exists()) { + file.getParentFile().mkdirs(); + + LOG.trace("File NOT exists: {}", file); try { file.createNewFile(); } catch (IOException e) { @@ -89,6 +121,8 @@ public void increaseGrowId() { } private void store() { + LOG.trace("Try to store property file: {}", file); + File parentFolder = file.getParentFile(); if (!parentFolder.exists()) { if (!parentFolder.mkdirs()) { @@ -100,6 +134,7 @@ private void store() { } catch (IOException e) { throw new IllegalStateException("cannot store: " + file.getAbsolutePath(), e); } + LOG.trace("Stored property file: {}, content was: {}", file.getName(), properties); } public String getGrowId() { diff --git a/sechub-integrationtest/src/test/java/com/mercedesbenz/sechub/integrationtest/scenario2/JobScenario2IntTest.java b/sechub-integrationtest/src/test/java/com/mercedesbenz/sechub/integrationtest/scenario2/JobScenario2IntTest.java index 936ec2cc9a..45fc0530ad 100644 --- a/sechub-integrationtest/src/test/java/com/mercedesbenz/sechub/integrationtest/scenario2/JobScenario2IntTest.java +++ b/sechub-integrationtest/src/test/java/com/mercedesbenz/sechub/integrationtest/scenario2/JobScenario2IntTest.java @@ -3,21 +3,30 @@ import static com.mercedesbenz.sechub.integrationtest.api.AssertMail.*; import static com.mercedesbenz.sechub.integrationtest.api.TestAPI.*; +import static com.mercedesbenz.sechub.integrationtest.api.TestAPI.as; import static com.mercedesbenz.sechub.integrationtest.scenario2.Scenario2.*; +import static org.assertj.core.api.Assertions.*; import java.util.UUID; import org.junit.Rule; import org.junit.Test; import org.junit.rules.Timeout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.mercedesbenz.sechub.integrationtest.api.AssertJobScheduler.TestExecutionResult; import com.mercedesbenz.sechub.integrationtest.api.AssertJobScheduler.TestExecutionState; import com.mercedesbenz.sechub.integrationtest.api.IntegrationTestMockMode; import com.mercedesbenz.sechub.integrationtest.api.IntegrationTestSetup; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherAlgorithm; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherPasswordSourceType; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionData; public class JobScenario2IntTest { + private static final Logger LOG = LoggerFactory.getLogger(JobScenario2IntTest.class); + @Rule public IntegrationTestSetup setup = IntegrationTestSetup.forScenario(Scenario2.class); @@ -95,14 +104,82 @@ public void a_triggered_job_is_found_in_running_jobs_list_by_admin__when_not_alr } @Test - public void a_triggered_job_is_NOT_found_in_running_jobs_list_by_admin__when_already_done() { + public void job_list_for_done_job__and_encryption_and_cleanup_are_working() { + /* step 1 : job list entries */ + UUID doneJobUUID = assertAlreadyDoneJobIsNotListedInAdminJobList(); + int scheduleDataSizeBeforeRotate = assertEncryptionStatus().domain("schedule").hasData().getDataSize(); + + /* step 2 : rotate encryption for job */ + triggerEncryptionRotationAndAssertEncryptionIsDone(doneJobUUID); + + /* step3: check status must have now one more data */ + int scheduleDataSizeAfterRotate = assertEncryptionStatus()./* dump(). */domain("schedule").hasData().getDataSize(); + assertThat(scheduleDataSizeAfterRotate).isEqualTo(scheduleDataSizeBeforeRotate + 1); // must be one more... + + /* + * step4: rotate encryption for job again - means we ensure former job uses no + * longer the older cipher pool data and next cleanup will at least remove this + * one + */ + triggerEncryptionRotationAndAssertEncryptionIsDone(doneJobUUID); + + int scheduleDataSizeAfterRotate2 = assertEncryptionStatus()/* .dump() */.domain("schedule").hasData().getDataSize(); + assertThat(scheduleDataSizeAfterRotate2).isEqualTo(scheduleDataSizeBeforeRotate + 2); // must be one more... + + /* + * now cleanup cipher pool data (we do not want to wait for auto cleanup... + * takes too long time + */ + startScheduleCipherPoolDataCleanup(); + + /* + * wait until auto cleanup is done and encryption pool is cleaned + */ + executeRunnableAndAcceptAssertionsMaximumTimes(20, () -> { + int scheduleDataSize3 = assertEncryptionStatus()./* dump(). */domain("schedule").hasData().getDataSize(); + LOG.info("Fetched schedule encryption pool size(3): {}", scheduleDataSize3); + assertThat(scheduleDataSize3).isLessThan(scheduleDataSizeAfterRotate2); // must be less + + }, 500); + } + + private void triggerEncryptionRotationAndAssertEncryptionIsDone(UUID doneJobUUID) { + /* @formatter:off */ + /* prepare 3 */ + Long formerEncryptionPoolid = fetchScheduleEncryptionPoolIdForJob(doneJobUUID); + LOG.info("Job: {} had encryption pool id: {}", doneJobUUID, formerEncryptionPoolid); + + SecHubEncryptionData data = new SecHubEncryptionData(); + data.setAlgorithm(SecHubCipherAlgorithm.AES_GCM_SIV_256); + data.setPasswordSourceType(SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE); + data.setPasswordSourceData("INTEGRATION_TEST_SECRET_1_AES_256"); // see IntegrationTestEncryptionEnvironmentEntryProvider + + /* execution 3 - change encryption */ + as(SUPER_ADMIN).rotateEncryption(data); + + /* test 3 */ + executeRunnableAndAcceptAssertionsMaximumTimes(10, ()->{ + + Long newEncryptionPoolid = fetchScheduleEncryptionPoolIdForJob(doneJobUUID); + assertThat(newEncryptionPoolid).isNotEqualTo(formerEncryptionPoolid); + LOG.info("Job: {} has now encryption pool id: {}", doneJobUUID, newEncryptionPoolid); + + }, 500); + /* @formatter:on */ + } + + private UUID assertAlreadyDoneJobIsNotListedInAdminJobList() { + /* @formatter:off */ + /* prepare */ as(SUPER_ADMIN).assignUserToProject(USER_1, PROJECT_1); /* @formatter:off */ + /* execute 1 - start job */ UUID jobUUID = assertUser(USER_1). canCreateWebScan(PROJECT_1,IntegrationTestMockMode.WEBSCAN__NETSPARKER_GREEN__ZERO_WAIT); + /* test 1 - start job and wait job to be done. After this not in running jobs any more*/ assertUser(USER_1). onJobScheduling(PROJECT_1). canFindJob(jobUUID). @@ -121,8 +198,8 @@ public void a_triggered_job_is_NOT_found_in_running_jobs_list_by_admin__when_alr and(). onJobAdministration(). canNotFindRunningJob(jobUUID); // means events are triggered and handled */ - /* @formatter:on */ - + return jobUUID; + /* @formatter:on */ } } diff --git a/sechub-openapi-java/src/main/resources/openapi.yaml b/sechub-openapi-java/src/main/resources/openapi.yaml index 446e5cad94..79c63fc5d8 100644 --- a/sechub-openapi-java/src/main/resources/openapi.yaml +++ b/sechub-openapi-java/src/main/resources/openapi.yaml @@ -30,6 +30,8 @@ tags: description: Operations relevant to testing - name: Configuration description: Operations relevant to configuration parts + - name: Encryption + description: Operations relevant to encryption - name: Other description: All other use cases @@ -1641,7 +1643,97 @@ components: properties: cleanupTime: $ref: '#/components/schemas/CleanupTime' - + + ################ + ## Encryption ## + ################ + SecHubCipherPasswordSourceType: + title: SecHubCipherPasswordSourceType + type: string + enum: + - NONE + - ENVIRONMENT_VARIABLE + + SecHubPasswordSource: + title: SecHubPasswordSource + type: object + properties: + type: + type: object + $ref: '#/components/schemas/SecHubCipherPasswordSourceType' + data: + type: string + + SecHubCipherAlgorithm: + title: SecHubCipherAlgorithm + type: string + enum: + - NONE + - AES_GCM_SIV_128 + - AES_GCM_SIV_256 + + SecHubDomainEncryptionData: + title: SecHubDomainEncryptionData + type: object + properties: + id: + description: Identifer for encryption configuration inside the domain + type: string + algorithm: + type: object + $ref: '#/components/schemas/SecHubCipherAlgorithm' + passwordSource: + type: object + $ref: '#/components/schemas/SecHubPasswordSource' + created: + description: Creation timestamp + type: string + format: date-time + createdFrom: + description: User id of admin who has created the configuration + type: string + usage: + description: Generic information about encryption usages inside domain + type: object + additionalProperties: true + + SecHubDomainEncryptionStatus: + title: SecHubDomainEncryptionStatus + type: object + properties: + name: + type: string + data: + type: array + items: + $ref: '#/components/schemas/SecHubDomainEncryptionData' + + SecHubEncryptionStatus: + title: SecHubEncryptionStatus + type: object + properties: + type: + type: string + domains: + type: array + items: + $ref: '#/components/schemas/SecHubDomainEncryptionStatus' + + SecHubEncryptionData: + title: SecHubEncryptionData + type: object + properties: + algorithm: + description: Algorithm to use for encryption rotation + type: object + $ref: '#/components/schemas/SecHubCipherAlgorithm' + passwordSourceType: + description: Password source type to use for algorithm + type: object + $ref: '#/components/schemas/SecHubCipherPasswordSourceType' + passwordSourceData: + description: Password source data for used type. E.g. for ENVIRONMENT_VARIABLE the name of the variable + type: string ########### ## Other ## ########### @@ -3250,6 +3342,40 @@ paths: tags: - Configuration + ################ + ## Encryption ## + ################ + /api/admin/encryption/status: + get: + summary: Admin fetches encryption status + description: "An administrator fetches encryption status from all domains where encryption is used." + operationId: adminFetchesEncryptionStatus + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/SecHubEncryptionStatus' + tags: + - Encryption + + /api/admin/encryption/rotate: + post: + summary: Admin starts encryption rotation + description: "An administrator starts encryption rotation." + operationId: adminStartsEncryptionRotation + requestBody: + content: + application/json;charset=UTF-8: + schema: + $ref: '#/components/schemas/SecHubEncryptionData' + responses: + "200": + description: "Ok" + x-accepts: application/json + tags: + - Encryption + ########### ## Other ## ########### diff --git a/sechub-pds-core/build.gradle b/sechub-pds-core/build.gradle index f42e39307e..0f91b842ea 100644 --- a/sechub-pds-core/build.gradle +++ b/sechub-pds-core/build.gradle @@ -19,6 +19,8 @@ dependencies { api project(':sechub-commons-core') api project(':sechub-commons-model') api project(':sechub-commons-archive') + api project(':sechub-commons-encryption') + api project(':sechub-storage-core') api project(':sechub-pds-commons-core') diff --git a/sechub-pds-core/src/main/java/com/mercedesbenz/sechub/pds/usecase/PDSDocumentationScopeConstants.java b/sechub-pds-core/src/main/java/com/mercedesbenz/sechub/pds/usecase/PDSDocumentationScopeConstants.java new file mode 100644 index 0000000000..00feb7f538 --- /dev/null +++ b/sechub-pds-core/src/main/java/com/mercedesbenz/sechub/pds/usecase/PDSDocumentationScopeConstants.java @@ -0,0 +1,6 @@ +package com.mercedesbenz.sechub.pds.usecase; + +public class PDSDocumentationScopeConstants { + + public static final String SCOPE_ENCRYPTION = "Encryption"; +} diff --git a/sechub-pds/build.gradle b/sechub-pds/build.gradle index 71f9ceeded..db6a44ab48 100644 --- a/sechub-pds/build.gradle +++ b/sechub-pds/build.gradle @@ -22,9 +22,6 @@ dependencies { implementation(library.apache_commons_io) implementation(library.apache_commons_fileupload2_core) implementation(library.apache_commons_fileupload2_jakarta) - - - api project(':sechub-pds-core') diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/PDSEncryptionConfiguration.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/PDSPasswordEncoderConfiguration.java similarity index 91% rename from sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/PDSEncryptionConfiguration.java rename to sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/PDSPasswordEncoderConfiguration.java index 2368c16751..c0021837d3 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/PDSEncryptionConfiguration.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/PDSPasswordEncoderConfiguration.java @@ -7,7 +7,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; @Configuration -public class PDSEncryptionConfiguration { +public class PDSPasswordEncoderConfiguration { @Bean public PasswordEncoder passwordEncoder() { diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/PDSPojoFactory.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/PDSPojoFactory.java index 626062db8d..c72a5cff05 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/PDSPojoFactory.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/PDSPojoFactory.java @@ -10,6 +10,8 @@ import com.mercedesbenz.sechub.commons.core.environment.SystemEnvironment; import com.mercedesbenz.sechub.commons.core.environment.SystemEnvironmentVariableSupport; import com.mercedesbenz.sechub.commons.core.security.CheckSumSupport; +import com.mercedesbenz.sechub.commons.encryption.EncryptionSupport; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherFactory; import com.mercedesbenz.sechub.commons.model.CodeScanPathCollector; import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModelSupport; import com.mercedesbenz.sechub.commons.model.SecHubDataConfigurationTypeListParser; @@ -26,6 +28,16 @@ @Component public class PDSPojoFactory { + @Bean + EncryptionSupport createEncryptionSupport() { + return new EncryptionSupport(); + } + + @Bean + PersistentCipherFactory createPersistentCipherFactory() { + return new PersistentCipherFactory(); + } + @Bean SecHubDataConfigurationTypeListParser createTypeListParser() { return new SecHubDataConfigurationTypeListParser(); diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/PDSStartupAssertEnvironmentVariablesUsed.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/PDSStartupAssertEnvironmentVariablesUsed.java index 863164a360..59ad6f1222 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/PDSStartupAssertEnvironmentVariablesUsed.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/PDSStartupAssertEnvironmentVariablesUsed.java @@ -13,6 +13,8 @@ import com.mercedesbenz.sechub.commons.core.environment.SecureEnvironmentVariableKeyValueRegistry; import com.mercedesbenz.sechub.commons.core.environment.SecureEnvironmentVariableKeyValueRegistry.EnvironmentVariableKeyValueEntry; import com.mercedesbenz.sechub.commons.core.environment.SystemEnvironmentVariableSupport; +import com.mercedesbenz.sechub.pds.encryption.PDSCipherAlgorithm; +import com.mercedesbenz.sechub.pds.encryption.PDSEncryptionConfiguration; import com.mercedesbenz.sechub.pds.security.PDSSecurityConfiguration; import com.mercedesbenz.sechub.pds.storage.PDSS3PropertiesSetup; import com.mercedesbenz.sechub.pds.storage.PDSSharedVolumePropertiesSetup; @@ -38,6 +40,9 @@ public class PDSStartupAssertEnvironmentVariablesUsed { @Autowired PDSSecurityConfiguration securityConfiguration; + @Autowired + PDSEncryptionConfiguration encryptionConfiguration; + @Autowired Environment environment; @@ -90,10 +95,14 @@ public SecureEnvironmentVariableKeyValueRegistry createRegistryForOnlyAllowedAsE if (securityConfiguration == null) { securityConfiguration = PDSSecurityConfiguration.create("test-user", "test-user-token", "test-admin", "test-admintoken"); } + if (encryptionConfiguration == null) { + encryptionConfiguration = PDSEncryptionConfiguration.create(PDSCipherAlgorithm.NONE, null); + } } s3Setup.registerOnlyAllowedAsEnvironmentVariables(sensitiveDataRegistry); sharedVolumeSetup.registerOnlyAllowedAsEnvironmentVariables(sensitiveDataRegistry); securityConfiguration.registerOnlyAllowedAsEnvironmentVariables(sensitiveDataRegistry); + encryptionConfiguration.registerOnlyAllowedAsEnvironmentVariables(sensitiveDataRegistry); // some additional parts which shall only be available as environment variables // - h2 databases allow no setup here, so not mandatory diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/autocleanup/PDSAutoCleanupDaysCalculator.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/autocleanup/PDSAutoCleanupDaysCalculator.java index fd45ea425b..51c8d6a740 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/autocleanup/PDSAutoCleanupDaysCalculator.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/autocleanup/PDSAutoCleanupDaysCalculator.java @@ -15,7 +15,7 @@ public class PDSAutoCleanupDaysCalculator { /** * Calculates cleanup time in days * - * @param config + * @param configuration * @return cleanup time in days */ public long calculateCleanupTimeInDays(PDSAutoCleanupConfig config) { diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/config/PDSConfigService.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/config/PDSConfigService.java index fa33d7e72a..178d495cb8 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/config/PDSConfigService.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/config/PDSConfigService.java @@ -38,7 +38,7 @@ public class PDSConfigService { PDSAutoCleanupDaysCalculator calculator; @UseCaseAdminUpdatesAutoCleanupConfiguration(@PDSStep(number = 2, next = { 3, 4, - 5 }, name = "Updates auto cleanup config", description = "Updates auto cleanup configuration as JSON in database")) + 5 }, name = "Updates auto cleanup configuration", description = "Updates auto cleanup configuration as JSON in database")) public void updateAutoCleanupConfiguration(PDSAutoCleanupConfig configuration) { Assert.notNull(configuration, "configuration may not be null"); @@ -72,7 +72,7 @@ public void updateAutoCleanupInDays(long autoCleanupInDays) { transactionService.saveConfigInOwnTransaction(config); } - @UseCaseAdminFetchesAutoCleanupConfiguration(@PDSStep(number = 2, name = "Fetches auto cleanup config", description = "Fetches auto cleanup configuration from database")) + @UseCaseAdminFetchesAutoCleanupConfiguration(@PDSStep(number = 2, name = "Fetches auto cleanup configuration", description = "Fetches auto cleanup configuration from database")) public PDSAutoCleanupConfig fetchAutoCleanupConfiguration() { String cleanupConfigJson = getOrCreateConfig().autoCleanupConfiguration; PDSAutoCleanupConfig cleanupConfig = null; diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/config/PDSServerConfigurationService.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/config/PDSServerConfigurationService.java index 9ae837d6ee..f38bdaad38 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/config/PDSServerConfigurationService.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/config/PDSServerConfigurationService.java @@ -32,7 +32,7 @@ public class PDSServerConfigurationService { private static final Logger LOG = LoggerFactory.getLogger(PDSServerConfigurationService.class); - private static final String DEFAULT_PATH = "./pds-config.json"; + private static final String DEFAULT_PATH = "./pds-configuration.json"; private static final int defaultMinutesToWaitForProduct = PDSDefaultParameterValueConstants.DEFAULT_MINUTES_TO_WAIT_FOR_PRODUCT; private static final int defaultMaxConfigurableMinutesToWaitForProduct = PDSDefaultParameterValueConstants.MAXIMUM_CONFIGURABLE_TIME_TO_WAIT_FOR_PRODUCT_IN_MINUTES; @@ -86,10 +86,10 @@ protected void postConstruct() { } } catch (PDSJSONConverterException | IOException e) { - LOG.error("no configuration available, because cannot read config file", e); + LOG.error("no configuration available, because cannot read configuration file", e); } } else { - LOG.error("No config file found at {} !", file.getAbsolutePath()); + LOG.error("No configuration file found at {} !", file.getAbsolutePath()); } if (configuration == null) { LOG.error( diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/config/PDSSummaryLogService.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/config/PDSSummaryLogService.java index 4dce6b864e..676124600e 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/config/PDSSummaryLogService.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/config/PDSSummaryLogService.java @@ -17,6 +17,7 @@ import com.mercedesbenz.sechub.pds.commons.core.config.PDSProductParameterSetup; import com.mercedesbenz.sechub.pds.commons.core.config.PDSProductSetup; import com.mercedesbenz.sechub.pds.commons.core.config.PDSServerConfiguration; +import com.mercedesbenz.sechub.pds.encryption.PDSEncryptionConfiguration; @Service public class PDSSummaryLogService { @@ -26,18 +27,22 @@ public class PDSSummaryLogService { @Autowired PDSServerConfigurationService configurationService; + @Autowired + PDSEncryptionConfiguration encryptionConfigurationService; + @EventListener(ApplicationReadyEvent.class) void applicationReady() { PDSServerConfiguration configuration = configurationService.getServerConfiguration(); StringBuilder summary = new StringBuilder(); summary.append("PDS has been started successfully.\n**************************\n Summary\n**************************"); - summary.append("\n- config file used: ").append(configurationService.pathToConfigFile); + summary.append("\n- configuration file used: ").append(configurationService.pathToConfigFile); summary.append("\n- server id: ").append(configuration.getServerId()); summary.append("\n- system wide minutes to wait for product: ").append(configurationService.getMinutesToWaitForProduct()); summary.append("\n- minimum configurable minutes to wait for product: ").append(configurationService.getMinimumConfigurableMinutesToWaitForProduct()); summary.append("\n- maximum configurable minutes to wait for product: ").append(configurationService.getMaximumConfigurableMinutesToWaitForProduct()); + summary.append("\n- encryption algorithm used: ").append(encryptionConfigurationService.getAlgorithm()); List products = configuration.getProducts(); summary.append("\n- Available products: ").append(products.size()); diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/encryption/PDSCipherAlgorithm.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/encryption/PDSCipherAlgorithm.java new file mode 100644 index 0000000000..6a913813e9 --- /dev/null +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/encryption/PDSCipherAlgorithm.java @@ -0,0 +1,23 @@ +package com.mercedesbenz.sechub.pds.encryption; + +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherType; + +public enum PDSCipherAlgorithm { + + NONE(PersistentCipherType.NONE), + + AES_GCM_SIV_128(PersistentCipherType.AES_GCM_SIV_128), + + AES_GCM_SIV_256(PersistentCipherType.AES_GCM_SIV_256),; + + private PersistentCipherType type; + + private PDSCipherAlgorithm(PersistentCipherType type) { + this.type = type; + } + + public PersistentCipherType getType() { + return type; + } + +} diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/encryption/PDSEncryptionConfiguration.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/encryption/PDSEncryptionConfiguration.java new file mode 100644 index 0000000000..61a221302f --- /dev/null +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/encryption/PDSEncryptionConfiguration.java @@ -0,0 +1,111 @@ +package com.mercedesbenz.sechub.pds.encryption; + +import static com.mercedesbenz.sechub.pds.usecase.PDSDocumentationScopeConstants.*; + +import java.util.List; + +import javax.crypto.SealedObject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import com.mercedesbenz.sechub.commons.core.environment.SecureEnvironmentVariableKeyValueRegistry; +import com.mercedesbenz.sechub.commons.core.security.CryptoAccess; +import com.mercedesbenz.sechub.pds.PDSMustBeDocumented; +import com.mercedesbenz.sechub.pds.commons.core.PDSProfiles; + +import jakarta.annotation.PostConstruct; + +@Component +public class PDSEncryptionConfiguration { + + private static final Logger LOG = LoggerFactory.getLogger(PDSEncryptionConfiguration.class); + + private static final String KEY_ROOT_PATH = "pds.encryption."; + private static final String KEY_SECRET_KEY = KEY_ROOT_PATH + "secret-key"; + private static final String KEY_ALGORITHM = KEY_ROOT_PATH + "algorithm"; + private static final String ENV_ALGORITHM = KEY_ALGORITHM.toUpperCase().replace('.', '_'); + + @PDSMustBeDocumented(value = "The secret key used for encryption. It must be base64 encoded, otherwise it is not accepted.", scope = SCOPE_ENCRYPTION, secret = true) + @Value("${" + KEY_SECRET_KEY + ":}") + String secretKeyAsString; + + @PDSMustBeDocumented(value = "The encryption type. Allowed values are: NONE, AES_GCM_SIV_128 or AES_GCM_SIV_256", scope = SCOPE_ENCRYPTION) + @Value("${" + KEY_ALGORITHM + ":NONE}") + String algorithmAsString; + + @Autowired + private Environment springEnvironment; + + private PDSCipherAlgorithm algorithm; + private SealedObject sealedSecretKey; + + @PostConstruct + void init() throws PDSEncryptionException { + boolean secretKeyWasEmpty = secretKeyAsString == null || secretKeyAsString.isBlank(); + sealedSecretKey = CryptoAccess.CRYPTO_STRING.seal(secretKeyAsString); + secretKeyAsString = null; // reset + + try { + algorithm = PDSCipherAlgorithm.valueOf(algorithmAsString.toUpperCase()); + } catch (RuntimeException e) { + throw new PDSEncryptionException("Algorithm '" + algorithmAsString + "' not supported. Please set " + ENV_ALGORITHM + + " to one of the following values: " + List.of(PDSCipherAlgorithm.values()), e); + } + handleSecretKeyEmptyOrNot(secretKeyWasEmpty); + } + + private void handleSecretKeyEmptyOrNot(boolean secretKeyWasEmpty) throws PDSEncryptionException { + switch (algorithm) { + case NONE: + if (!secretKeyWasEmpty) { + LOG.warn("You used a non empty secret key for cipher algorithm {}. Did you forget to change the algorithm type?", algorithm); + } + break; + default: + if (secretKeyWasEmpty) { + throw new PDSEncryptionException("The cipher algorithm " + algorithm + " does not allow an empty secret key!"); + } + } + } + + public byte[] getSecretKeyBytes() { + return CryptoAccess.CRYPTO_STRING.unseal(sealedSecretKey).getBytes(); + } + + public PDSCipherAlgorithm getAlgorithm() { + return algorithm; + } + + public void registerOnlyAllowedAsEnvironmentVariables(SecureEnvironmentVariableKeyValueRegistry registry) { + if (springEnvironment != null && springEnvironment.matchesProfiles(PDSProfiles.INTEGRATIONTEST)) { + /* + * on integration test we accept credentials from configuration file or as + * system properties - not marked as sensitive + */ + return; + } + registry.register(registry.newEntry().key(KEY_ALGORITHM).notNullValue(algorithmAsString)); + registry.register(registry.newEntry().key(KEY_SECRET_KEY).nullableValue(CryptoAccess.CRYPTO_STRING.unseal(sealedSecretKey))); + + } + + /** + * Creates a encryption configuration which can be used by AsciiDoc generator + * + * @param algorithm algorithm to use for encryption + * @param secretKeyAsString secret key as plain string + * @return configuration, never null + */ + public static PDSEncryptionConfiguration create(PDSCipherAlgorithm algorithm, String secretKeyAsString) { + PDSEncryptionConfiguration config = new PDSEncryptionConfiguration(); + config.algorithmAsString = algorithm.name(); + config.secretKeyAsString = secretKeyAsString; + return config; + } + +} diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/encryption/PDSEncryptionException.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/encryption/PDSEncryptionException.java new file mode 100644 index 0000000000..597a2c05d5 --- /dev/null +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/encryption/PDSEncryptionException.java @@ -0,0 +1,14 @@ +package com.mercedesbenz.sechub.pds.encryption; + +public class PDSEncryptionException extends Exception { + + private static final long serialVersionUID = 1L; + + public PDSEncryptionException(String message) { + super(message); + } + + public PDSEncryptionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/encryption/PDSEncryptionService.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/encryption/PDSEncryptionService.java new file mode 100644 index 0000000000..a3b711433e --- /dev/null +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/encryption/PDSEncryptionService.java @@ -0,0 +1,60 @@ +package com.mercedesbenz.sechub.pds.encryption; + +import java.util.Base64; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.mercedesbenz.sechub.commons.encryption.DefaultSecretKeyProvider; +import com.mercedesbenz.sechub.commons.encryption.EncryptionResult; +import com.mercedesbenz.sechub.commons.encryption.EncryptionSupport; +import com.mercedesbenz.sechub.commons.encryption.InitializationVector; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipher; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherFactory; +import com.mercedesbenz.sechub.commons.encryption.SecretKeyProvider; + +import jakarta.annotation.PostConstruct; + +@Service +public class PDSEncryptionService { + + @Autowired + EncryptionSupport encryptionSupport; + + @Autowired + PersistentCipherFactory cipherFactory; + + @Autowired + PDSEncryptionConfiguration configuration; + + PersistentCipher cipher; + + @PostConstruct + void init() { + + PDSCipherAlgorithm algorithm = configuration.getAlgorithm(); + if (algorithm == null) { + throw new IllegalStateException("No cipher algorithm defined!"); + } + SecretKeyProvider secretKeyProvider = null; + switch (algorithm) { + case NONE: + break; + default: + byte[] base64decoded = Base64.getDecoder().decode(configuration.getSecretKeyBytes()); + secretKeyProvider = new DefaultSecretKeyProvider(base64decoded, algorithm.getType()); + break; + + } + cipher = cipherFactory.createCipher(secretKeyProvider, algorithm.getType()); + } + + public String decryptString(byte[] encryptedData, InitializationVector initialVector) { + return encryptionSupport.decryptString(encryptedData, cipher, initialVector); + } + + public EncryptionResult encryptString(String plainText) { + return encryptionSupport.encryptString(plainText, cipher); + } + +} diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSApplyFutureExecutionResultToJobService.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSApplyFutureExecutionResultToJobService.java new file mode 100644 index 0000000000..b2e9858aad --- /dev/null +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSApplyFutureExecutionResultToJobService.java @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.pds.execution; + +import java.time.LocalDateTime; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.mercedesbenz.sechub.commons.pds.data.PDSJobStatusState; +import com.mercedesbenz.sechub.pds.job.PDSJob; +import com.mercedesbenz.sechub.pds.job.PDSJobRepository; + +@Service +public class PDSApplyFutureExecutionResultToJobService { + + private static final Logger LOG = LoggerFactory.getLogger(PDSApplyFutureExecutionResultToJobService.class); + + @Autowired + PDSJobRepository repository; + + /** + * Applies given future of an execution result to the given PDS job. + * + * @param future future result + * @param job pds + */ + public void applyResultToJob(Future future, PDSJob job) { + // we use this moment of time for all, currently the easiest and central way + job.setEnded(LocalDateTime.now()); + + UUID jobUUID = job.getUUID(); + if (future.isCancelled()) { + job.setState(PDSJobStatusState.CANCELED); + } else { + PDSExecutionResult callResult; + try { + callResult = future.get(); + LOG.debug("Fetch job result from future, pds job uuid={}, state={}", jobUUID, job.getState()); + job.setResult(callResult.getResult()); + + if (callResult.isCanceled()) { + job.setState(PDSJobStatusState.CANCELED); + } else if (callResult.isFailed()) { + job.setState(PDSJobStatusState.FAILED); + if (callResult.isEncryptionFailure()) { + job.setEncryptionOutOfSync(true); + } + } else { + job.setState(PDSJobStatusState.DONE); + } + + } catch (InterruptedException e) { + LOG.error("Job with uuid:{} was interrupted", jobUUID, e); + + job.setState(PDSJobStatusState.FAILED); + job.setResult("Job interrupted"); + } catch (ExecutionException e) { + LOG.error("Job with uuid:{} failed in execution", jobUUID, e); + + job.setState(PDSJobStatusState.FAILED); + job.setResult("Job execution failed"); + } + LOG.debug("Handled job result and state job uuid={}, state={}", jobUUID, job.getState()); + } + repository.save(job); + LOG.debug("Stored job pds uuid={}, state={}", jobUUID, job.getState()); + } + +} diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionCallable.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionCallable.java index 738fb7633e..6dfb1085f2 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionCallable.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionCallable.java @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.pds.execution; +import static com.mercedesbenz.sechub.commons.pds.PDSDefaultParameterKeyConstants.*; import static com.mercedesbenz.sechub.pds.util.PDSAssert.*; import java.io.File; @@ -25,7 +26,7 @@ import com.mercedesbenz.sechub.commons.pds.PDSDefaultParameterKeyConstants; import com.mercedesbenz.sechub.commons.pds.ProcessAdapter; import com.mercedesbenz.sechub.pds.PDSLogConstants; -import com.mercedesbenz.sechub.pds.commons.core.PDSJSONConverterException; +import com.mercedesbenz.sechub.pds.encryption.PDSEncryptionException; import com.mercedesbenz.sechub.pds.job.JobConfigurationData; import com.mercedesbenz.sechub.pds.job.PDSCheckJobStatusService; import com.mercedesbenz.sechub.pds.job.PDSGetJobStreamService; @@ -101,9 +102,8 @@ public PDSExecutionResult call() throws Exception { MDC.put(PDSLogConstants.MDC_PDS_JOB_UUID, Objects.toString(pdsJobUUID)); getJobTransactionService().markJobAsRunningInOwnTransaction(pdsJobUUID); - - JobConfigurationData data = getJobTransactionService().getJobConfigurationData(pdsJobUUID); - config = PDSJobConfiguration.fromJSON(data.getJobConfigurationJson()); + JobConfigurationData data = getJobTransactionService().getJobConfigurationDataOrFail(pdsJobUUID); + config = data.getJobConfiguration(); MDC.put(PDSLogConstants.MDC_SECHUB_JOB_UUID, Objects.toString(config.getSechubJobUUID())); @@ -127,15 +127,18 @@ public PDSExecutionResult call() throws Exception { LOG.info("Workspace not prepared enough for launcher script, so skipping execution of product: {} for pds job: {}", config.getProductId(), pdsJobUUID); - result.exitCode = 0; + result.setExitCode(0); } } catch (Exception e) { LOG.error("Execution of job uuid:{} failed", pdsJobUUID, e); - result.failed = true; - result.result = "Execution of job uuid:" + pdsJobUUID + " failed. Please look into PDS logs for details and search for former string."; + result.setFailed(true); + if (e instanceof PDSEncryptionException) { + result.setEncryptionFailure(true); + } + result.setResult("Execution of job uuid:" + pdsJobUUID + " failed. Please look into PDS logs for details and search for former string."); } finally { cleanUpWorkspace(pdsJobUUID, config); @@ -147,15 +150,15 @@ public PDSExecutionResult call() throws Exception { * handle always exit code. Everything having an exit code != 0 is handled as an * error */ - if (result.exitCode != 0) { - result.failed = true; + if (result.getExitCode() != 0) { + result.setFailed(true); } - result.canceled = cancelOperationsHasBeenStarted; + result.setCanceled(cancelOperationsHasBeenStarted); - LOG.info("Finished execution of job {} with exitCode={}, failed={}, cancelOperationsHasBeenStarted={}", pdsJobUUID, result.exitCode, result.failed, - cancelOperationsHasBeenStarted); + LOG.info("Finished execution of job {} with exitCode={}, failed={}, cancelOperationsHasBeenStarted={}", pdsJobUUID, result.getExitCode(), + result.isFailed(), cancelOperationsHasBeenStarted); - if (result.failed) { + if (result.isFailed()) { PDSGetJobStreamService pdsGetJobStreamService = serviceCollection.getPdsGetJobStreamService(); String truncatedErrorStream = pdsGetJobStreamService.getJobErrorStreamTruncated(pdsJobUUID); String truncatedOutputStream = pdsGetJobStreamService.getJobOutputStreamTruncated(pdsJobUUID); @@ -175,7 +178,7 @@ Job output stream (last %s chars): ------------------------------------ %s - """.formatted(pdsJobUUID, productPath, result.exitCode, lastChars, truncatedErrorStream, lastChars, truncatedOutputStream); + """.formatted(pdsJobUUID, productPath, result.getExitCode(), lastChars, truncatedErrorStream, lastChars, truncatedOutputStream); LOG.error(message); } @@ -228,10 +231,10 @@ void waitForProcessEndAndGetResultByFiles(PDSExecutionResult result, UUID jobUUI if (exitDoneInTime) { LOG.debug("Job execution {} done - product id:{}.", jobUUID, config.getProductId()); - result.failed = false; - result.exitCode = process.exitValue(); + result.setFailed(false); + result.setExitCode(process.exitValue()); - LOG.debug("Process of PDS job with uuid: {} ended in time with exit code: {} after {} ms - for product with id: {}", jobUUID, result.exitCode, + LOG.debug("Process of PDS job with uuid: {} ended in time with exit code: {} after {} ms - for product with id: {}", jobUUID, result.getExitCode(), timeElapsedInMilliseconds, config.getProductId()); storeResultFileOrCreateShrinkedProblemDataInstead(result, jobUUID); @@ -240,9 +243,9 @@ void waitForProcessEndAndGetResultByFiles(PDSExecutionResult result, UUID jobUUI LOG.error("Process did not end in time for PDS job with uuid: {} for product id: {}. Waited {} minutes.", jobUUID, config.getProductId(), minutesToWaitForResult); - result.failed = true; - result.result = "Product time out."; - result.exitCode = 1; + result.setFailed(true); + result.setResult("Product time out."); + result.setExitCode(1); prepareForCancel(true); } @@ -263,12 +266,12 @@ private void storeResultFileOrCreateShrinkedProblemDataInstead(PDSExecutionResul if (file.exists()) { LOG.debug("Result file found - will read data and set as result"); - result.result = FileUtils.readFileToString(file, encoding); + result.setResult(FileUtils.readFileToString(file, encoding)); } else { LOG.debug("Result file NOT found - will append output and error streams as result"); - result.failed = true; - result.result = "Result file not found at " + file.getAbsolutePath(); + result.setFailed(true); + result.setResult("Result file not found at " + file.getAbsolutePath()); int max = MAXIMUM_START_TRUNCATE_CHARS; @@ -299,7 +302,7 @@ private String appendErrorStreamToResultAndReturnShrinkedVariant(PDSExecutionRes File systemErrorFile = getWorkspaceService().getSystemErrorFile(jobUUID); if (systemErrorFile.exists()) { String error = FileUtils.readFileToString(systemErrorFile, encoding); - result.result += "\nErrors:\n" + error; + result.setResult(result.getResult() + "\nErrors:\n" + error); shrinkedErrorStream = shrinkTo(error, max); } return shrinkedErrorStream; @@ -311,7 +314,7 @@ private String appendOutputStreamToResultAndReturnShrinkedVariant(PDSExecutionRe if (systemOutFile.exists()) { String output = FileUtils.readFileToString(systemOutFile, encoding); - result.result += "\nOutput:\n" + output; + result.setResult(result.getResult() + "\nOutput:\n" + output); shrinkedOutputStream = shrinkTo(output, max); } return shrinkedOutputStream; @@ -512,15 +515,21 @@ boolean prepareForCancel(boolean mayInterruptIfRunning) { } catch (RuntimeException e) { return false; } finally { - - JobConfigurationData data = getJobTransactionService().getJobConfigurationData(pdsJobUUID); + PDSJobConfiguration jobConfiguration = null; try { - PDSJobConfiguration config = PDSJobConfiguration.fromJSON(data.getJobConfigurationJson()); - cleanUpWorkspace(pdsJobUUID, config); - } catch (PDSJSONConverterException e) { - LOG.error("Was not able fetch job config for {} - workspace clean only workspace files", pdsJobUUID, e); + JobConfigurationData data = getJobTransactionService().getJobConfigurationDataOrFail(pdsJobUUID); + jobConfiguration = data.getJobConfiguration(); + + } catch (PDSEncryptionException e) { + LOG.warn("Was not able to decrypt configuration of PDS job: {}", pdsJobUUID, e); + + LOG.info("Create fallback configuration, asuming sechub storage reuse is enabled (SecHub does storage cleanup)"); + jobConfiguration = new PDSJobConfiguration(); + PDSExecutionParameterEntry resueSecHubConfigParameter = new PDSExecutionParameterEntry(PARAM_KEY_PDS_CONFIG_USE_SECHUB_STORAGE, "true"); + jobConfiguration.getParameters().add(resueSecHubConfigParameter); } + cleanUpWorkspace(pdsJobUUID, jobConfiguration); } diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionEnvironmentService.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionEnvironmentService.java index 825f3c4064..27fdcaa7eb 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionEnvironmentService.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionEnvironmentService.java @@ -49,10 +49,10 @@ public void initProcessBuilderEnvironmentMap(UUID pdsJobUUID, PDSJobConfiguratio throw new IllegalArgumentException("pds job uuid may not be null!"); } if (config == null) { - throw new IllegalArgumentException("pds job config may not be null!"); + throw new IllegalArgumentException("pds job configuration may not be null!"); } if (builder == null) { - throw new IllegalArgumentException("pds job config may not be null!"); + throw new IllegalArgumentException("pds job configuration may not be null!"); } String productId = config.getProductId(); if (productId == null) { diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionResult.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionResult.java index efbbacd133..3d5aa29cff 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionResult.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionResult.java @@ -3,9 +3,51 @@ public class PDSExecutionResult { - int exitCode; - boolean failed; + private int exitCode; + private boolean failed; + + private String result; + private boolean canceled; + private boolean encryptionFailure; + + public int getExitCode() { + return exitCode; + } + + public void setExitCode(int exitCode) { + this.exitCode = exitCode; + } + + public boolean isFailed() { + return failed; + } + + public void setFailed(boolean failed) { + this.failed = failed; + } + + public String getResult() { + return result; + } + + public void setResult(String result) { + this.result = result; + } + + public boolean isCanceled() { + return canceled; + } + + public void setCanceled(boolean canceled) { + this.canceled = canceled; + } + + public boolean isEncryptionFailure() { + return encryptionFailure; + } + + public void setEncryptionFailure(boolean encryptionFailure) { + this.encryptionFailure = encryptionFailure; + } - String result; - boolean canceled; } diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionService.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionService.java index 432f11d797..c58b489075 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionService.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionService.java @@ -3,7 +3,6 @@ import static com.mercedesbenz.sechub.pds.util.PDSAssert.*; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; @@ -14,7 +13,6 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -94,6 +92,9 @@ public class PDSExecutionService { @Autowired PDSWorkspaceService workspaceService; + @Autowired + PDSApplyFutureExecutionResultToJobService applyService; + @PostConstruct protected void postConstruct() { workers = Executors.newFixedThreadPool(workerThreadCount); @@ -318,43 +319,10 @@ private boolean isFutureDoneAndChangesToDatabaseCanBeApplied(Entry mandatories = productSetup.getParameters().getMandatory(); for (PDSProductParameterDefinition mandatory : mandatories) { diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSJobRestController.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSJobRestController.java index b7462f25c8..fb797fbb01 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSJobRestController.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSJobRestController.java @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; +import com.mercedesbenz.sechub.commons.pds.data.PDSJobStatus; import com.mercedesbenz.sechub.pds.PDSAPIConstants; import com.mercedesbenz.sechub.pds.security.PDSRoleConstants; import com.mercedesbenz.sechub.pds.usecase.PDSStep; diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSJobStatus.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSJobStatus.java deleted file mode 100644 index cb338ab003..0000000000 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSJobStatus.java +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.pds.job; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.UUID; - -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.mercedesbenz.sechub.commons.pds.data.PDSJobStatusState; -import com.mercedesbenz.sechub.pds.commons.core.PDSJSONConverter; -import com.mercedesbenz.sechub.pds.commons.core.PDSJSONConverterException; - -/** - * This class represents the schedule job status which can be obtained by REST - * - * @author Albert Tregnaghi - * - */ -@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(Include.NON_NULL) -public class PDSJobStatus { - - public static final String PROPERTY_JOBUUID = "jobUUID"; - public static final String PROPERTY_OWNER = "owner"; - public static final String PROPERTY_CREATED = "created"; - public static final String PROPERTY_STARTED = "started"; - public static final String PROPERTY_ENDED = "ended"; - public static final String PROPERTY_STATE = "state"; - - UUID jobUUID; - - String owner; - - String created; - String started; - String ended; - - String state; - - PDSJobStatus() { - - } - - public PDSJobStatus(PDSJob secHubJob) { - this.jobUUID = secHubJob.getUUID(); - - this.owner = secHubJob.getOwner(); - - this.created = convertToString(secHubJob.getCreated()); - this.started = convertToString(secHubJob.getStarted()); - this.ended = convertToString(secHubJob.getEnded()); - - this.state = convertToString(secHubJob.getState()); - } - - private String convertToString(PDSJobStatusState result) { - if (result == null) { - return ""; - } - return result.name(); - } - - private String convertToString(LocalDateTime localDateTime) { - if (localDateTime == null) { - return ""; - } - return localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); - } - - public String toJSON() throws PDSJSONConverterException { - return PDSJSONConverter.get().toJSON(this); - } -} \ No newline at end of file diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSJobTransactionService.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSJobTransactionService.java index a98cf72970..5ed48fa1f7 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSJobTransactionService.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSJobTransactionService.java @@ -18,6 +18,7 @@ import com.mercedesbenz.sechub.commons.model.SecHubMessagesList; import com.mercedesbenz.sechub.commons.pds.data.PDSJobStatusState; +import com.mercedesbenz.sechub.pds.encryption.PDSEncryptionException; import com.mercedesbenz.sechub.pds.execution.PDSExecutionData; import com.mercedesbenz.sechub.pds.security.PDSRoleConstants; import com.mercedesbenz.sechub.pds.usecase.PDSStep; @@ -36,6 +37,9 @@ public class PDSJobTransactionService { @Autowired PDSJobRepository repository; + @Autowired + PDSJobConfigurationAccess access; + public PDSJobTransactionService() { } @@ -115,16 +119,14 @@ private void updateJobInOwnTransaction(UUID jobUUID, String result, LocalDateTim * * @param jobUUID * @return job configuration, will fail when job is not found + * @throws PDSEncryptionException when it was not possible to access the + * encrypted configuration data */ - public JobConfigurationData getJobConfigurationData(UUID jobUUID) { + public JobConfigurationData getJobConfigurationDataOrFail(UUID jobUUID) throws PDSEncryptionException { PDSJob job = assertJobFound(jobUUID, repository); - JobConfigurationData data = new JobConfigurationData(); - data.jobConfigurationJson = job.getJsonConfiguration(); - data.metaData = job.getMetaDataText(); - - return data; + return new JobConfigurationData(access.resolveUnEncryptedJobConfiguration(job), job.getMetaDataText()); } /** @@ -166,14 +168,14 @@ public void updateJobMessagesInOwnTransaction(UUID jobUUID, SecHubMessagesList s } public void markJobAsCancelRequestedInOwnTransaction(UUID jobUUID) { - updatJobStatusState(jobUUID, PDSJobStatusState.CANCEL_REQUESTED); + updateJobStatusState(jobUUID, PDSJobStatusState.CANCEL_REQUESTED); } public void markJobAsCanceledInOwnTransaction(UUID jobUUID) { - updatJobStatusState(jobUUID, PDSJobStatusState.CANCELED); + updateJobStatusState(jobUUID, PDSJobStatusState.CANCELED); } - private void updatJobStatusState(UUID jobUUID, PDSJobStatusState state) { + private void updateJobStatusState(UUID jobUUID, PDSJobStatusState state) { PDSJob job = assertJobFound(jobUUID, repository); PDSJobStatusState oldState = job.getState(); if (state == oldState) { diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSWorkspacePreparationContextFactory.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSWorkspacePreparationContextFactory.java index ab34430006..a135eb211d 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSWorkspacePreparationContextFactory.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSWorkspacePreparationContextFactory.java @@ -31,7 +31,7 @@ public class PDSWorkspacePreparationContextFactory { public PDSWorkspacePreparationContext createPreparationContext(PDSJobConfigurationSupport configurationSupport) { if (configurationSupport == null) { - throw new IllegalArgumentException("configuration support may not be null!"); + throw new IllegalArgumentException("configuration encryptionSupport may not be null!"); } PDSWorkspacePreparationContext preparationContext = new PDSWorkspacePreparationContext(); diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSWorkspaceService.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSWorkspaceService.java index e35b3ca982..31586fa29c 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSWorkspaceService.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/job/PDSWorkspaceService.java @@ -146,7 +146,7 @@ public void throwException(String message, Exception cause) throws IOException { * * * @param pdsJobUUID - * @param config + * @param configuration * @param metaData * @return {@link PDSWorkspacePreparationResult}, never null * @throws IOException diff --git a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/security/PDSSecurityConfiguration.java b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/security/PDSSecurityConfiguration.java index f3b127cd2e..7e2df4336b 100644 --- a/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/security/PDSSecurityConfiguration.java +++ b/sechub-pds/src/main/java/com/mercedesbenz/sechub/pds/security/PDSSecurityConfiguration.java @@ -33,7 +33,7 @@ public class PDSSecurityConfiguration { private static final String KEY_ADMIN_USERID = "pds.admin.userid"; private static final String KEY_ADMIN_APITOKEN = "pds.admin.apitoken"; - @PDSMustBeDocumented(value = "Techuser user id", scope = "credentials.") + @PDSMustBeDocumented(value = "Techuser user id.", scope = "credentials.") @Value("${" + KEY_TECHUSER_USERID + "}") String techUserId; diff --git a/sechub-pds/src/main/resources/application-pds_integrationtest.yml b/sechub-pds/src/main/resources/application-pds_integrationtest.yml index 022027b4dc..2a4e2e0823 100644 --- a/sechub-pds/src/main/resources/application-pds_integrationtest.yml +++ b/sechub-pds/src/main/resources/application-pds_integrationtest.yml @@ -31,6 +31,14 @@ pds: userid: pds-inttest-admin apitoken: '{noop}pds-inttest-apitoken' + encryption: + algorithm: AES_GCM_SIV_256 + # Test key to have encryption also in integraation tests + # (this is an exception: normally only environment variables are accepted, but + # for integration tests we allow this one) + # In spring boot smoke test this is not set, there is NONE used which is the default + secret-key: nj3AS0UOcA4d/K5cIfOUXipkB/x9oJAVZpVwLqJ5LJE= + logging: level: com.mercedesbenz.sechub: DEBUG diff --git a/sechub-pds/src/main/resources/db/migration/U6__encryption.sql b/sechub-pds/src/main/resources/db/migration/U6__encryption.sql new file mode 100644 index 0000000000..90213bc28c --- /dev/null +++ b/sechub-pds/src/main/resources/db/migration/U6__encryption.sql @@ -0,0 +1,6 @@ +-- SPDX-License-Identifier: MIT +ALTER TABLE pds_job DROP COLUMN encrypted_configuration bytea; +ALTER TABLE pds_job DROP COLUMN encrypt_initial_vector bytea; +ALTER TABLE pds_job DROP COLUMN encryption_out_of_sync; + +ALTER TABLE pds_job ADD COLUMN configuration varchar(8192) not null; diff --git a/sechub-pds/src/main/resources/db/migration/V6__encryption.sql b/sechub-pds/src/main/resources/db/migration/V6__encryption.sql new file mode 100644 index 0000000000..da047c8489 --- /dev/null +++ b/sechub-pds/src/main/resources/db/migration/V6__encryption.sql @@ -0,0 +1,13 @@ +-- SPDX-License-Identifier: MIT + +-- New encryption columns. +-- We allow null values here explicit. This happens only for old unencrypted data +-- and will result in restarts of current running unencrypted jobs (if necessary at all) +-- by SecHub +ALTER TABLE pds_job ADD COLUMN encrypted_configuration bytea; +ALTER TABLE pds_job ADD COLUMN encrypt_initial_vector bytea; +ALTER TABLE pds_job ADD COLUMN encryption_out_of_sync boolean; + +-- Delete old configuration column (+data), existing data will be lost - this is a wanted behavor +-- see comment before. +ALTER TABLE pds_job DROP COLUMN configuration; diff --git a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/PDSStartupAssertEnvironmentVariablesUsedTest.java b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/PDSStartupAssertEnvironmentVariablesUsedTest.java index fb37b78dce..2294016507 100644 --- a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/PDSStartupAssertEnvironmentVariablesUsedTest.java +++ b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/PDSStartupAssertEnvironmentVariablesUsedTest.java @@ -17,6 +17,7 @@ import com.mercedesbenz.sechub.commons.core.environment.SecureEnvironmentVariableKeyValueRegistry; import com.mercedesbenz.sechub.commons.core.environment.SystemEnvironmentVariableSupport; +import com.mercedesbenz.sechub.pds.encryption.PDSEncryptionConfiguration; import com.mercedesbenz.sechub.pds.security.PDSSecurityConfiguration; import com.mercedesbenz.sechub.pds.storage.PDSS3PropertiesSetup; import com.mercedesbenz.sechub.pds.storage.PDSSharedVolumePropertiesSetup; @@ -29,6 +30,7 @@ class PDSStartupAssertEnvironmentVariablesUsedTest { private PDSSecurityConfiguration securityConfiguration; private PDSSharedVolumePropertiesSetup sharedVolumeSetup; private Environment environment; + private PDSEncryptionConfiguration encryptionConfiguration; @BeforeEach void beforeEach() { @@ -38,6 +40,8 @@ void beforeEach() { securityConfiguration = mock(PDSSecurityConfiguration.class); envVariableSupport = mock(SystemEnvironmentVariableSupport.class); sharedVolumeSetup = mock(PDSSharedVolumePropertiesSetup.class); + encryptionConfiguration = mock(PDSEncryptionConfiguration.class); + environment = mock(Environment.class); assertionToTest.envVariableSupport = envVariableSupport; @@ -45,6 +49,7 @@ void beforeEach() { assertionToTest.s3Setup = s3setup; assertionToTest.sharedVolumeSetup = sharedVolumeSetup; assertionToTest.environment = environment; + assertionToTest.encryptionConfiguration = encryptionConfiguration; } diff --git a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/encryption/PDSEncryptionConfigurationTest.java b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/encryption/PDSEncryptionConfigurationTest.java new file mode 100644 index 0000000000..ba634b68ba --- /dev/null +++ b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/encryption/PDSEncryptionConfigurationTest.java @@ -0,0 +1,114 @@ +package com.mercedesbenz.sechub.pds.encryption; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EmptySource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.EnumSource.Mode; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +class PDSEncryptionConfigurationTest { + + private PDSEncryptionConfiguration configurationToTest; + + @BeforeEach + void beforeEach() { + configurationToTest = new PDSEncryptionConfiguration(); + } + + @ParameterizedTest + @EnumSource(value = PDSCipherAlgorithm.class, mode = Mode.EXCLUDE, names = "NONE") + void init_with_empty_key_throws_exception(PDSCipherAlgorithm algorithm) throws Exception { + + /* prepare */ + configurationToTest.algorithmAsString = algorithm.name(); + configurationToTest.secretKeyAsString = ""; + + /* execute + test @formatter:off */ + assertThatThrownBy(() -> + configurationToTest.init()). + isInstanceOf(PDSEncryptionException.class). + hasMessageContaining(algorithm+" does not allow an empty secret key"); + + /* @formatter:on */ + + } + + @ParameterizedTest + @EmptySource + @NullSource + void init_with_invalid_key_throws_NO_exception_for_NONE_algorithm(String invalidSecretKey) throws Exception { + + /* prepare */ + configurationToTest.algorithmAsString = PDSCipherAlgorithm.NONE.name(); + configurationToTest.secretKeyAsString = invalidSecretKey; + + /* execute + test @formatter:off */ + assertThatNoException().isThrownBy(() -> + configurationToTest.init()); + /* @formatter:on */ + + } + + @ParameterizedTest + @EnumSource(PDSCipherAlgorithm.class) + void init_resets_secret_key_string_and_setup_internal_data(PDSCipherAlgorithm algorithm) throws Exception { + + /* prepare */ + configurationToTest.algorithmAsString = algorithm.name(); + configurationToTest.secretKeyAsString = "test-secret"; + + /* execute */ + configurationToTest.init(); + + /* test */ + assertThat(configurationToTest.secretKeyAsString).isNull(); // reset is done + assertThat(configurationToTest.algorithmAsString).isNotNull(); + + assertThat(configurationToTest.getAlgorithm()).isEqualTo(algorithm); + assertThat(configurationToTest.getSecretKeyBytes()).isEqualTo("test-secret".getBytes()); + + } + + @ParameterizedTest + @EnumSource(PDSCipherAlgorithm.class) + void init_resets_secret_key_string_and_setup_internal_data_lowercase_works_as_well(PDSCipherAlgorithm algorithm) throws Exception { + + /* prepare */ + configurationToTest.algorithmAsString = algorithm.name().toLowerCase(); + configurationToTest.secretKeyAsString = "test-secret"; + + /* execute */ + configurationToTest.init(); + + /* test */ + assertThat(configurationToTest.secretKeyAsString).isNull(); // reset is done + assertThat(configurationToTest.algorithmAsString).isNotNull(); + + assertThat(configurationToTest.getAlgorithm()).isEqualTo(algorithm); + assertThat(configurationToTest.getSecretKeyBytes()).isEqualTo("test-secret".getBytes()); + + } + + @ParameterizedTest + @ValueSource(strings = "unknown") + @EmptySource + @NullSource + void init_unknown_algorithm_throws_exception(String wrongAlgorithmAsText) throws Exception { + + /* prepare */ + configurationToTest.algorithmAsString = wrongAlgorithmAsText; + configurationToTest.secretKeyAsString = "test-secret"; + + /* execute + test */ + assertThatThrownBy(() -> configurationToTest.init()).isInstanceOf(PDSEncryptionException.class).hasMessageContaining("not supported"); + + /* test */ + assertThat(configurationToTest.secretKeyAsString).isNull(); // reset is still done + + } + +} diff --git a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/encryption/PDSEncryptionServiceTest.java b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/encryption/PDSEncryptionServiceTest.java new file mode 100644 index 0000000000..a6bcc1fbe1 --- /dev/null +++ b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/encryption/PDSEncryptionServiceTest.java @@ -0,0 +1,102 @@ +package com.mercedesbenz.sechub.pds.encryption; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.Base64; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.ArgumentCaptor; + +import com.mercedesbenz.sechub.commons.encryption.EncryptionSupport; +import com.mercedesbenz.sechub.commons.encryption.InitializationVector; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipher; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherFactory; +import com.mercedesbenz.sechub.commons.encryption.SecretKeyProvider; + +class PDSEncryptionServiceTest { + + private PDSEncryptionConfiguration configuration; + private PDSEncryptionService serviceToTest; + private PersistentCipherFactory cipherFactory; + private EncryptionSupport encryptionSupport; + + @BeforeEach + public void beforeEach() throws Exception { + configuration = mock(PDSEncryptionConfiguration.class); + cipherFactory = mock(PersistentCipherFactory.class); + encryptionSupport = mock(EncryptionSupport.class); + + serviceToTest = new PDSEncryptionService(); + + serviceToTest.configuration = configuration; + serviceToTest.cipherFactory = cipherFactory; + serviceToTest.encryptionSupport = encryptionSupport; + } + + @ParameterizedTest + @EnumSource(PDSCipherAlgorithm.class) + void init_creates_expected_cipher_with_secret_key_inside(PDSCipherAlgorithm algorithm) throws Exception { + + /* prepare */ + String testSecretPlainText = "test-secret"; + + when(configuration.getAlgorithm()).thenReturn(algorithm); + String base64String = Base64.getEncoder().encodeToString(testSecretPlainText.getBytes()); + when(configuration.getSecretKeyBytes()).thenReturn(base64String.getBytes()); + + PersistentCipher cipher = mock(PersistentCipher.class); + when(cipherFactory.createCipher(any(), eq(algorithm.getType()))).thenReturn(cipher); + + /* execute */ + serviceToTest.init(); + + /* test */ + assertThat(serviceToTest.cipher).isEqualTo(cipher); + ArgumentCaptor captor = ArgumentCaptor.forClass(SecretKeyProvider.class); + verify(cipherFactory).createCipher(captor.capture(), eq(algorithm.getType())); + + SecretKeyProvider capturedProvider = captor.getValue(); + switch (algorithm) { + case NONE: + assertThat(capturedProvider).isNull(); // no secret key provider for NONE + break; + default: + assertThat(capturedProvider.getSecretKey().getEncoded()).isEqualTo(testSecretPlainText.getBytes()); + break; + + } + + } + + @Test + void decryptString_returns_decrypted_string_by_encryption_support() throws Exception { + + /* prepare */ + + InitializationVector initialVector = mock(InitializationVector.class); + byte[] encryptedData = "encrypted-data".getBytes(); + + PersistentCipher cipher = mock(PersistentCipher.class); + when(cipherFactory.createCipher(any(), any())).thenReturn(cipher); + PDSCipherAlgorithm algorithm = mock(PDSCipherAlgorithm.class); + when(configuration.getAlgorithm()).thenReturn(algorithm); + + serviceToTest.init(); + + String resultFromEncryptionSupport = "encrypted test data from encryption support..."; + when(encryptionSupport.decryptString(encryptedData, cipher, initialVector)).thenReturn(resultFromEncryptionSupport); + + /* execute */ + String result = serviceToTest.decryptString(encryptedData, initialVector); + + /* test */ + assertThat(result).isEqualTo(resultFromEncryptionSupport); + + } + +} diff --git a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/execution/PDSApplyFutureExecutionResultToJobServiceTest.java b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/execution/PDSApplyFutureExecutionResultToJobServiceTest.java new file mode 100644 index 0000000000..77dd4149f6 --- /dev/null +++ b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/execution/PDSApplyFutureExecutionResultToJobServiceTest.java @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.pds.execution; + +import static org.mockito.Mockito.*; + +import java.util.concurrent.Future; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import com.mercedesbenz.sechub.commons.pds.data.PDSJobStatusState; +import com.mercedesbenz.sechub.pds.job.PDSJob; +import com.mercedesbenz.sechub.pds.job.PDSJobRepository; + +class PDSApplyFutureExecutionResultToJobServiceTest { + + private PDSApplyFutureExecutionResultToJobService serviceToTest; + private Future future; + private PDSJob job; + private PDSJobRepository repository; + private PDSExecutionResult result; + + @SuppressWarnings("unchecked") + @BeforeEach + public void beforeEach() throws Exception { + serviceToTest = new PDSApplyFutureExecutionResultToJobService(); + repository = mock(PDSJobRepository.class); + serviceToTest.repository = repository; + + future = mock(Future.class); + job = mock(PDSJob.class); + + result = mock(PDSExecutionResult.class); + when(future.get()).thenReturn(result); + } + + @Test + void future_canceled_then_job_is_set_to_canceled_and_then_stored() throws Exception { + + /* prepare */ + when(future.isCancelled()).thenReturn(true); + + /* execute */ + serviceToTest.applyResultToJob(future, job); + + /* test */ + InOrder inOrder = inOrder(job, repository); + inOrder.verify(job).setState(PDSJobStatusState.CANCELED); + inOrder.verify(repository).save(job); + + } + + @Test + void job_with_result() throws Exception { + + /* prepare */ + when(future.isCancelled()).thenReturn(false); + when(result.getResult()).thenReturn("result1"); + when(result.isEncryptionFailure()).thenReturn(false); + when(result.isFailed()).thenReturn(false); + when(result.isCanceled()).thenReturn(false); + + /* execute */ + serviceToTest.applyResultToJob(future, job); + + /* test */ + InOrder inOrder = inOrder(job, repository); + inOrder.verify(job).setResult("result1"); + inOrder.verify(job).setState(PDSJobStatusState.DONE); + inOrder.verify(repository).save(job); + + } + + @Test + void job_canceled() throws Exception { + + /* prepare */ + when(future.isCancelled()).thenReturn(false); + when(result.getResult()).thenReturn(""); + when(result.isEncryptionFailure()).thenReturn(false); + when(result.isFailed()).thenReturn(false); + when(result.isCanceled()).thenReturn(true); + + /* execute */ + serviceToTest.applyResultToJob(future, job); + + /* test */ + InOrder inOrder = inOrder(job, repository); + inOrder.verify(job).setResult(""); + inOrder.verify(job).setState(PDSJobStatusState.CANCELED); + inOrder.verify(repository).save(job); + + } + + @Test + void job_with_encryption_failure() throws Exception { + + /* prepare */ + when(future.isCancelled()).thenReturn(false); + when(result.getResult()).thenReturn(""); + when(result.isFailed()).thenReturn(true); + when(result.isCanceled()).thenReturn(false); + when(result.isEncryptionFailure()).thenReturn(true); + + /* execute */ + serviceToTest.applyResultToJob(future, job); + + /* test */ + InOrder inOrder = inOrder(job, repository); + inOrder.verify(job).setResult(""); + inOrder.verify(job).setState(PDSJobStatusState.FAILED); + inOrder.verify(job).setEncryptionOutOfSync(true); + inOrder.verify(repository).save(job); + + } + +} diff --git a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionCallableTest.java b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionCallableTest.java index c60c14ffb3..d3c4ad89f7 100644 --- a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionCallableTest.java +++ b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/execution/PDSExecutionCallableTest.java @@ -6,7 +6,6 @@ import static org.mockito.Mockito.*; import java.io.File; -import java.io.IOException; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -21,9 +20,11 @@ import com.mercedesbenz.sechub.commons.model.SecHubMessagesList; import com.mercedesbenz.sechub.commons.pds.PDSProcessAdapterFactory; import com.mercedesbenz.sechub.commons.pds.ProcessAdapter; +import com.mercedesbenz.sechub.pds.encryption.PDSEncryptionException; import com.mercedesbenz.sechub.pds.job.JobConfigurationData; import com.mercedesbenz.sechub.pds.job.PDSCheckJobStatusService; import com.mercedesbenz.sechub.pds.job.PDSGetJobStreamService; +import com.mercedesbenz.sechub.pds.job.PDSJobConfiguration; import com.mercedesbenz.sechub.pds.job.PDSJobTransactionService; import com.mercedesbenz.sechub.pds.job.PDSWorkspacePreparationResult; import com.mercedesbenz.sechub.pds.job.PDSWorkspaceService; @@ -53,7 +54,7 @@ class PDSExecutionCallableTest { private ProductLaunchProcessHandlingData launchProcessHandlingData; @BeforeEach - void beforeEach() throws IOException { + void beforeEach() throws Exception { jobUUID = UUID.randomUUID(); workspaceTestDataJob1Folder = new File("./src/test/resources/testdata/workspace1/job1"); resultFile = new File(workspaceTestDataJob1Folder, "result.txt"); @@ -98,10 +99,11 @@ void beforeEach() throws IOException { PDSWorkspacePreparationResult launchScriptExecutableResult = new PDSWorkspacePreparationResult(true); when(workspaceService.prepare(eq(jobUUID), any(), any())).thenReturn(launchScriptExecutableResult); - when(data.getJobConfigurationJson()).thenReturn("{}"); + PDSJobConfiguration configuration = new PDSJobConfiguration(); + when(data.getJobConfiguration()).thenReturn(configuration); when(processAdapterFactory.startProcess(any())).thenReturn(processAdapter); - when(jobTransactionService.getJobConfigurationData(jobUUID)).thenReturn(data); + when(jobTransactionService.getJobConfigurationDataOrFail(jobUUID)).thenReturn(data); when(workspaceService.getProductPathFor(any())).thenReturn("the/path/to/product.sh"); when(workspaceService.createLocationData(jobUUID)).thenReturn(locationData); when(workspaceService.getMessagesFolder(jobUUID)).thenReturn(messageFolder); @@ -134,12 +136,13 @@ void timeout_with_0_fails_without_job_transaction_write() throws Exception { /* test */ assertJobHasMarkedAsRunningInOwnTransaction(); - assertTrue(result.failed); + assertTrue(result.isFailed()); // internally an illegal state is thrown before any execution, verify(processAdapterFactory, never()).startProcess(any()); verify(jobTransactionService, never()).updateJobExecutionDataInOwnTransaction(any(), any()); // check there is no wait for a process at all verify(processAdapter, never()).waitFor(anyLong(), any()); + assertFalse(result.isEncryptionFailure()); } @Test @@ -152,8 +155,8 @@ void a_timeout_does_write_execution_result_data() throws Exception { /* test */ assertJobHasMarkedAsRunningInOwnTransaction(); - assertTrue(result.failed); - assertEquals("Product time out.", result.result); + assertTrue(result.isFailed()); + assertEquals("Product time out.", result.getResult()); ArgumentCaptor executionDataCaptor = ArgumentCaptor.forClass(PDSExecutionData.class); verify(jobTransactionService).updateJobExecutionDataInOwnTransaction(eq(jobUUID), executionDataCaptor.capture()); @@ -162,6 +165,7 @@ void a_timeout_does_write_execution_result_data() throws Exception { assertEquals("the output", executionData.getOutputStreamData()); assertEquals("an error", executionData.getErrorStreamData()); assertEquals("meta data", executionData.getMetaData()); + assertFalse(result.isEncryptionFailure()); } @@ -175,8 +179,8 @@ void no_timeout_does_not_fail_and_writes_execution_result_data() throws Exceptio /* test */ assertJobHasMarkedAsRunningInOwnTransaction(); - assertFalse(result.failed); - assertEquals("the result", result.result); + assertFalse(result.isFailed()); + assertEquals("the result", result.getResult()); ArgumentCaptor executionDataCaptor = ArgumentCaptor.forClass(PDSExecutionData.class); verify(jobTransactionService).updateJobExecutionDataInOwnTransaction(eq(jobUUID), executionDataCaptor.capture()); @@ -185,6 +189,7 @@ void no_timeout_does_not_fail_and_writes_execution_result_data() throws Exceptio assertEquals("the output", executionData.getOutputStreamData()); assertEquals("an error", executionData.getErrorStreamData()); assertEquals("meta data", executionData.getMetaData()); + assertFalse(result.isEncryptionFailure()); } @Test @@ -197,9 +202,9 @@ void no_timeout_does_not_fail_and_writes_messages() throws Exception { /* test */ assertJobHasMarkedAsRunningInOwnTransaction(); - assertFalse(result.failed); + assertFalse(result.isFailed()); assertJob1MessageHasBeenPersistedToDB(); - + assertFalse(result.isEncryptionFailure()); } @Test @@ -212,8 +217,9 @@ void a_timeout_does_fail_and_writes_messages() throws Exception { /* test */ assertJobHasMarkedAsRunningInOwnTransaction(); - assertTrue(result.failed); + assertTrue(result.isFailed()); assertJob1MessageHasBeenPersistedToDB(); + assertFalse(result.isEncryptionFailure()); } @@ -230,6 +236,19 @@ void process_waiting_is_done_with_value_from_handling_data_factory_and_time_unit verify(processAdapter).waitFor(value, TimeUnit.MINUTES); } + @Test + void when_getJobConfigurationDataOrFail_fails_the_exception_leads_to_a_job_marked_as_encryption_out_of_synch() throws Exception { + /* prepare */ + when(jobTransactionService.getJobConfigurationDataOrFail(any())).thenThrow(new PDSEncryptionException("some text")); + simulateProcessDone(); + + /* execute */ + PDSExecutionResult result = callableToTest.call(); + + /* test */ + assertTrue(result.isEncryptionFailure()); + } + /* ++++++++++++++++++++++++++++++++++++++++++++++++++++ */ /* + ................Helpers......................... + */ /* ++++++++++++++++++++++++++++++++++++++++++++++++++++ */ diff --git a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSCreateJobServiceTest.java b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSCreateJobServiceTest.java index 2b480b73c2..3c94420863 100644 --- a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSCreateJobServiceTest.java +++ b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSCreateJobServiceTest.java @@ -1,28 +1,25 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.pds.job; -import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.util.UUID; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import com.mercedesbenz.sechub.commons.encryption.EncryptionResult; +import com.mercedesbenz.sechub.commons.encryption.InitializationVector; import com.mercedesbenz.sechub.pds.PDSNotAcceptableException; import com.mercedesbenz.sechub.pds.config.PDSServerConfigurationService; +import com.mercedesbenz.sechub.pds.encryption.PDSEncryptionService; import com.mercedesbenz.sechub.pds.security.PDSUserContextService; -import com.mercedesbenz.sechub.test.junit4.ExpectedExceptionFactory; public class PDSCreateJobServiceTest { - @Rule - public ExpectedException expected = ExpectedExceptionFactory.none(); - private PDSCreateJobService serviceToTest; private UUID sechubJobUUID; private PDSJobRepository repository; @@ -32,8 +29,11 @@ public class PDSCreateJobServiceTest { private PDSJobConfigurationValidator configurationValidator; private PDSServerConfigurationService serverConfigurationService; - @Before - public void before() throws Exception { + private PDSEncryptionService encryptionService; + private EncryptionResult encryptionResult; + + @BeforeEach + void before() throws Exception { sechubJobUUID = UUID.randomUUID(); createdJob1UUID = UUID.randomUUID(); repository = mock(PDSJobRepository.class); @@ -42,21 +42,31 @@ public void before() throws Exception { userContextService = mock(PDSUserContextService.class); when(userContextService.getUserId()).thenReturn("callerName"); + encryptionService = mock(PDSEncryptionService.class); serviceToTest = new PDSCreateJobService(); serviceToTest.repository = repository; serviceToTest.userContextService = userContextService; serviceToTest.configurationValidator = configurationValidator; serviceToTest.serverConfigurationService = serverConfigurationService; + serviceToTest.encryptionService = encryptionService; resultJob1 = new PDSJob(); resultJob1.uUID = createdJob1UUID; when(repository.save(any())).thenReturn(resultJob1); + + // encryption stup + encryptionResult = mock(EncryptionResult.class); + byte[] encryptedBytes = "some-pseudo-encrypted-data".getBytes(); + when(encryptionResult.getEncryptedData()).thenReturn(encryptedBytes); + InitializationVector initialVector = mock(InitializationVector.class); + when(encryptionResult.getInitialVector()).thenReturn(initialVector); + when(encryptionService.encryptString(any())).thenReturn(encryptionResult); } @Test - public void creating_a_job_returns_jobUUD_of_stored_job_in_repository() { + void creating_a_job_returns_jobUUD_of_stored_job_in_repository() { /* prepare */ PDSJobConfiguration configuration = new PDSJobConfiguration(); configuration.setSechubJobUUID(sechubJobUUID); @@ -65,13 +75,13 @@ public void creating_a_job_returns_jobUUD_of_stored_job_in_repository() { PDSJobCreateResult result = serviceToTest.createJob(configuration); /* test */ - assertNotNull(result); + assertThat(result).isNotNull(); UUID pdsJobUUID = result.getJobUUID(); - assertEquals(createdJob1UUID, pdsJobUUID); + assertThat(createdJob1UUID).isEqualTo(pdsJobUUID); } @Test - public void creating_a_job_sets_current_user_as_owner() { + void creating_a_job_sets_current_user_as_owner() { /* prepare */ PDSJobConfiguration configuration = new PDSJobConfiguration(); @@ -79,33 +89,40 @@ public void creating_a_job_sets_current_user_as_owner() { PDSJobCreateResult result = serviceToTest.createJob(configuration); /* test */ - assertNotNull(result); + assertThat(result).isNotNull(); ArgumentCaptor jobCaptor = ArgumentCaptor.forClass(PDSJob.class); verify(repository).save(jobCaptor.capture()); - assertEquals("callerName", jobCaptor.getValue().getOwner()); + assertThat(jobCaptor.getValue().getOwner()).isEqualTo("callerName"); } @Test - public void creating_a_job_sets_configuration_as_json() throws Exception { + void creating_a_job_sets_encrypted_configuration() throws Exception { /* prepare */ PDSJobConfiguration configuration = new PDSJobConfiguration(); + // check precondition + String json = configuration.toJSON(); + // Next line normally not valid, but validator does not throw an exception here, + // so we can have an empty configuration here... Just to test it + assertThat(json).isEqualTo("{\"parameters\":[]}"); + + // simulate encryption for this parameter + byte[] encryptedBytes = ("encrypted:" + json).getBytes(); + when(encryptionResult.getEncryptedData()).thenReturn(encryptedBytes); /* execute */ serviceToTest.createJob(configuration); /* test */ + ArgumentCaptor jobCaptor = ArgumentCaptor.forClass(PDSJob.class); verify(repository).save(jobCaptor.capture()); - String json = configuration.toJSON(); - // Next line normally not valid, but validator does not throw an exception here, - // so we can have an empty config here... Just to test it - assertEquals("{\"parameters\":[]}", json); - assertEquals(json, jobCaptor.getValue().getJsonConfiguration()); + PDSJob persistedJob = jobCaptor.getValue(); + assertThat(persistedJob.getEncryptedConfiguration()).isEqualTo(encryptedBytes); } @Test - public void creating_a_job_calls_configurationValidator() { + void creating_a_job_calls_configurationValidator() { /* prepare */ PDSJobConfiguration configuration = new PDSJobConfiguration(); @@ -117,17 +134,13 @@ public void creating_a_job_calls_configurationValidator() { } @Test - public void creating_a_job_fires_exception_thrown_by_validator() { + void creating_a_job_fires_exception_thrown_by_validator() { /* prepare */ PDSJobConfiguration configuration = new PDSJobConfiguration(); doThrow(new PDSNotAcceptableException("ups")).when(configurationValidator).assertPDSConfigurationValid(configuration); - /* test */ - expected.expect(PDSNotAcceptableException.class); - expected.expectMessage("ups"); - /* execute */ - serviceToTest.createJob(configuration); + assertThatThrownBy(() -> serviceToTest.createJob(configuration)).isInstanceOf(PDSNotAcceptableException.class).hasMessage("ups"); } diff --git a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSGetJobStatusServiceTest.java b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSGetJobStatusServiceTest.java index 04c9bebd21..5cd966ede0 100644 --- a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSGetJobStatusServiceTest.java +++ b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSGetJobStatusServiceTest.java @@ -1,88 +1,97 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.pds.job; -import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import java.time.LocalDateTime; import java.util.Optional; import java.util.UUID; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import com.mercedesbenz.sechub.commons.pds.data.PDSJobStatus; import com.mercedesbenz.sechub.commons.pds.data.PDSJobStatusState; import com.mercedesbenz.sechub.pds.PDSNotFoundException; -import com.mercedesbenz.sechub.test.junit4.ExpectedExceptionFactory; public class PDSGetJobStatusServiceTest { - @Rule - public ExpectedException expected = ExpectedExceptionFactory.none(); - + private static final LocalDateTime CREATION_TIME = LocalDateTime.of(2020, 06, 23, 16, 35, 01); + private static final LocalDateTime END_TIME = LocalDateTime.of(2020, 06, 23, 16, 37, 03); private PDSGetJobStatusService serviceToTest; private UUID jobUUID; private PDSJobRepository repository; private PDSJob job; - @Before - public void before() throws Exception { + @BeforeEach + void beforeEach() throws Exception { repository = mock(PDSJobRepository.class); jobUUID = UUID.randomUUID(); - job = new PDSJob(); - job.uUID = jobUUID; - job.created = LocalDateTime.of(2020, 06, 23, 16, 35, 01); - job.owner = "theOwner"; - - when(repository.findById(jobUUID)).thenReturn(Optional.of(job)); serviceToTest = new PDSGetJobStatusService(); serviceToTest.repository = repository; } - @Test - public void get_status_works_for_any_state() { - for (PDSJobStatusState state : PDSJobStatusState.values()) { - String ended = null; - if (PDSJobStatusState.DONE.equals(state)) { - ended = "2020-06-23T16:37:03"; - job.ended = LocalDateTime.of(2020, 06, 23, 16, 37, 03); - } else { - ended = ""; - } - fetchStateWorksFor(state, ended); - } - } - - @Test - public void job_not_found_throws_pds_not_found_exception() { - /* test */ - expected.expect(PDSNotFoundException.class); - expected.expectMessage("Given job does not exist"); + @ParameterizedTest + @EnumSource(PDSJobStatusState.class) + void get_status_works_for_any_state(PDSJobStatusState state) { + /* prepare */ + prepareJob(state, END_TIME, false); /* execute */ - UUID notExistingJobUUID = UUID.randomUUID(); - serviceToTest.getJobStatus(notExistingJobUUID); + PDSJobStatus result = serviceToTest.getJobStatus(jobUUID); + /* test */ + assertThat(result.getOwner()).isEqualTo(job.owner); + assertThat(result.getCreated()).isEqualTo(CREATION_TIME.toString()); + assertThat(result.getEnded()).isEqualTo(END_TIME.toString()); + assertThat(result.getState()).isEqualTo(job.state); + assertThat(result.isEncryptionOutOfSync()).isEqualTo(false); } - private void fetchStateWorksFor(PDSJobStatusState state, String eexpectedEnded) { - /* prepare */ - job.setState(state); + @ParameterizedTest + @EnumSource(PDSJobStatusState.class) + void get_status_works_for_any_state_encryption_out_of_sync(PDSJobStatusState state) { /* prepare */ - job.state = PDSJobStatusState.DONE; + prepareJob(state, END_TIME, true); /* execute */ PDSJobStatus result = serviceToTest.getJobStatus(jobUUID); /* test */ - assertEquals(job.owner, result.owner); - assertEquals("2020-06-23T16:35:01", result.created); - assertEquals(eexpectedEnded, result.ended); - assertEquals(job.state.name(), result.state); + assertThat(result.getOwner()).isEqualTo(job.owner); + assertThat(result.getCreated()).isEqualTo(CREATION_TIME.toString()); + assertThat(result.getEnded()).isEqualTo(END_TIME.toString()); + assertThat(result.getState()).isEqualTo(job.state); + assertThat(result.isEncryptionOutOfSync()).isEqualTo(true); + } + + @Test + void job_not_found_throws_pds_not_found_exception() { + /* prepare */ + UUID notExistingJobUUID = UUID.randomUUID(); + + /* execute + test */ + assertThatThrownBy(() -> serviceToTest.getJobStatus(notExistingJobUUID)).isInstanceOf(PDSNotFoundException.class) + .hasMessageContaining("Given job does not exist"); + + } + + private PDSJob prepareJob(PDSJobStatusState state, LocalDateTime expectedEnded, boolean encryptionOutOfSync) { + job = new PDSJob(); + job.uUID = jobUUID; + job.created = CREATION_TIME; + job.setOwner("theOwner"); + job.setEnded(expectedEnded); + job.setState(state); + job.setEncryptionOutOfSync(encryptionOutOfSync); + + when(repository.findById(jobUUID)).thenReturn(Optional.of(job)); + return job; } } diff --git a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSJobConfigurationAccessTest.java b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSJobConfigurationAccessTest.java new file mode 100644 index 0000000000..8951fc1fca --- /dev/null +++ b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSJobConfigurationAccessTest.java @@ -0,0 +1,97 @@ +package com.mercedesbenz.sechub.pds.job; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.mercedesbenz.sechub.commons.encryption.InitializationVector; +import com.mercedesbenz.sechub.pds.encryption.PDSEncryptionException; +import com.mercedesbenz.sechub.pds.encryption.PDSEncryptionService; +import com.mercedesbenz.sechub.test.TestCanaryException; + +class PDSJobConfigurationAccessTest { + + private PDSJobConfigurationAccess accessToTest; + private PDSEncryptionService encryptionService; + + @BeforeEach + public void beforeEach() throws Exception { + accessToTest = new PDSJobConfigurationAccess(); + encryptionService = mock(PDSEncryptionService.class); + + accessToTest.encryptionService = encryptionService; + } + + @Test + void access_can_resolve_pds_job_configuration_from_encrypted_bytes() throws Exception { + + /* prepare */ + byte[] persistedInitialVector = "initme".getBytes(); + byte[] persistedEncryptedConfiguration = "i-am-encrypted".getBytes(); + InitializationVector givenVector = new InitializationVector(persistedInitialVector); + + PDSJob job = mock(PDSJob.class); + when(job.getEncryptionInitialVectorData()).thenReturn(persistedInitialVector); + when(job.getEncryptedConfiguration()).thenReturn(persistedEncryptedConfiguration); + + PDSJobConfiguration configuration = new PDSJobConfiguration(); + String jsonAsPlainText = configuration.toJSON(); + + when(encryptionService.decryptString(eq(persistedEncryptedConfiguration), eq(givenVector))).thenReturn(jsonAsPlainText); + + /* execute */ + PDSJobConfiguration result = accessToTest.resolveUnEncryptedJobConfiguration(job); + + /* test */ + assertThat(result).isNotNull(); + assertThat(result.toJSON()).isEqualTo(jsonAsPlainText); + + } + + @Test + void access_throws_pds_encryption_exception_when_job_has_encrypted_data_but_encryption_service_throws_an_exception() { + /* prepare */ + PDSJob job = mock(PDSJob.class); + when(encryptionService.decryptString(any(), any())).thenThrow(new TestCanaryException()); + when(job.getEncryptedConfiguration()).thenReturn("something".getBytes()); + when(job.getEncryptionInitialVectorData()).thenReturn("init".getBytes()); + + /* execute */ + assertThatThrownBy(() -> accessToTest.resolveUnEncryptedJobConfiguration(job)).isInstanceOf(PDSEncryptionException.class) + .hasRootCauseInstanceOf(TestCanaryException.class); + } + + @Test + void access_throws_pds_encryption_exception_when_job_has_NO_encrypted_data() { + /* prepare */ + PDSJob job = mock(PDSJob.class); + UUID uuid = UUID.randomUUID(); + when(job.getUUID()).thenReturn(uuid); + when(job.getEncryptedConfiguration()).thenReturn(null); + when(job.getEncryptionInitialVectorData()).thenReturn("init".getBytes()); + + /* execute */ + assertThatThrownBy(() -> accessToTest.resolveUnEncryptedJobConfiguration(job)).isInstanceOf(PDSEncryptionException.class) + .hasRootCauseInstanceOf(IllegalStateException.class).hasRootCauseMessage("No encrypted configuration found for PDS job: " + uuid); + } + + @Test + void access_throws_pds_encryption_exception_when_job_has_NO_initial_vector_data() { + /* prepare */ + PDSJob job = mock(PDSJob.class); + UUID uuid = UUID.randomUUID(); + when(job.getUUID()).thenReturn(uuid); + when(job.getEncryptedConfiguration()).thenReturn("config".getBytes()); + when(job.getEncryptionInitialVectorData()).thenReturn(null); + + /* execute */ + assertThatThrownBy(() -> accessToTest.resolveUnEncryptedJobConfiguration(job)).isInstanceOf(PDSEncryptionException.class) + .hasRootCauseInstanceOf(IllegalStateException.class).hasRootCauseMessage("No initial vector data found for PDS job: " + uuid); + } + +} diff --git a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSJobConfigurationValidatorTest.java b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSJobConfigurationValidatorTest.java index 8dccbc8190..8a93f61ea2 100644 --- a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSJobConfigurationValidatorTest.java +++ b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSJobConfigurationValidatorTest.java @@ -96,7 +96,7 @@ void when_a_server_configuration_does_not_contain_product_id_an_exception_is_thr /* execute + test */ PDSNotAcceptableException exception = assertThrows(PDSNotAcceptableException.class, () -> validatorToTest.assertPDSConfigurationValid(config)); String message = exception.getMessage(); - assertTrue(message.contains("does not support product identifier")); + assertTrue(message.contains("does not encryptionSupport product identifier")); } @Test diff --git a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSJobRepositoryDBTest.java b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSJobRepositoryDBTest.java index b616d56f97..264f536276 100644 --- a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSJobRepositoryDBTest.java +++ b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSJobRepositoryDBTest.java @@ -495,7 +495,8 @@ private PDSJob createJob(PDSJobStatusState state, int minutes, String serverId) // necessary because must be not null job.created = LocalDateTime.of(2020, 06, 24, 13, 55, 01).minusMinutes(minutes); job.owner = "owner"; - job.jsonConfiguration = "{}"; + job.encryptedConfiguration = "{}".getBytes(); // simulate encryption + job.encryptionInitialVectorData = "initial-vector".getBytes(); // simulate initial vector job.state = state; /* persist */ diff --git a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSJobRestControllerMockTest.java b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSJobRestControllerMockTest.java index c00fc3a9ad..0fa03b5fa6 100644 --- a/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSJobRestControllerMockTest.java +++ b/sechub-pds/src/test/java/com/mercedesbenz/sechub/pds/job/PDSJobRestControllerMockTest.java @@ -1,19 +1,13 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.pds.job; -import static com.mercedesbenz.sechub.test.PDSTestURLBuilder.https; -import static com.mercedesbenz.sechub.test.TestConstants.SOURCECODE_ZIP; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static com.mercedesbenz.sechub.test.PDSTestURLBuilder.*; +import static com.mercedesbenz.sechub.test.TestConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.util.UUID; @@ -34,6 +28,9 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; +import com.mercedesbenz.sechub.commons.model.JSONConverter; +import com.mercedesbenz.sechub.commons.pds.data.PDSJobStatus; +import com.mercedesbenz.sechub.commons.pds.data.PDSJobStatusState; import com.mercedesbenz.sechub.pds.commons.core.PDSProfiles; import com.mercedesbenz.sechub.pds.security.PDSAPISecurityConfiguration; import com.mercedesbenz.sechub.pds.security.PDSRoleConstants; @@ -114,11 +111,11 @@ public void a_get_job_status_call_calls_status_service_and_returns_status_as_JSO UUID jobUUID = UUID.randomUUID(); PDSJobStatus status = new PDSJobStatus(); - status.created = "created1"; - status.ended = "ended1"; - status.jobUUID = jobUUID; - status.owner = "owner1"; - status.state = "state1"; + status.setCreated("created1"); + status.setEnded("ended1"); + status.setJobUUID(jobUUID); + status.setOwner("owner1"); + status.setState(PDSJobStatusState.RUNNING); when(mockedJobStatusService.getJobStatus(jobUUID)).thenReturn(status); @@ -128,7 +125,7 @@ public void a_get_job_status_call_calls_status_service_and_returns_status_as_JSO get(https(PORT_USED).buildGetJobStatus(jobUUID)) ). andExpect(status().isOk()). - andExpect(content().json(status.toJSON(),true) + andExpect(content().json(JSONConverter.get().toJSON(status),true) ); /* @formatter:on */ diff --git a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSResilienceConsultant.java b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSResilienceConsultant.java index 630120b1cf..d610a87f50 100644 --- a/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSResilienceConsultant.java +++ b/sechub-scan-product-pds/src/main/java/com/mercedesbenz/sechub/domain/scan/product/pds/PDSResilienceConsultant.java @@ -9,6 +9,7 @@ import org.springframework.stereotype.Component; import org.springframework.web.client.HttpClientErrorException; +import com.mercedesbenz.sechub.adapter.pds.PDSEncryptionOutOfSyncException; import com.mercedesbenz.sechub.commons.core.resilience.ResilienceConsultant; import com.mercedesbenz.sechub.commons.core.resilience.ResilienceContext; import com.mercedesbenz.sechub.commons.core.resilience.ResilienceProposal; @@ -20,11 +21,23 @@ public class PDSResilienceConsultant implements ResilienceConsultant { private static final Logger LOG = LoggerFactory.getLogger(PDSResilienceConsultant.class); + private static final int DEFAULT_ENCRYPTION_OUT_OF_SYNC_RETRY_MAX = 3; + private static final int DEFAULT_ENCRYPTION_OUT_OF_SYNC_RETRY_TIMEOUT_MILLISECONDS = 2000; + private static final int DEFAULT_BADREQUEST_RETRY_MAX = 3; private static final int DEFAULT_BADREQUEST_RETRY_TIMEOUT_MILLISECONDS = 2000; + private static final int DEFAULT_SERVERERROR_RETRY_MAX = 1; private static final int DEFAULT_SERVERERROR_RETRY_TIMEOUT_MILLISECONDS = 5000; + @Value("${sechub.adapter.pds.resilience.encryption-out-of-sync.retry.max:" + DEFAULT_ENCRYPTION_OUT_OF_SYNC_RETRY_MAX + "}") + @MustBeDocumented("Amount of retries done when a PDS encryption out of sync problem happens") + private int encryptionOutOfSyncMaxRetries = DEFAULT_ENCRYPTION_OUT_OF_SYNC_RETRY_MAX; + + @Value("${sechub.adapter.pds.resilience.encryption-out-of-sync.retry.wait:" + DEFAULT_ENCRYPTION_OUT_OF_SYNC_RETRY_TIMEOUT_MILLISECONDS + "}") + @MustBeDocumented("Time to wait until retry is done when a PDS encryption out of sync problem happens") + private int encryptionOutOfSyncMRetryTimeToWaitInMilliseconds = DEFAULT_ENCRYPTION_OUT_OF_SYNC_RETRY_TIMEOUT_MILLISECONDS; + @Value("${sechub.adapter.checkmarx.resilience.badrequest.retry.max:" + DEFAULT_BADREQUEST_RETRY_MAX + "}") @MustBeDocumented("Amount of retries done when a 400 bad request happened on Checkmarx server") private int badRequestMaxRetries = DEFAULT_BADREQUEST_RETRY_MAX; @@ -45,16 +58,22 @@ public class PDSResilienceConsultant implements ResilienceConsultant { public ResilienceProposal consultFor(ResilienceContext context) { Objects.requireNonNull(context); Throwable rootCause = StacktraceUtil.findRootCause(context.getCurrentError()); + if (rootCause instanceof PDSEncryptionOutOfSyncException) { + LOG.info("Propose retry for PDS encryption out of sync"); + return new SimpleRetryResilienceProposal("Encryption out of sync handling", encryptionOutOfSyncMaxRetries, + encryptionOutOfSyncMRetryTimeToWaitInMilliseconds); + } + if (rootCause instanceof HttpClientErrorException) { HttpClientErrorException hce = (HttpClientErrorException) rootCause; - int statusCode = hce.getRawStatusCode(); + int statusCode = hce.getStatusCode().value(); if (statusCode == 400) { /* * BAD request - this can happen for same project scans put to queue because * there can a CHECKMARX server error happen */ LOG.info("Propose retry for bad request"); - return new SimpleRetryResilienceProposal("checkmarx bad request handling", badRequestMaxRetries, badRequestRetryTimeToWaitInMilliseconds); + return new SimpleRetryResilienceProposal("bad request handling", badRequestMaxRetries, badRequestRetryTimeToWaitInMilliseconds); } else if (statusCode == 500) { /* diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ScanService.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ScanService.java index efed688e84..266412acf0 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ScanService.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/ScanService.java @@ -186,7 +186,7 @@ private SecHubExecutionContext createExecutionContext(DomainMessage message) thr UUID sechubJobUUID = message.get(SECHUB_JOB_UUID); String executedBy = message.get(EXECUTED_BY); - SecHubConfiguration configuration = message.get(SECHUB_CONFIG); + SecHubConfiguration configuration = message.get(SECHUB_UNENCRYPTED_CONFIG); if (configuration == null) { throw new IllegalStateException("SecHubConfiguration not found in message - so cannot execute!"); } diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/log/ProjectScanLog.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/log/ProjectScanLog.java index 5421d14a78..bb2a7f485b 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/log/ProjectScanLog.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/log/ProjectScanLog.java @@ -1,13 +1,11 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.domain.scan.log; -import java.sql.Types; import java.time.LocalDateTime; import java.util.Objects; import java.util.UUID; import org.hibernate.annotations.GenericGenerator; -import org.hibernate.annotations.JdbcTypeCode; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; @@ -44,7 +42,6 @@ public class ProjectScanLog { public static final String COLUMN_PROJECT_ID = "PROJECT_ID"; public static final String COLUMN_EXECUTED_BY = "EXECUTED_BY"; public static final String COLUMN_SECHUB_JOB_UUID = "SECHUB_JOB_UUID"; - public static final String COLUMN_CONFIG = "CONFIG"; public static final String COLUMN_STATUS = "STATUS"; public static final String COLUMN_STARTED = "STARTED"; @@ -83,10 +80,6 @@ public class ProjectScanLog { @Column(name = COLUMN_SECHUB_JOB_UUID, nullable = false, columnDefinition = "UUID") UUID sechubJobUUID; - @JdbcTypeCode(Types.LONGVARCHAR) - @Column(name = COLUMN_CONFIG) - String config; - @Column(name = COLUMN_STATUS) String status; @@ -108,11 +101,10 @@ public class ProjectScanLog { // jpa only } - public ProjectScanLog(String projectId, UUID sechubJobUUID, String executedBy, String config) { + public ProjectScanLog(String projectId, UUID sechubJobUUID, String executedBy) { this.projectId = projectId; this.sechubJobUUID = sechubJobUUID; this.executedBy = executedBy; - this.config = config; this.started = LocalDateTime.now(); } @@ -121,10 +113,6 @@ public void setStatus(String status) { this.status = status; } - public String getConfig() { - return config; - } - public LocalDateTime getStarted() { return started; } @@ -151,7 +139,7 @@ public String getExecutedBy() { @Override public int hashCode() { - return Objects.hash(config, ended, executedBy, projectId, sechubJobUUID, started, uUID, version); + return Objects.hash(ended, executedBy, projectId, sechubJobUUID, started, uUID, version); } @Override @@ -163,15 +151,15 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) return false; ProjectScanLog other = (ProjectScanLog) obj; - return Objects.equals(config, other.config) && Objects.equals(ended, other.ended) && Objects.equals(executedBy, other.executedBy) - && Objects.equals(projectId, other.projectId) && Objects.equals(sechubJobUUID, other.sechubJobUUID) && Objects.equals(started, other.started) - && Objects.equals(uUID, other.uUID) && Objects.equals(version, other.version); + return Objects.equals(ended, other.ended) && Objects.equals(executedBy, other.executedBy) && Objects.equals(projectId, other.projectId) + && Objects.equals(sechubJobUUID, other.sechubJobUUID) && Objects.equals(started, other.started) && Objects.equals(uUID, other.uUID) + && Objects.equals(version, other.version); } @Override public String toString() { return "ProjectScanLog [\nuUID=" + uUID + ", \nexecutedBy=" + executedBy + ", \nprojectId=" + projectId + ", \nsechubJobUUID=" + sechubJobUUID - + ", \nstatus=" + status + ", \nstarted=" + started + ", \nended=" + ended + ", \nconfig=" + config + "\n]"; + + ", \nstatus=" + status + ", \nstarted=" + started + ", \nended=" + ended + "\n]"; } } diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/log/ProjectScanLogService.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/log/ProjectScanLogService.java index 38d5476e28..26ace64bd5 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/log/ProjectScanLogService.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/log/ProjectScanLogService.java @@ -32,10 +32,9 @@ public class ProjectScanLogService { public UUID logScanStarted(SecHubExecutionContext context) { String projectId = context.getConfiguration().getProjectId(); UUID secHubJobUUID = context.getSechubJobUUID(); - String config = context.getConfiguration().toJSON(); String executedBy = context.getExecutedBy(); - ProjectScanLog log = new ProjectScanLog(projectId, secHubJobUUID, executedBy, config); + ProjectScanLog log = new ProjectScanLog(projectId, secHubJobUUID, executedBy); log.setStatus(ProjectScanLog.STATUS_STARTED); ProjectScanLog persistedLog = repository.save(log); return persistedLog.getUUID(); diff --git a/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/ScanServiceTest.java b/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/ScanServiceTest.java index 852b67cb43..fc181cc6c6 100644 --- a/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/ScanServiceTest.java +++ b/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/ScanServiceTest.java @@ -257,7 +257,7 @@ public void event_handling_works_as_expected_and_SCAN_DONE_is_returned_as_result public void event_handling_FAILED_when_configuration_is_not_set() { /* prepare */ DomainMessage request = prepareValidRequest(); - request.set(MessageDataKeys.SECHUB_CONFIG, null); + request.set(MessageDataKeys.SECHUB_UNENCRYPTED_CONFIG, null); /* execute */ DomainMessageSynchronousResult result = simulateEventSend(request, serviceToTest); @@ -329,7 +329,7 @@ private DomainMessage prepareRequest(SecHubConfiguration configMin) { DomainMessage request = new DomainMessage(MessageID.START_SCAN); request.set(MessageDataKeys.SECHUB_JOB_UUID, SECHUB_JOB_UUID); request.set(MessageDataKeys.SECHUB_EXECUTION_UUID, EXECUTION_UUID); - request.set(MessageDataKeys.SECHUB_CONFIG, configMin); + request.set(MessageDataKeys.SECHUB_UNENCRYPTED_CONFIG, configMin); return request; } diff --git a/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/admin/FullScanDataToZipOutputSupportTest.java b/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/admin/FullScanDataToZipOutputSupportTest.java index dd6f18ec79..c7c40bc2ae 100644 --- a/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/admin/FullScanDataToZipOutputSupportTest.java +++ b/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/admin/FullScanDataToZipOutputSupportTest.java @@ -25,6 +25,8 @@ public class FullScanDataToZipOutputSupportTest { + private static final String EXECUTOR1 = "executor1"; + private static final String PROJECT1 = "project1"; private FullScanDataToZipOutputSupport supportToTest; private UUID sechubJobUUID; @@ -59,7 +61,9 @@ public void writeScanDataContainsDataAsExpected() throws Exception { assertTrue(data1.content.contains("OK")); assertTrue(data2.content.contains("NOT-OK")); - assertTrue(data3.content.contains("'heavy'")); + assertTrue(data3.content.contains("sechubJobUUID=" + sechubJobUUID)); + assertTrue(data3.content.contains("projectId=project1")); + assertTrue(data3.content.contains("executedBy=" + EXECUTOR1)); assertTrue(data4.content.contains("metadata")); assertTrue(data4.content.contains("product1")); @@ -126,7 +130,7 @@ private FullScanData createFullScanDataTwoProductsOneLogEntry() { fullScanData.allScanData.add(data1); fullScanData.allScanData.add(data2); - ProjectScanLog log1 = new ProjectScanLog("project1", sechubJobUUID, "executor1", "{'config':'heavy'}"); + ProjectScanLog log1 = new ProjectScanLog(PROJECT1, sechubJobUUID, EXECUTOR1); fullScanData.allScanLogs.add(log1); return fullScanData; } diff --git a/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/log/ProjectScanLogRepositoryDBTest.java b/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/log/ProjectScanLogRepositoryDBTest.java index 661f77613d..dbe8e03e20 100644 --- a/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/log/ProjectScanLogRepositoryDBTest.java +++ b/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/log/ProjectScanLogRepositoryDBTest.java @@ -256,7 +256,7 @@ private ProjectScanLog create(LocalDateTime since) { } private ProjectScanLog createNewProjectScanLog(String projectId) { - return new ProjectScanLog(projectId, UUID.randomUUID(), "testuser", "{}"); + return new ProjectScanLog(projectId, UUID.randomUUID(), "testuser"); } @TestConfiguration diff --git a/sechub-schedule/build.gradle b/sechub-schedule/build.gradle index 8428acda80..4dbe2c0cdf 100644 --- a/sechub-schedule/build.gradle +++ b/sechub-schedule/build.gradle @@ -12,6 +12,7 @@ dependencies { implementation(library.apache_commons_fileupload2_jakarta) implementation project(':sechub-shared-kernel') + implementation project(':sechub-commons-encryption') testImplementation project(':sechub-testframework') testImplementation project(':sechub-commons-model-testframework') diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/IntegrationTestSchedulerRestController.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/IntegrationTestSchedulerRestController.java index 8a1209e169..e5ad91ea10 100644 --- a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/IntegrationTestSchedulerRestController.java +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/IntegrationTestSchedulerRestController.java @@ -1,11 +1,13 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.domain.schedule; +import java.util.Optional; import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -13,6 +15,9 @@ import com.mercedesbenz.sechub.domain.schedule.access.ScheduleAccessCountService; import com.mercedesbenz.sechub.domain.schedule.config.SchedulerConfigService; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleCipherPoolCleanupService; +import com.mercedesbenz.sechub.domain.schedule.job.ScheduleSecHubJob; +import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobRepository; import com.mercedesbenz.sechub.domain.schedule.strategy.SchedulerStrategyFactory; import com.mercedesbenz.sechub.sharedkernel.APIConstants; import com.mercedesbenz.sechub.sharedkernel.Profiles; @@ -40,12 +45,24 @@ public class IntegrationTestSchedulerRestController { @Autowired private SchedulerConfigService scheduleConfigService; + @Autowired + private SecHubJobRepository jobRepository; + + @Autowired + private ScheduleCipherPoolCleanupService scheduleCipherPoolCleanupService; + @RequestMapping(path = APIConstants.API_ANONYMOUS + "integrationtest/autocleanup/inspection/schedule/days", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE }) public long fetchScheduleAutoCleanupConfiguredDays() { return scheduleConfigService.getAutoCleanupInDays(); } + @RequestMapping(path = APIConstants.API_ANONYMOUS + "integrationtest/schedule/cipher-pool-data/cleanup", method = RequestMethod.PUT, produces = { + MediaType.APPLICATION_JSON_VALUE }) + public void startScheduleAutoCleanupDirectlyForTesting() { + scheduleCipherPoolCleanupService.cleanupCipherPoolDataIfNecessaryAndPossible(); + } + @RequestMapping(path = APIConstants.API_ANONYMOUS + "integrationtest/project/{projectId}/schedule/access/count", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE }) public long countProjectAccess(@PathVariable("projectId") String projectId) { @@ -61,7 +78,6 @@ public void deleteWaitingJobs() { @RequestMapping(path = APIConstants.API_ANONYMOUS + "integrationtest/schedule/revert/job/{sechubJobUUID}/still-running", method = RequestMethod.PUT, produces = { MediaType.APPLICATION_JSON_VALUE }) public void revertJobAsStillRunning(@PathVariable("sechubJobUUID") UUID sechubJobUUID) { - ; integrationTestSchedulerService.revertJobAsStillRunning(sechubJobUUID); } @@ -69,7 +85,6 @@ public void revertJobAsStillRunning(@PathVariable("sechubJobUUID") UUID sechubJo + "integrationtest/schedule/revert/job/{sechubJobUUID}/still-not-approved", method = RequestMethod.PUT, produces = { MediaType.APPLICATION_JSON_VALUE }) public void revertJobAsStillNotApproved(@PathVariable("sechubJobUUID") UUID sechubJobUUID) { - ; integrationTestSchedulerService.revertJobAsStillNotApproved(sechubJobUUID); } @@ -79,4 +94,15 @@ public void setSchedulerStrategy(@PathVariable("strategyId") String strategyId) schedulerStrategyFactory.setStrategyIdentifier(strategyId); } + @RequestMapping(path = APIConstants.API_ANONYMOUS + + "integrationtest/schedule/encryption-pool-id/job/{sechubJobUUID}", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE }) + @Transactional + public Long fetchEncryptionPoolIdForSecHubJob(@PathVariable("sechubJobUUID") UUID sechubJobUUID) { + Optional job = jobRepository.findById(sechubJobUUID); + if (job.isEmpty()) { + throw new IllegalArgumentException("SecHub job: " + sechubJobUUID + " not found!"); + } + return job.get().getEncryptionCipherPoolId(); + } + } diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/IntegrationTestSchedulerService.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/IntegrationTestSchedulerService.java index 74bf15dd12..a53ce32135 100644 --- a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/IntegrationTestSchedulerService.java +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/IntegrationTestSchedulerService.java @@ -40,7 +40,7 @@ public class IntegrationTestSchedulerService { */ public void deleteWaitingJobs() { /* - * we do not add the query to the repository, because it is not used in + * we do not add the query to the poolDataRepository, because it is not used in * production but only for testing */ Query query = entityManager.createQuery(DELETE_WAITING_JOBS_QUERY); diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/ScheduleJobLauncherService.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/ScheduleJobLauncherService.java index bea8cf14bb..bbf78acded 100644 --- a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/ScheduleJobLauncherService.java +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/ScheduleJobLauncherService.java @@ -52,17 +52,16 @@ public void executeJob(ScheduleSecHubJob secHubJob) { executor.execute(secHubJob); /* send domain event */ - sendJobStarted(secHubJob.getProjectId(), secHubJobUUID, secHubJob.getJsonConfiguration(), secHubJob.getOwner()); + sendJobStarted(secHubJob.getProjectId(), secHubJobUUID, secHubJob.getOwner()); } @IsSendingAsyncMessage(MessageID.JOB_STARTED) - private void sendJobStarted(String projectId, UUID jobUUID, String configuration, String owner) { + private void sendJobStarted(String projectId, UUID jobUUID, String owner) { DomainMessage request = new DomainMessage(MessageID.JOB_STARTED); JobMessage message = new JobMessage(); message.setProjectId(projectId); message.setJobUUID(jobUUID); - message.setConfiguration(configuration); message.setOwner(owner); message.setSince(LocalDateTime.now()); diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/ScheduleMessageHandler.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/ScheduleMessageHandler.java index be653f5c45..fdd91bf9ef 100644 --- a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/ScheduleMessageHandler.java +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/ScheduleMessageHandler.java @@ -15,10 +15,13 @@ import com.mercedesbenz.sechub.domain.schedule.access.ScheduleRevokeUserAccessFromProjectService; import com.mercedesbenz.sechub.domain.schedule.config.SchedulerConfigService; import com.mercedesbenz.sechub.domain.schedule.config.SchedulerProjectConfigService; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionRotationService; +import com.mercedesbenz.sechub.domain.schedule.job.ScheduleSecHubJobEncryptionUpdateService; import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobTransactionService; import com.mercedesbenz.sechub.domain.schedule.status.SchedulerStatusService; import com.mercedesbenz.sechub.domain.schedule.whitelist.ProjectWhiteListUpdateService; import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionData; import com.mercedesbenz.sechub.sharedkernel.messaging.AdministrationConfigMessage; import com.mercedesbenz.sechub.sharedkernel.messaging.AsynchronMessageHandler; import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessage; @@ -69,10 +72,17 @@ public class ScheduleMessageHandler implements AsynchronMessageHandler { @Autowired SecHubJobTransactionService jobTransactionService; + @Autowired + ScheduleEncryptionRotationService encryptionRotatonService; + + @Autowired + ScheduleSecHubJobEncryptionUpdateService encryptionUpdateService; + @Override public void receiveAsyncMessage(DomainMessage request) { + LOG.debug("received asynchronous domain request: {}", request); + MessageID messageId = request.getMessageId(); - LOG.debug("received domain request: {}", request); switch (messageId) { case USER_ADDED_TO_PROJECT: @@ -120,6 +130,12 @@ public void receiveAsyncMessage(DomainMessage request) { case PRODUCT_EXECUTOR_CANCEL_OPERATIONS_DONE: handleProductExecutorCancelOperationsDone(request); break; + case START_ENCRYPTION_ROTATION: + handleEncryptionRotation(request); + break; + case SCHEDULE_ENCRYPTION_POOL_INITIALIZED: + handleEncryptionPoolInitialized(request); + break; default: throw new IllegalStateException("unhandled message id:" + messageId); } @@ -230,6 +246,19 @@ private void handleProjectDeleted(DomainMessage request) { projectConfigService.deleteProjectConfiguration(projectId); } + @IsReceivingAsyncMessage(MessageID.START_ENCRYPTION_ROTATION) + private void handleEncryptionRotation(DomainMessage request) { + SecHubEncryptionData data = request.get(MessageDataKeys.SECHUB_ENCRYPT_ROTATION_DATA); + String executedBy = request.get(MessageDataKeys.EXECUTED_BY); + + encryptionRotatonService.startEncryptionRotation(data, executedBy); + } + + @IsReceivingAsyncMessage(MessageID.SCHEDULE_ENCRYPTION_POOL_INITIALIZED) + private void handleEncryptionPoolInitialized(DomainMessage request) { + encryptionUpdateService.updateEncryptedDataIfNecessary(); + } + private void updateWhiteList(ProjectMessage data) { projectWhiteListUpdateService.update(data.getProjectId(), data.getWhitelist()); } diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/ScheduleShutdownService.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/ScheduleShutdownService.java new file mode 100644 index 0000000000..34efb24c62 --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/ScheduleShutdownService.java @@ -0,0 +1,36 @@ +package com.mercedesbenz.sechub.domain.schedule; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.stereotype.Service; + +@Service +public class ScheduleShutdownService implements ApplicationContextAware { + + private static final Logger LOG = LoggerFactory.getLogger(ScheduleShutdownService.class); + + private ApplicationContext context; + + public void shutdownApplication() { + if (context instanceof ConfigurableApplicationContext) { + LOG.info("Will now trigger shutdown of application context"); + ((ConfigurableApplicationContext) context).close(); + } else { + if (context == null) { + LOG.error("Cannot shutdown application context because context null!"); + } else { + LOG.error("Cannot shutdown application context because wrong context:" + context.getClass()); + } + } + } + + @Override + public void setApplicationContext(ApplicationContext ctx) throws BeansException { + this.context = ctx; + + } +} \ No newline at end of file diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/SchedulerJobBatchTriggerService.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/SchedulerJobBatchTriggerService.java index 409079f820..f64915e849 100644 --- a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/SchedulerJobBatchTriggerService.java +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/SchedulerJobBatchTriggerService.java @@ -48,8 +48,8 @@ public class SchedulerJobBatchTriggerService { @MustBeDocumented("When retry mechanism is enabled by `sechub.config.trigger.nextjob.retries`, and a retry is necessary, " + "this value is used to define the maximum time period in millis which will be waited before retry. " + "Why max value? Because cluster instances seems to be created often on exact same time by kubernetes. " - + "So having here a max value will result in a randomized wait time so cluster members will do " - + "fetch operations time shifted and automatically reduce collisions!") + + "So having here a max value will result in a randomized wait time: means cluster members will do " + + "fetch operations time shifted and this automatically reduces collisions!") @Value("${sechub.config.trigger.nextjob.maxwaitretry:" + DEFAULT_RETRY_MAX_MILLIS + "}") private int markNextJobWaitBeforeRetryMillis = DEFAULT_RETRY_MAX_MILLIS; @@ -61,7 +61,7 @@ public class SchedulerJobBatchTriggerService { @Value("${sechub.config.trigger.nextjob.delay:" + DEFAULT_FIXED_DELAY_MILLIS + "}") private String infoFixedDelay; // here only for logging - used in scheduler annotation as well! - @MustBeDocumented("When enabled each trigger will do an healtching by monitoring service. If system has too much CPU load or uses too much memory, the trigger will not execute until memory and CPU load is at normal level!") + @MustBeDocumented("When enabled each trigger will do an health check by monitoring service. If system has too much CPU load or uses too much memory, the trigger will not execute until memory and CPU load is at normal level!") @Value("${sechub.config.trigger.healthcheck.enabled:" + DEFAULT_HEALTHCHECK_ENABLED + "}") private boolean healthCheckEnabled = DEFAULT_HEALTHCHECK_ENABLED; @@ -143,6 +143,7 @@ public void triggerExecutionOfNextJob() { retryContext.markAsFatalFailure(); } + } while (retryContext.isRetryPossible()); if (!retryContext.isExecutionDone()) { diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/autocleanup/ScheduleAutoCleanupService.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/autocleanup/ScheduleAutoCleanupService.java index 7ab2093e48..695a05fa4c 100644 --- a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/autocleanup/ScheduleAutoCleanupService.java +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/autocleanup/ScheduleAutoCleanupService.java @@ -9,6 +9,7 @@ import org.springframework.stereotype.Service; import com.mercedesbenz.sechub.domain.schedule.config.SchedulerConfigService; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleCipherPoolCleanupService; import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobDataRepository; import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobRepository; import com.mercedesbenz.sechub.sharedkernel.Step; @@ -37,6 +38,9 @@ public class ScheduleAutoCleanupService { @Autowired AutoCleanupResultInspector inspector; + @Autowired + ScheduleCipherPoolCleanupService encryptionPoolCleanupService; + @UseCaseScheduleAutoCleanExecution(@Step(number = 2, name = "Delete old data", description = "deletes old job information")) public void cleanup() { /* calculate */ @@ -61,6 +65,9 @@ public void cleanup() { ); /* @formatter:on */ + /* cleanup encryption */ + encryptionPoolCleanupService.cleanupCipherPoolDataIfNecessaryAndPossible(); + } } diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/batch/SynchronSecHubJobExecutor.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/batch/SynchronSecHubJobExecutor.java index 490c326957..6d06354b2c 100644 --- a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/batch/SynchronSecHubJobExecutor.java +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/batch/SynchronSecHubJobExecutor.java @@ -20,6 +20,7 @@ import com.mercedesbenz.sechub.commons.model.job.ExecutionResult; import com.mercedesbenz.sechub.domain.schedule.UUIDContainer; import com.mercedesbenz.sechub.domain.schedule.job.ScheduleSecHubJob; +import com.mercedesbenz.sechub.domain.schedule.job.SecHubConfigurationModelAccess; import com.mercedesbenz.sechub.sharedkernel.LogConstants; import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessage; import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessageService; @@ -54,6 +55,9 @@ public class SynchronSecHubJobExecutor { @Autowired SecHubJobSafeUpdater secHubJobSafeUpdater; + @Autowired + SecHubConfigurationModelAccess configurationModelAccess; + @IsSendingSyncMessage(MessageID.START_SCAN) public void execute(final ScheduleSecHubJob secHubJob) { Thread scheduleWorkerThread = new Thread(() -> executeInsideThread(secHubJob), SECHUB_SCHEDULE_THREAD_PREFIX + secHubJob.getUUID()); @@ -66,7 +70,7 @@ private void executeInsideThread(final ScheduleSecHubJob secHubJob) { uuids.setSecHubJobUUID(secHubJob.getUUID()); try { - String secHubConfiguration = secHubJob.getJsonConfiguration(); + String secHubConfiguration = configurationModelAccess.resolveUnencryptedConfigurationasJson(secHubJob); /* own thread so MDC.put necessary */ MDC.clear(); @@ -76,18 +80,18 @@ private void executeInsideThread(final ScheduleSecHubJob secHubJob) { LOG.info("Executing sechub job: {}, execution uuid: {}", uuids.getSecHubJobUUIDasString(), uuids.getExecutionUUIDAsString()); - sendJobExecutionStartingEvent(secHubJob, uuids, secHubConfiguration); + sendJobExecutionStartingEvent(secHubJob, uuids); /* we send now a synchronous SCAN event */ - DomainMessage request = new DomainMessage(MessageID.START_SCAN); - request.set(MessageDataKeys.SECHUB_EXECUTION_UUID, uuids.getExecutionUUID()); - request.set(MessageDataKeys.EXECUTED_BY, secHubJob.getOwner()); + DomainMessage startScanRequest = new DomainMessage(MessageID.START_SCAN); + startScanRequest.set(MessageDataKeys.SECHUB_EXECUTION_UUID, uuids.getExecutionUUID()); + startScanRequest.set(MessageDataKeys.EXECUTED_BY, secHubJob.getOwner()); - request.set(MessageDataKeys.SECHUB_JOB_UUID, uuids.getSecHubJobUUID()); - request.set(MessageDataKeys.SECHUB_CONFIG, MessageDataKeys.SECHUB_CONFIG.getProvider().get(secHubConfiguration)); + startScanRequest.set(MessageDataKeys.SECHUB_JOB_UUID, uuids.getSecHubJobUUID()); + startScanRequest.set(MessageDataKeys.SECHUB_UNENCRYPTED_CONFIG, MessageDataKeys.SECHUB_UNENCRYPTED_CONFIG.getProvider().get(secHubConfiguration)); /* wait for scan event result - synchron */ - DomainMessageSynchronousResult response = messageService.sendSynchron(request); + DomainMessageSynchronousResult response = messageService.sendSynchron(startScanRequest); updateSecHubJob(uuids, response); @@ -106,7 +110,7 @@ private void executeInsideThread(final ScheduleSecHubJob secHubJob) { } @IsSendingAsyncMessage(MessageID.JOB_EXECUTION_STARTING) - private void sendJobExecutionStartingEvent(final ScheduleSecHubJob secHubJob, UUIDContainer uuids, String secHubConfiguration) { + private void sendJobExecutionStartingEvent(final ScheduleSecHubJob secHubJob, UUIDContainer uuids) { /* we send asynchronous an information event */ DomainMessage jobExecRequest = new DomainMessage(MessageID.JOB_EXECUTION_STARTING); diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolCleanupService.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolCleanupService.java new file mode 100644 index 0000000000..f1e935b0c8 --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolCleanupService.java @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobRepository; +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.usecases.autocleanup.UseCaseScheduleAutoCleanExecution; +import com.mercedesbenz.sechub.sharedkernel.usecases.encryption.UseCaseEncryptionCleanup; + +@Service +public class ScheduleCipherPoolCleanupService { + + private static final String DESCRIPTION = "Removes cipher pool data entries from database which are no longer used by any job"; + + private static final Logger LOG = LoggerFactory.getLogger(ScheduleCipherPoolCleanupService.class); + + @Autowired + ScheduleEncryptionService encryptionService; + + @Autowired + ScheduleLatestCipherPoolDataCalculator latestCipherPoolDataCalculator; + + @Autowired + ScheduleCipherPoolDataRepository poolDataRepository; + + @Autowired + SecHubJobRepository jobRepository; + + @Autowired + SecHubOutdatedEncryptionPoolSupport outdatedEncryptionPoolSupport; + + @UseCaseEncryptionCleanup(@Step(number = 1, name = "Schedule cipher pool data cleanup", description = DESCRIPTION)) + @UseCaseScheduleAutoCleanExecution(@Step(number = 3, name = "Schedule cipher pool data cleanup", description = DESCRIPTION)) + public void cleanupCipherPoolDataIfNecessaryAndPossible() { + LOG.debug("Encryption pool cleanup check"); + + /* check clean up possible */ + if (outdatedEncryptionPoolSupport.isOutdatedEncryptionPoolPossibleInCluster()) { + LOG.debug("It is stil possible to have outdated encryption pools, cannot cleanup"); + return; + } + + /* resolve data */ + List allPoolData = poolDataRepository.findAll(); + if (allPoolData.isEmpty()) { + LOG.warn("No pool data found in database, cannot do encryption cleanup"); + return; + } + ScheduleCipherPoolData latestPoolDataFromDatabase = latestCipherPoolDataCalculator.calculateLatestPoolData(allPoolData); + if (latestPoolDataFromDatabase == null) { + LOG.error("latestPoolDataFromDatabase is null - should never happen at this point! Cannot do encryption cleanup"); + return; + } + + /* + * Skip if this instance is outdated itself (because this instance could still + * create jobs with old ciphers) + */ + if (!Objects.equals(encryptionService.getLatestCipherPoolId(), latestPoolDataFromDatabase.getId())) { + LOG.debug("Encryption pool of this instance is outdated, cannot cleanup"); + return; + } + + startEncryptionCleanup(allPoolData, latestPoolDataFromDatabase); + + } + + private void startEncryptionCleanup(List allPoolData, ScheduleCipherPoolData latestPoolDataFromDatabase) { + LOG.debug("Encryption pool cleanup start"); + + List poolDataToRemove = calculatePoolDataToRemove(allPoolData, latestPoolDataFromDatabase); + if (poolDataToRemove.isEmpty()) { + LOG.debug("Found no pool data to remove"); + return; + } + + LOG.info("Found {} pool entries to remove - start deletetion process", poolDataToRemove.size()); + + for (ScheduleCipherPoolData poolData : poolDataToRemove) { + + LOG.info("Start delete of encryption pool entry: id='{}', algorithm='{}', pwdSourceType='{}', pwdSourceData='{}', created='{}', createdFrom='{}' ", + poolData.getId(), poolData.getAlgorithm(), poolData.getPasswordSourceType(), poolData.getPasswordSourceData(), poolData.getCreated(), + poolData.getCreatedFrom()); + + poolDataRepository.delete(poolData); + } + } + + private List calculatePoolDataToRemove(List allPoolData, + ScheduleCipherPoolData latestPoolDataFromDatabase) { + + List allUsedEncryptionPoolIds = jobRepository.collectAllUsedEncryptionPoolIdsInsideJobs(); + + List poolDataToRemove = new ArrayList<>(); + + for (ScheduleCipherPoolData poolData : allPoolData) { + boolean mustBeKept = latestPoolDataFromDatabase.equals(poolData); + mustBeKept = mustBeKept || allUsedEncryptionPoolIds.contains(poolData.getId()); + + if (mustBeKept) { + continue; + } + poolDataToRemove.add(poolData); + } + return poolDataToRemove; + } + +} diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolData.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolData.java new file mode 100644 index 0000000000..093ec0b175 --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolData.java @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import static jakarta.persistence.EnumType.*; + +import java.time.LocalDateTime; + +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherAlgorithm; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherPasswordSourceType; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +/** + * PersistentCipher pool data table for domain 'schedule' inside database. + * Contains information about how existing schedule job entries are encrypted. + * + * @author Albert Tregnaghi + * + */ +@Entity +@Table(name = ScheduleCipherPoolData.TABLE_NAME) +public class ScheduleCipherPoolData { + + /* +-----------------------------------------------------------------------+ */ + /* +............................ SQL ......................................+ */ + /* +-----------------------------------------------------------------------+ */ + public static final String TABLE_NAME = "SCHEDULE_CIPHER_POOL_DATA"; + + public static final String COLUMN_ID = "POOL_ID"; + + public static final String COLUMN_ALGORITHM = "POOL_ALGORITHM"; + + public static final String COLUMN_PWD_SOURCE_TYPE = "POOL_PWD_SRC_TYPE"; + + public static final String COLUMN_PWD_SOURCE_DATA = "POOL_PWD_SRC_DATA"; + + public static final String COLUMN_TEST_TEXT = "POOL_TEST_TEXT"; + public static final String COLUMN_TEST_INITIAL_VECTOR = "POOL_TEST_INITIAL_VECTOR"; + public static final String COLUMN_TEST_ENCRYPTED = "POOL_TEST_ENCRYPTED"; + + public static final String COLUMN_CREATION_TIMESTAMP = "POOL_CREATION_TIMESTAMP"; + public static final String COLUMN_CREATED_FROM = "POOL_CREATED_FROM"; + + /* +-----------------------------------------------------------------------+ */ + /* +............................ JPQL .....................................+ */ + /* +-----------------------------------------------------------------------+ */ + public static final String CLASS_NAME = ScheduleCipherPoolData.class.getSimpleName(); + + public static final String PROPERTY_ID = "id"; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = COLUMN_ID, unique = true, nullable = false) + Long id; + + @Enumerated(STRING) + @Column(name = COLUMN_ALGORITHM, nullable = false) + SecHubCipherAlgorithm algorithm; + + @Enumerated(STRING) + @Column(name = COLUMN_PWD_SOURCE_TYPE, nullable = false) + SecHubCipherPasswordSourceType secHubCipherPasswordSourceType; + + @Column(name = COLUMN_PWD_SOURCE_DATA, nullable = true) + String passwordSourceData; + + @Column(name = COLUMN_TEST_TEXT, nullable = false) + String testText; + + @Column(name = COLUMN_TEST_INITIAL_VECTOR, nullable = true) + byte[] testInitialVector; + + @Column(name = COLUMN_TEST_ENCRYPTED, nullable = false) + byte[] testEncrypted; + + @Column(name = COLUMN_CREATION_TIMESTAMP, nullable = false) // remark: we setup hibernate to use UTC settings - see + LocalDateTime created; + + @Column(name = COLUMN_CREATED_FROM, nullable = true) + String createdFrom; + + @Version + @Column(name = "VERSION") + Integer version; + + public Long getId() { + return id; + } + + public SecHubCipherAlgorithm getAlgorithm() { + return algorithm; + } + + public SecHubCipherPasswordSourceType getPasswordSourceType() { + return secHubCipherPasswordSourceType; + } + + public String getPasswordSourceData() { + return passwordSourceData; + } + + public String getTestText() { + return testText; + } + + public byte[] getTestInitialVector() { + return testInitialVector; + } + + public byte[] getTestEncrypted() { + return testEncrypted; + } + + public LocalDateTime getCreated() { + return created; + } + + public String getCreatedFrom() { + return createdFrom; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ScheduleCipherPoolData other = (ScheduleCipherPoolData) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) { + return false; + } + return true; + } + +} \ No newline at end of file diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataProvider.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataProvider.java new file mode 100644 index 0000000000..ced866b260 --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataProvider.java @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherAlgorithm; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherPasswordSourceType; + +/** + * Provides cipher pool data + * + * @author Albert Tregnaghi + * + */ +@Component +public class ScheduleCipherPoolDataProvider { + + private static final Logger LOG = LoggerFactory.getLogger(ScheduleCipherPoolDataProvider.class); + + @Autowired + ScheduleCipherPoolDataRepository repository; + + /** + * Resolves all existing cipher pool data from database. If none is available, a + * fallback will be persisted additionally. + * + * @return list of cipher pool data, never empty or null + */ + public List ensurePoolDataAvailable() { + List allPoolDataEntries = repository.findAll(); + + if (allPoolDataEntries.isEmpty()) { + + ScheduleCipherPoolData fallbackEntry = createFallback(); + repository.save(fallbackEntry); + + LOG.warn( + "No pool data entry found - created and stored fallback with algorithm: {}. Remark: This may only happen inside tests! Real server have default initialized by SQL scripts!", + fallbackEntry.getAlgorithm()); + allPoolDataEntries = List.of(fallbackEntry); + } + + if (allPoolDataEntries.isEmpty()) { + throw new IllegalStateException("Found no pool data entry - should never happen"); + } + return allPoolDataEntries; + } + + private ScheduleCipherPoolData createFallback() { + ScheduleCipherPoolData fallbackEntry = new ScheduleCipherPoolData(); + fallbackEntry.id = Long.valueOf(0); + fallbackEntry.secHubCipherPasswordSourceType = SecHubCipherPasswordSourceType.NONE; + fallbackEntry.testText = "fallback"; + fallbackEntry.testEncrypted = "fallback".getBytes(); + fallbackEntry.algorithm = SecHubCipherAlgorithm.NONE; + fallbackEntry.created = LocalDateTime.now(); + fallbackEntry.createdFrom = null; + fallbackEntry.testInitialVector = null; + fallbackEntry.version = Integer.valueOf(0); + return fallbackEntry; + } + + /** + * Checks if given set of pool ids are exactly the same pool ids available + * inside database + * + * @param currentPoolIds + * @return true when same pool ids, false if there is + * any difference + * @throws IllegalArgumentException if current pool id set is null + */ + public boolean isContainingExactlyGivenPoolIds(Set currentPoolIds) { + if (currentPoolIds == null) { + throw new IllegalArgumentException("Current pool ids may not be null!"); + } + + Set poolIdsFromDatabase = repository.fetchAllCipherPoolIds(); + + int dbSize = poolIdsFromDatabase.size(); + int memorySize = currentPoolIds.size(); + + if (dbSize != memorySize) { + LOG.debug("Pool size differs. Memory: {}, Database: {}", memorySize, dbSize); + return false; + } + + boolean allPoolIdsContained = poolIdsFromDatabase.containsAll(currentPoolIds); + + LOG.trace("all pool ids contained: {}, found pool ids in db: {}", allPoolIdsContained, poolIdsFromDatabase); + + return allPoolIdsContained; + } + +} \ No newline at end of file diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataRepository.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataRepository.java new file mode 100644 index 0000000000..e509daa061 --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataRepository.java @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import java.util.Set; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface ScheduleCipherPoolDataRepository extends JpaRepository { + + @Query("select " + ScheduleCipherPoolData.PROPERTY_ID + " from #{#entityName}") + public Set fetchAllCipherPoolIds(); +} diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataTransactionService.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataTransactionService.java new file mode 100644 index 0000000000..3114b6c210 --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataTransactionService.java @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.usecases.encryption.UseCaseAdminStartsEncryptionRotation; + +@Service +public class ScheduleCipherPoolDataTransactionService { + + @Autowired + ScheduleCipherPoolDataRepository repository; + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @UseCaseAdminStartsEncryptionRotation(@Step(number = 4, name = "Service call", description = "Creates new cipher pool entry in database in own transaction")) + public ScheduleCipherPoolData storeInOwnTransaction(ScheduleCipherPoolData poolData) throws ScheduleEncryptionException { + return repository.save(poolData); + } + +} diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionException.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionException.java new file mode 100644 index 0000000000..b87a276494 --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionException.java @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +public class ScheduleEncryptionException extends Exception { + + private static final long serialVersionUID = 1L; + + public ScheduleEncryptionException(String description) { + super(description); + } +} diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionPojoFactory.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionPojoFactory.java new file mode 100644 index 0000000000..26f2c8e16f --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionPojoFactory.java @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +import com.mercedesbenz.sechub.commons.encryption.EncryptionRotator; +import com.mercedesbenz.sechub.commons.encryption.EncryptionSupport; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherFactory; + +/** + * This factory creates some "plain old java" objects and inject them into + * spring boot container. These objects are from libraries where we do not have + * spring annotations inside for automatic injection. + * + * @author Albert Tregnaghi + * + */ +@Component +public class ScheduleEncryptionPojoFactory { + + @Bean + PersistentCipherFactory createPersistentCipherFactory() { + return new PersistentCipherFactory(); + } + + @Bean + EncryptionSupport createEncryptionSupport() { + return new EncryptionSupport(); + } + + @Bean + EncryptionRotator createEncryptionRotator() { + return new EncryptionRotator(); + } +} diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionPool.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionPool.java new file mode 100644 index 0000000000..f8aada586b --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionPool.java @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import com.mercedesbenz.sechub.commons.encryption.PersistentCipher; + +/** + * This class represents the runtime container cache which provides + * {@link PersistentCipher} objects for dedicated pool identifier. + * + * It has another name than {@link ScheduleCipherPoolData} which represents the + * data source from database (but which is only used for creation of this pool). + * To differ between database entities and runtime container object, the names + * differ. + * + * @author Albert Tregnaghi + * + */ +public class ScheduleEncryptionPool { + + Map poolDataIdToPersistentCipherMap = new LinkedHashMap<>(); + + ScheduleEncryptionPool(Map map) { + if (map != null) { + poolDataIdToPersistentCipherMap.putAll(map); + } + } + + /** + * Resolves cipher for given pool id + * + * @param poolId + * @return cipher instance or null if not found inside pool + */ + public PersistentCipher getCipherForPoolId(Long poolId) { + return poolDataIdToPersistentCipherMap.get(poolId); + } + + public Set getAllPoolIds() { + return new HashSet<>(poolDataIdToPersistentCipherMap.keySet()); + } +} \ No newline at end of file diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionPoolFactory.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionPoolFactory.java new file mode 100644 index 0000000000..b92550107a --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionPoolFactory.java @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.mercedesbenz.sechub.commons.encryption.EncryptionSupport; +import com.mercedesbenz.sechub.commons.encryption.InitializationVector; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipher; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherFactory; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherType; +import com.mercedesbenz.sechub.commons.encryption.SecretKeyProvider; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherPasswordSourceType; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubSecretKeyProviderFactory; + +@Component +public class ScheduleEncryptionPoolFactory { + + @Autowired + EncryptionSupport encryptionSupport; + + @Autowired + PersistentCipherFactory cipherFactory; + + @Autowired + SecHubSecretKeyProviderFactory secHubSecretKeyProviderFactory; + + /** + * Creates an encryption pool for the given list of scheduler cipher pool data. + * The factory will create the necessary persistent cipher instances and + * automatically check that the encryption is possible with the current setup. + * + * If it is not possible - e.g. a cipher is created but with the wrong password + * - the factory will throw an ScheduleEncryptionException + * + * @param allPoolDataEntries - may not be null + * @return + */ + public ScheduleEncryptionPool createEncryptionPool(List allPoolDataEntries) throws ScheduleEncryptionException { + if (allPoolDataEntries == null) { + throw new IllegalArgumentException("allPoolDataEntries may never be null!"); + } + + Map map = new LinkedHashMap<>(); + + for (ScheduleCipherPoolData poolData : allPoolDataEntries) { + + PersistentCipher cipher = createCipher(poolData.getAlgorithm().getType(), poolData.getPasswordSourceType(), poolData.getPasswordSourceData()); + + byte[] testEncrypted = poolData.getTestEncrypted(); + InitializationVector initialVector = new InitializationVector(poolData.getTestInitialVector()); + + String decrypted = encryptionSupport.decryptString(testEncrypted, cipher, initialVector); + + String expected = poolData.getTestText(); + + if (!expected.equals(decrypted)) { + /* + * We block server start here because a new server must always be able to handle + * the complete encryption pool. + */ + throw new ScheduleEncryptionException( + "The cipher pool entry with id: %d cannot be handled by the server, because origin test text: '%s' was not retrieved from encrypted test text data, but instead: '%s'" + .formatted(poolData.getId(), expected, decrypted)); + } + + /* Server is able to encrypt/decrypt data with given secret - register it */ + map.put(poolData.getId(), cipher); + + } + return new ScheduleEncryptionPool(map); + } + + private PersistentCipher createCipher(PersistentCipherType cipherType, SecHubCipherPasswordSourceType passwordSourceType, String passwordSourceData) { + + SecretKeyProvider secretKeyProvider = secHubSecretKeyProviderFactory.createSecretKeyProvider(cipherType, passwordSourceType, passwordSourceData); + + return cipherFactory.createCipher(secretKeyProvider, cipherType); + + } +} diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionResult.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionResult.java new file mode 100644 index 0000000000..2df5d71546 --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionResult.java @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import com.mercedesbenz.sechub.commons.encryption.EncryptionResult; +import com.mercedesbenz.sechub.commons.encryption.InitializationVector; + +public class ScheduleEncryptionResult { + private Long cipherPoolId; + private EncryptionResult encryptionResult; + + /** + * Creates encryption result + * + * @param cipherPoolId may never be null + * @param encryptionResult may never be null + * @throws IllegalArgumentException if one of the arguments is null + */ + public ScheduleEncryptionResult(Long cipherPoolId, EncryptionResult encryptionResult) { + if (cipherPoolId == null) { + throw new IllegalArgumentException("cipher pool id may never be null!"); + } + if (encryptionResult == null) { + throw new IllegalArgumentException("encryptionResult may never be null!"); + } + this.cipherPoolId = cipherPoolId; + this.encryptionResult = encryptionResult; + } + + /** + * @return cipher pool id used for encryption, never null + */ + public Long getCipherPoolId() { + return cipherPoolId; + } + + /** + * @return encrypted data or null + */ + public byte[] getEncryptedData() { + return encryptionResult.getEncryptedData(); + } + + /** + * @return initial vector, never null + */ + public InitializationVector getInitialVector() { + return encryptionResult.getInitialVector(); + } + +} \ No newline at end of file diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionRotationService.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionRotationService.java new file mode 100644 index 0000000000..3377472f16 --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionRotationService.java @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionData; +import com.mercedesbenz.sechub.sharedkernel.usecases.encryption.UseCaseAdminStartsEncryptionRotation; + +@Service +public class ScheduleEncryptionRotationService { + + private static final Logger LOG = LoggerFactory.getLogger(ScheduleEncryptionRotationService.class); + + @Autowired + ScheduleCipherPoolDataTransactionService transactionService; + + @Autowired + ScheduleEncryptionService encryptionService; + + @UseCaseAdminStartsEncryptionRotation(@Step(number = 3, name = "Service call", description = "Forces new cipher pool entry creation and triggers encryption service pool refresh")) + public void startEncryptionRotation(SecHubEncryptionData data, String executedBy) { + /* first create new cipher pool entry */ + try { + + LOG.info("start rotation encryption"); + + String testText = UUID.randomUUID().toString(); + + ScheduleCipherPoolData poolData = encryptionService.createInitialCipherPoolData(data, testText); + poolData.createdFrom = executedBy; + + ScheduleCipherPoolData newCreatedEntry = transactionService.storeInOwnTransaction(poolData); + + LOG.info("Created new cipher pool entry with id: {}, algorithm: {}, creation timestamp: {}, created from: {} ", newCreatedEntry.id, + newCreatedEntry.algorithm, newCreatedEntry.created, newCreatedEntry.createdFrom); + } catch (ScheduleEncryptionException e) { + LOG.error("Was not able to create new cipher pool entry!", e); + } + + LOG.info("Trigger refresh of encryption pool in this instance"); + encryptionService.refreshEncryptionPoolAndLatestPoolIdIfNecessary(); + + } + +} diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionService.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionService.java new file mode 100644 index 0000000000..d95aae0083 --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionService.java @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +import com.mercedesbenz.sechub.commons.encryption.EncryptionResult; +import com.mercedesbenz.sechub.commons.encryption.EncryptionRotationSetup; +import com.mercedesbenz.sechub.commons.encryption.EncryptionRotator; +import com.mercedesbenz.sechub.commons.encryption.EncryptionSupport; +import com.mercedesbenz.sechub.commons.encryption.InitializationVector; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipher; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherFactory; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherType; +import com.mercedesbenz.sechub.commons.encryption.SecretKeyProvider; +import com.mercedesbenz.sechub.domain.schedule.ScheduleShutdownService; +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionData; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubSecretKeyProviderFactory; +import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessage; +import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessageService; +import com.mercedesbenz.sechub.sharedkernel.messaging.IsSendingAsyncMessage; +import com.mercedesbenz.sechub.sharedkernel.messaging.MessageID; +import com.mercedesbenz.sechub.sharedkernel.usecases.encryption.UseCaseAdminStartsEncryptionRotation; +import com.mercedesbenz.sechub.sharedkernel.usecases.encryption.UseCaseScheduleEncryptionPoolRefresh; + +@Service +public class ScheduleEncryptionService { + + private static final Logger LOG = LoggerFactory.getLogger(ScheduleEncryptionService.class); + + @Autowired + ScheduleCipherPoolDataProvider poolDataProvider; + + @Autowired + EncryptionSupport encryptionSupport; + + @Autowired + ScheduleEncryptionPoolFactory scheduleEncryptionPoolFactory; + + @Autowired + ScheduleLatestCipherPoolDataCalculator latestCipherPoolDataCalculator; + + @Autowired + EncryptionRotator rotator; + + @Autowired + PersistentCipherFactory cipherFactory; + + @Autowired + SecHubSecretKeyProviderFactory secretKeyProviderFactory; + + @Autowired + SecHubOutdatedEncryptionPoolSupport outdatedEncryptionPoolSupport; + + @Autowired + ScheduleShutdownService shutdownService; + + @Autowired + @Lazy + DomainMessageService domainMessageService; + + Long latestCipherPoolId; + + ScheduleEncryptionPool scheduleEncryptionPool; + + @EventListener(ApplicationStartedEvent.class) + @UseCaseScheduleEncryptionPoolRefresh(@Step(number = 1, next = 3, name = "Init encryption pool", description = "Encryption pool is created on startup")) + void applicationStarted() throws ScheduleEncryptionException { + // If a new started server is not able to handle all ciphers from cipher pool it + // will just + // not start (ensures old and new jobs can always be handled) + initNewEncryptionPoolOrFail(); + } + + /** + * @return latest cipher pool id supported by this service + */ + public Long getLatestCipherPoolId() { + return latestCipherPoolId; + } + + /** + * Tries to refresh encryption pool and latest id. Same like application startup + * - but if it is not possible to create a new encryption pool (e.g. new + * encryption is currently not supported by this server instance) just a warning + * is logged and the old encryption pool is still in use. + */ + @UseCaseScheduleEncryptionPoolRefresh(@Step(number = 2, next = 3, name = "Refresh encryption pool", description = "Encryption pool is refreshed (if necessary)")) + @UseCaseAdminStartsEncryptionRotation(@Step(number = 5, name = "Refresh encryption pool", description = "Encryption pool is refreshed (necessary because pool changed before this method call)")) + public void refreshEncryptionPoolAndLatestPoolIdIfNecessary() { + + if (isStillSameEncryptionPool()) { + LOG.trace("Encryption pool has not changed. No update necessary"); + return; + } + + LOG.info("Encryption pool has changed, start encryption pool recreation."); + + try { + initNewEncryptionPoolOrFail(); + LOG.info("Encryption pool has been recreated sucessfully."); + + } catch (Exception e) { + LOG.warn("Was not able to refresh encryption pool. Reason: {}", e.getMessage()); + + if (outdatedEncryptionPoolSupport.isOutdatedEncryptionStillAllowedOnThisClusterMember()) { + LOG.info("Failing encrytpion pool initialization is still accepted, will use old existing encryption pool."); + } else { + LOG.info("Old (outdated) encryption pool no longer accepted, will trigger shutdown"); + shutdownService.shutdownApplication(); + } + + } + + } + + private boolean isStillSameEncryptionPool() { + return poolDataProvider.isContainingExactlyGivenPoolIds(scheduleEncryptionPool.getAllPoolIds()); + } + + @IsSendingAsyncMessage(MessageID.SCHEDULE_ENCRYPTION_POOL_INITIALIZED) + private void initNewEncryptionPoolOrFail() throws ScheduleEncryptionException { + List allPoolDataEntries = poolDataProvider.ensurePoolDataAvailable(); + + scheduleEncryptionPool = scheduleEncryptionPoolFactory.createEncryptionPool(allPoolDataEntries); + + latestCipherPoolId = latestCipherPoolDataCalculator.calculateLatestPoolId(allPoolDataEntries); + if (latestCipherPoolId == null) { + throw new IllegalStateException("Was not able to determine latest cipher pool data!"); + } + + /* sanity check */ + if (scheduleEncryptionPool.getCipherForPoolId(latestCipherPoolId) == null) { + throw new IllegalStateException("Encryption pool has no entry for latest cipher pool id: %d".formatted(latestCipherPoolId)); + } + + LOG.info("Encryption pool created ({} pool entries)", allPoolDataEntries.size()); + + /* send message about new pool creation */ + DomainMessage message = new DomainMessage(MessageID.SCHEDULE_ENCRYPTION_POOL_INITIALIZED); + domainMessageService.sendAsynchron(message); + } + + public ScheduleEncryptionResult encryptWithLatestCipher(String string) { + return new ScheduleEncryptionResult(latestCipherPoolId, enryptToByteArray(string, latestCipherPoolId)); + } + + public String decryptToString(byte[] encrypted, Long encryptionPoolId, InitializationVector initialVector) { + PersistentCipher cipher = scheduleEncryptionPool.getCipherForPoolId(encryptionPoolId); + if (cipher == null) { + throw new IllegalStateException("There was no registered cipher entry for pool id: %d".formatted(encryptionPoolId)); + } + return encryptionSupport.decryptString(encrypted, cipher, initialVector); + } + + private EncryptionResult enryptToByteArray(String string, Long encryptionPoolId) { + PersistentCipher cipher = scheduleEncryptionPool.getCipherForPoolId(encryptionPoolId); + if (cipher == null) { + throw new IllegalStateException("There was no registered cipher entry for pool id: %d".formatted(encryptionPoolId)); + } + return encryptionSupport.encryptString(string, cipher); + } + + public ScheduleEncryptionResult rotateEncryption(byte[] data, Long oldCipherPoolId, InitializationVector oldInitialVector) + throws ScheduleEncryptionException { + if (latestCipherPoolId == null) { + throw new IllegalStateException("latest cipher pool id is null!"); + } + long newCipherPoolId = latestCipherPoolId; + + PersistentCipher oldCipher = scheduleEncryptionPool.getCipherForPoolId(oldCipherPoolId); + PersistentCipher newCipher = scheduleEncryptionPool.getCipherForPoolId(newCipherPoolId); + + if (oldCipher == null) { + throw new ScheduleEncryptionException("Old cipher not available: " + oldCipherPoolId); + } + if (newCipher == null) { + throw new ScheduleEncryptionException("New cipher not available: " + newCipherPoolId); + } + + InitializationVector newInitialVector = newCipher.createNewInitializationVector(); + + EncryptionRotationSetup rotateSetup = EncryptionRotationSetup.builder().newCipher(newCipher).oldCipher(oldCipher).oldInitialVector(oldInitialVector) + .newInitialVector(newInitialVector).build(); + + byte[] newEncrypted = rotator.rotate(data, rotateSetup); + EncryptionResult res = new EncryptionResult(newEncrypted, newInitialVector); + + return new ScheduleEncryptionResult(newCipherPoolId, res); + } + + /** + * Creates new initial cipher pool data entity. Will automatically check if + * encryption process can be done with this instance and given test text. Pool + * data will contain all relevant data, except information about user which was + * responsible for the new pool data. + * + * @param data encryption data + * @param testText text to be used for initial test + * @return initial pool data ready to be stored, but without setting the creator + * @throws ScheduleEncryptionException if encryption process has any problems + */ + public ScheduleCipherPoolData createInitialCipherPoolData(SecHubEncryptionData data, String testText) throws ScheduleEncryptionException { + + ScheduleCipherPoolData poolData = new ScheduleCipherPoolData(); + poolData.algorithm = data.getAlgorithm(); + poolData.created = LocalDateTime.now(); + poolData.passwordSourceData = data.getPasswordSourceData(); + poolData.secHubCipherPasswordSourceType = data.getPasswordSourceType(); + + poolData.testText = testText; + + PersistentCipherType cipherType = poolData.getAlgorithm().getType(); + SecretKeyProvider secretKey = secretKeyProviderFactory.createSecretKeyProvider(cipherType, poolData.getPasswordSourceType(), + poolData.getPasswordSourceData()); + PersistentCipher tempCipher = cipherFactory.createCipher(secretKey, cipherType); + + EncryptionResult result = encryptionSupport.encryptString(poolData.testText, tempCipher); + poolData.testInitialVector = result.getInitialVector().getInitializationBytes(); + poolData.testEncrypted = result.getEncryptedData(); + + // sanity check + String decrypted = encryptionSupport.decryptString(poolData.testEncrypted, tempCipher, new InitializationVector(poolData.getTestInitialVector())); + if (decrypted == null) { + throw new ScheduleEncryptionException("Was not able to instantiate new cipher pool data, because decrypted value is null!"); + } + if (!decrypted.equals(poolData.testText)) { + throw new ScheduleEncryptionException("Was not able to instantiate new cipher pool data, because decrypted value is not origin test text!"); + } + return poolData; + + } + + public Set getCurrentEncryptionPoolIds() { + return scheduleEncryptionPool.getAllPoolIds(); + } + +} diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionStatusService.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionStatusService.java new file mode 100644 index 0000000000..1ce4f1fde6 --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionStatusService.java @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.mercedesbenz.sechub.commons.model.job.ExecutionState; +import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobRepository; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubDomainEncryptionData; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubDomainEncryptionStatus; +import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessage; +import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessageSynchronousResult; +import com.mercedesbenz.sechub.sharedkernel.messaging.IsRecevingSyncMessage; +import com.mercedesbenz.sechub.sharedkernel.messaging.IsSendingSyncMessageAnswer; +import com.mercedesbenz.sechub.sharedkernel.messaging.MessageDataKeys; +import com.mercedesbenz.sechub.sharedkernel.messaging.MessageID; +import com.mercedesbenz.sechub.sharedkernel.messaging.SynchronMessageHandler; + +@Service +public class ScheduleEncryptionStatusService implements SynchronMessageHandler { + + private static final Logger LOG = LoggerFactory.getLogger(ScheduleEncryptionStatusService.class); + + @Autowired + ScheduleCipherPoolDataRepository poolDataRepository; + + @Autowired + SecHubJobRepository jobRepository; + + @Override + public DomainMessageSynchronousResult receiveSynchronMessage(DomainMessage request) { + LOG.debug("received synchronnous domain request: {}", request); + + MessageID messageId = request.getMessageId(); + + switch (messageId) { + case GET_ENCRYPTION_STATUS_SCHEDULE_DOMAIN: + return handleEncryptionStatusRequest(); + default: + throw new IllegalStateException("unhandled message id:" + messageId); + } + + } + + @IsRecevingSyncMessage(MessageID.GET_ENCRYPTION_STATUS_SCHEDULE_DOMAIN) + @IsSendingSyncMessageAnswer(value = MessageID.RESULT_ENCRYPTION_STATUS_SCHEDULE_DOMAIN, answeringTo = MessageID.GET_ENCRYPTION_STATUS_SCHEDULE_DOMAIN, branchName = "success") + private DomainMessageSynchronousResult handleEncryptionStatusRequest() { + + SecHubDomainEncryptionStatus status = createEncryptionStatus(); + + DomainMessageSynchronousResult result = new DomainMessageSynchronousResult(MessageID.RESULT_ENCRYPTION_STATUS_SCHEDULE_DOMAIN); + result.set(MessageDataKeys.SECHUB_DOMAIN_ENCRYPTION_STATUS, status); + return result; + } + + public SecHubDomainEncryptionStatus createEncryptionStatus() { + SecHubDomainEncryptionStatus status = new SecHubDomainEncryptionStatus(); + status.setName("schedule"); + + List all = poolDataRepository.findAll(); + + for (ScheduleCipherPoolData cipherPoolData : all) { + Long cipherPoolid = cipherPoolData.getId(); + + // initialize + SecHubDomainEncryptionData data = new SecHubDomainEncryptionData(); + data.setId(String.valueOf(cipherPoolid)); + data.setAlgorithm(cipherPoolData.getAlgorithm()); + data.getPasswordSource().setType(cipherPoolData.getPasswordSourceType()); + data.getPasswordSource().setData(cipherPoolData.getPasswordSourceData()); + data.setCreated(cipherPoolData.getCreated()); + data.setCreatedFrom(cipherPoolData.getCreatedFrom()); + + // add usage + for (ExecutionState state : ExecutionState.values()) { + long count = jobRepository.countJobsInExecutionStateAndEncryptedWithPoolId(state, cipherPoolid); + data.getUsage().put("job.state." + state.toString().toLowerCase(), count); + } + status.getData().add(data); + + } + + return status; + } + +} diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleLatestCipherPoolDataCalculator.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleLatestCipherPoolDataCalculator.java new file mode 100644 index 0000000000..af27008e93 --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleLatestCipherPoolDataCalculator.java @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import java.util.List; + +import org.springframework.stereotype.Component; + +@Component +public class ScheduleLatestCipherPoolDataCalculator { + + /** + * Calculates latest id from given list of data entries + * + * @param entries a list of pool data elements + * @return latest id of list, or null of entry list was empty or + * null + */ + public Long calculateLatestPoolId(List entries) { + ScheduleCipherPoolData latestCipherPoolData = calculateLatestPoolData(entries); + if (latestCipherPoolData == null) { + return null; + } + return latestCipherPoolData.getId(); + } + + /** + * Calculates latest entry from given list of data entries + * + * @param entries a list of pool data elements + * @return latest entry of list, or null of entry list was empty or + * null + */ + public ScheduleCipherPoolData calculateLatestPoolData(List entries) { + if (entries == null || entries.isEmpty()) { + return null; + } + + ScheduleCipherPoolData latestCipherPoolData = null; + for (ScheduleCipherPoolData poolData : entries) { + + /* calculate latest */ + if (latestCipherPoolData == null) { + latestCipherPoolData = poolData; + } else { + if (latestCipherPoolData.getCreated().isBefore(poolData.getCreated())) { + latestCipherPoolData = poolData; + } + } + } + return latestCipherPoolData; + } + +} diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleRefreshEncryptionServiceSetupTriggerService.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleRefreshEncryptionServiceSetupTriggerService.java new file mode 100644 index 0000000000..b56064b425 --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleRefreshEncryptionServiceSetupTriggerService.java @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import com.mercedesbenz.sechub.sharedkernel.MustBeDocumented; +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.usecases.encryption.UseCaseScheduleEncryptionPoolRefresh; + +/** + * This service is responsible to trigger periodically refresh checks on the + * encryption service to update encryption pool and latest pool id when + * necessary. + * + * The reason for the periodic check is that we do not want to check for latest + * cipher etc. on every job creation or every encryption rotation - we separated + * the pool refresh and the encryption/re-encryption (which will always use the + * encryption pool and its information). + * + * @author Albert Tregnaghi + * + */ +@Service +public class ScheduleRefreshEncryptionServiceSetupTriggerService { + + private static final int DEFAULT_INITIAL_DELAY_MILLIS = 5 * 1000; // 5 seconds delay + private static final int DEFAULT_FIXED_DELAY_MILLIS = 5 * 60 * 1000; // 5 minutes + + static final String SPRING_VALUE_INITIAL_DELAY_MILLISECONDS = "${sechub.schedule.encryption.refresh.initialdelay:" + DEFAULT_INITIAL_DELAY_MILLIS + "}"; + static final String SPRING_VALUE_FIXED_DELAY_MILLISECONDS = "${sechub.schedule.encryption.refresh.delay:" + DEFAULT_FIXED_DELAY_MILLIS + "}"; + + private static final String DESCRIPTION = "Scheduler instance will check if encryption pool is in sync with the database definitions. If not, the instance will try to create new encryption pool object and provide the new setup."; + + @Autowired + ScheduleEncryptionService encryptionService; + + @MustBeDocumented("Defines the initial and also the fixed delay for the refresh interval. These values are also used for calculation of remaining run time of outdated encrytion pools (when refresh fails)") + @Scheduled(initialDelayString = SPRING_VALUE_INITIAL_DELAY_MILLISECONDS, fixedDelayString = SPRING_VALUE_FIXED_DELAY_MILLISECONDS) + @UseCaseScheduleEncryptionPoolRefresh(@Step(number = 1, name = "Encryption pool data refresh trigger", description = DESCRIPTION)) + public void triggerEncryptionSetupRefresh() { + encryptionService.refreshEncryptionPoolAndLatestPoolIdIfNecessary(); + } +} diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/SecHubOutdatedEncryptionPoolSupport.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/SecHubOutdatedEncryptionPoolSupport.java new file mode 100644 index 0000000000..dfadb00d5a --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/encryption/SecHubOutdatedEncryptionPoolSupport.java @@ -0,0 +1,160 @@ +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import com.mercedesbenz.sechub.sharedkernel.MustBeDocumented; +import com.mercedesbenz.sechub.sharedkernel.SystemTimeProvider; + +@Component +public class SecHubOutdatedEncryptionPoolSupport { + + /* + * This is just an additional time buffer when calculating the maximum refresh + * interval time + */ + private static final int MAX_REFRESH_INTERVAL_ADDITIONAL_TIME_BUFFER_MILLISECONDS = 500; + + @MustBeDocumented("The maximum amount of milliseconds an outdated encryption pool is still accepted in refresh phase") + @Value("${sechub.schedule.encryption.refresh.accept-outdated.milliseconds:10000}") + long acceptOutdatedEncryptionPoolInMilliseconds; + + // documented in ScheduleRefreshEncryptionServiceSetupTriggerService + @Value(ScheduleRefreshEncryptionServiceSetupTriggerService.SPRING_VALUE_FIXED_DELAY_MILLISECONDS) + long encryptionRefreshFixedDelayInMilliseconds; + + // documented in ScheduleRefreshEncryptionServiceSetupTriggerService + @Value(ScheduleRefreshEncryptionServiceSetupTriggerService.SPRING_VALUE_INITIAL_DELAY_MILLISECONDS) + long encryptionRefreshInitialDelayInMilliseconds; + + @Autowired + SystemTimeProvider systemtimeProvider; + + @Autowired + ScheduleCipherPoolDataProvider poolDataProvider; + + @Autowired + ScheduleLatestCipherPoolDataCalculator latestCipherPoolDataCalculator; + + /** + * As long as an outdated encryption pool still needs to run (e.g. while a + * rolling update/ deployment is done with K8s and the old server is still up + * and running and does create jobs...). + * + * The method will check if the maximum allowed time for an outdated encryption + * pool has been reached by following algorithm: + *
      + *
    • the creation time stamp of latest cipher pool data entry from database is + * fetched
    • + *
    • the difference from current time and the creation time is calculated
    • + *
    • if the difference is bigger than the allowed maximum value this method + * returns false, otherwise true
    • + *
    + * + * @return true as long as still acceptable, otherwise + * false + */ + public boolean isOutdatedEncryptionStillAllowedOnThisClusterMember() { + Long latestPoolDataWasCreatedMillisecondsBefore = calculateMillisecondsLatestPoolDataHasBeenCreated(); + if (latestPoolDataWasCreatedMillisecondsBefore == null) { + /* no pool defined, means cannot determine - return false */ + return false; + } + return acceptOutdatedEncryptionPoolInMilliseconds > latestPoolDataWasCreatedMillisecondsBefore; + } + + /** + * This method is used to avoid the risk of race conditions when encryption pool + * entries are still in use but wanted to be removed. + *

    Example-Situation:

    + * + *
    +     * SecHub-Cluster member A: - SECRET_1 - SECRET_2 (new encryption pool entry PA, poolId = 2)
    +     *
    +     * SecHub-Cluster member B: - SECRET_1 (old encryption pool entry PB, poolId = 1)
    +     * 
    + * + * SecHub B creates now a new job which is currently not stored in database but + * with old encryption pool PB (poolId=1). In the mean time cluster member A + * does not find any jobs which using pool id 1 and could delete now the + * encryption pool entity with id 1...
    + *
    + * After this the member B would write the encrypted configuration with poolId=1 + * to database, because the encryption pool is still in memory and its latest + * entry is pool id 1 -> after this member 2 is also new started with only + * SECRET_2 settings.. + * + * --> Now we have a bad situation: we would have a created job encrypted with + * SECRET_1, but no possibility to handle the created job by SecHub any more. + * + * + *

    Problem

    + * + * SecHub does not use a full blown event bus (like KAFKA) but a simple event + * listener approach which works only inside one JVM, we have no possibility to + * send cluster wide events to check there are no longer outdated encryption + * pools used. + * + *

    Solution

    To handle the problem without a full blown event bus, we + * ensure that outdated encryption pools can remain running only a dedicated + * time. The time an outdated encryption pool is accepted is calculated by + * {@link #isOutdatedEncryptionStillAllowedOnThisClusterMember()}. + * + * Means we ask here the question: "Was the maximum outdate time exceeded at the + * last refresh interval?" + * + *
    +     *      Created                     Cluster wrong EP          Cluster wrong EP
    +     *        New      Refresh              possible:  Refresh           possible:
    +     *        Pool Id   Trigger                        Trigger
    +     * +-----------------+--------------------Y-----------+--------------------N-------------------
    +     *         |        Outdated                        Outdated               |
    +     *         |         |                                |                    |
    +     *         |         |----------------------------x   not                  |
    +     *         |            still allowed on cluster      allowed              |
    +     *         |            member                    (max time diff reached)  |
    +     *         |<------------------------------------------------------------->|
    +     *         |  (time from created to now)                                  (now)
    +     *         |
    +     *         |<------------------------------------------>X<-----------------|
    +     *         | (time from created to last refresh trigger)| (now-refresh trigger time)
    +     *                                                      |
    +     *                                                      |
    +     *                                                  timeOnLastRefreshTigger
    +     * 
    + * + * + * @return true when outdated encryption pool is possible in + * cluster, otherwise false + */ + public boolean isOutdatedEncryptionPoolPossibleInCluster() { + Long latestPoolDataWasCreatedMillisecondsBefore = calculateMillisecondsLatestPoolDataHasBeenCreated(); + if (latestPoolDataWasCreatedMillisecondsBefore == null) { + /* no pool defined, means outdated is not possible */ + return false; + } + + long maxRefreshIntervalTimeMillseconds = encryptionRefreshInitialDelayInMilliseconds + encryptionRefreshFixedDelayInMilliseconds + + MAX_REFRESH_INTERVAL_ADDITIONAL_TIME_BUFFER_MILLISECONDS; + + long maxMillisecondsDifferenceToLastRefreshTigger = Math.abs(latestPoolDataWasCreatedMillisecondsBefore - maxRefreshIntervalTimeMillseconds); + + return acceptOutdatedEncryptionPoolInMilliseconds > maxMillisecondsDifferenceToLastRefreshTigger; + } + + private Long calculateMillisecondsLatestPoolDataHasBeenCreated() { + List allAvailablePoolData = poolDataProvider.ensurePoolDataAvailable(); + ScheduleCipherPoolData latest = latestCipherPoolDataCalculator.calculateLatestPoolData(allAvailablePoolData); + if (latest == null) { + return null; + } + LocalDateTime latestCreationTimeStamp = latest.getCreated(); + + return Duration.between(latestCreationTimeStamp, systemtimeProvider.getNow()).toMillis(); + } +} diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/ScheduleSecHubJob.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/ScheduleSecHubJob.java index 049b57deed..3d908c860a 100644 --- a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/ScheduleSecHubJob.java +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/ScheduleSecHubJob.java @@ -48,7 +48,11 @@ public class ScheduleSecHubJob { public static final String COLUMN_STARTED = "STARTED"; public static final String COLUMN_ENDED = "ENDED"; public static final String COLUMN_STATE = "STATE"; - public static final String COLUMN_CONFIGURATION = "CONFIGURATION"; + + public static final String COLUMN_ENCRYPTED_CONFIGURATION = "ENCRYPTED_CONFIGURATION"; + public static final String COLUMN_ENCRYPT_INITIAL_VECTOR = "ENCRYPT_INITIAL_VECTOR"; + public static final String COLUMN_ENCRYPT_POOL_DATA_ID = "ENCRYPT_POOL_DATA_ID"; + public static final String COLUMN_TRAFFIC_LIGHT = "TRAFFIC_LIGHT"; public static final String COLUMN_MODULE_GROUP = "MODULE_GROUP"; @@ -74,6 +78,7 @@ public class ScheduleSecHubJob { public static final String PROPERTY_MESSAGES = "jsonMessages"; public static final String PROPERTY_MODULE_GROUP = "moduleGroup"; public static final String PROPERTY_DATA = "data"; + public static final String PROPERTY_ENCRYPTION_POOL_ID = "encryptionCipherPoolId"; public static final String QUERY_DELETE_JOB_OLDER_THAN = "DELETE FROM ScheduleSecHubJob j WHERE j." + PROPERTY_CREATED + " <:cleanTimeStamp"; @@ -98,8 +103,14 @@ public class ScheduleSecHubJob { @Column(name = COLUMN_ENDED) // remark: we setup hibernate to use UTC settings - see application.properties LocalDateTime ended; - @Column(name = COLUMN_CONFIGURATION) - String jsonConfiguration; + @Column(name = COLUMN_ENCRYPTED_CONFIGURATION) + byte[] encryptedConfiguration; + + @Column(name = COLUMN_ENCRYPT_INITIAL_VECTOR) + byte[] encryptionInitialVectorData; + + @Column(name = COLUMN_ENCRYPT_POOL_DATA_ID) + Long encryptionCipherPoolId; @Enumerated(STRING) @Column(name = COLUMN_STATE, nullable = false) @@ -179,10 +190,6 @@ public LocalDateTime getCreated() { return created; } - public String getJsonConfiguration() { - return jsonConfiguration; - } - public ExecutionState getExecutionState() { return executionState; } @@ -211,6 +218,30 @@ public ModuleGroup getModuleGroup() { return moduleGroup; } + public byte[] getEncryptedConfiguration() { + return encryptedConfiguration; + } + + public void setEncryptedConfiguration(byte[] encryptedConfiguration) { + this.encryptedConfiguration = encryptedConfiguration; + } + + public byte[] getEncryptionInitialVectorData() { + return encryptionInitialVectorData; + } + + public void setEncryptionInitialVectorData(byte[] encryptionInitialVectorData) { + this.encryptionInitialVectorData = encryptionInitialVectorData; + } + + public void setEncryptionCipherPoolId(Long encryptionPoolDataId) { + this.encryptionCipherPoolId = encryptionPoolDataId; + } + + public Long getEncryptionCipherPoolId() { + return encryptionCipherPoolId; + } + @Override public int hashCode() { final int prime = 31; diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/ScheduleSecHubJobEncryptionUpdateService.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/ScheduleSecHubJobEncryptionUpdateService.java new file mode 100644 index 0000000000..afb68bf64d --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/ScheduleSecHubJobEncryptionUpdateService.java @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.job; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.orm.ObjectOptimisticLockingFailureException; +import org.springframework.stereotype.Service; + +import com.mercedesbenz.sechub.commons.encryption.InitializationVector; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionException; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionResult; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionService; +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.usecases.encryption.UseCaseAdminStartsEncryptionRotation; +import com.mercedesbenz.sechub.sharedkernel.usecases.encryption.UseCaseScheduleEncryptionPoolRefresh; +import com.mercedesbenz.sechub.sharedkernel.usecases.encryption.UseCaseScheduleRotateDataEncryption; + +@Service +public class ScheduleSecHubJobEncryptionUpdateService { + + private static final Logger LOG = LoggerFactory.getLogger(ScheduleSecHubJobEncryptionUpdateService.class); + + @Autowired + ScheduleEncryptionService encryptionService; + + @Autowired + SecHubJobTransactionService jobTransactionService; + + @Value("${sechub.schedule.job.encryption.update.blocksize:50}") // 50 per default + int updateBlockSize; + + @Value("${sechub.schedule.job.encryption.update.statuslog.milliseconds:10000}") // every 10 seconds per default + long milliSecondsForNextStatusLog; + + @UseCaseAdminStartsEncryptionRotation(@Step(number = 6, name = "Update encrypted data", description = "Encrypted data is updated (a direct pool refresh was triggered by admin action)")) + @UseCaseScheduleEncryptionPoolRefresh(@Step(number = 3, name = "Update encrypted data", description = "Encrypted data is updated (all other cluster members)")) + @UseCaseScheduleRotateDataEncryption(@Step(number = 1, name = "Update encrypted data", description = "Final update of encrypted job data. Will update all SecHub jobs having a pool id which is lower than latest from encryption pool")) + public void updateEncryptedDataIfNecessary() { + LOG.debug("Start update of encrypted data"); + + try { + + long lastStatusLogTime = 0; + + do { + + Long latestPoolid = encryptionService.getLatestCipherPoolId(); + + long statusLogTimeDifferenceInMilliseconds = System.currentTimeMillis() - lastStatusLogTime; + + if (statusLogTimeDifferenceInMilliseconds > milliSecondsForNextStatusLog) { + long count = jobTransactionService.countCanceledOrEndedJobsWithEncryptionPoolIdLowerThan(latestPoolid); + LOG.info("Found {} jobs which are encrypted with a cipher pool entry lower than: {}", count, latestPoolid); + + lastStatusLogTime = System.currentTimeMillis(); + } + + /* 1. fetch next job (s) which shall be updated */ + List list = null; + try { + list = jobTransactionService.nextCanceledOrEndedJobsWithEncryptionPoolIdLowerThan(latestPoolid, updateBlockSize); + if (list.isEmpty()) { + LOG.debug("No jobs found which must be updated."); + break; + } + + /* 2. for every entry, rotate and save the job afterwards */ + int updatedJobs = 0; + for (ScheduleSecHubJob job : list) { + if (rotateJobDataEncryptionAndStoreFailSafe(job)) { + updatedJobs++; + } + } + LOG.debug("Tried re-encryption of {} jobs, {} were succesful. Block size was: {}", list.size(), updatedJobs, updateBlockSize); + } catch (ObjectOptimisticLockingFailureException e) { + LOG.info("Optmistic lock problem detected - will just retry"); + } + + } while (true); + + } catch (ScheduleEncryptionException e) { + LOG.error("Was not able toupdate encrypted data because of encrpytion problem - will stop complete update. Check cipher setup!", e); + } + LOG.debug("Encrypted data update done"); + + } + + private boolean rotateJobDataEncryptionAndStoreFailSafe(ScheduleSecHubJob job) throws ScheduleEncryptionException { + LOG.trace("rotate job with uuid: {}", job.getUUID()); + + try { + ScheduleEncryptionResult result = encryptionService.rotateEncryption(job.getEncryptedConfiguration(), job.getEncryptionCipherPoolId(), + new InitializationVector(job.getEncryptionInitialVectorData())); + + job.setEncryptedConfiguration(result.getEncryptedData()); + job.setEncryptionCipherPoolId(result.getCipherPoolId()); + job.setEncryptionInitialVectorData(result.getInitialVector().getInitializationBytes()); + + jobTransactionService.saveInOwnTransaction(job); + + return true; + + } catch (OptimisticLockingFailureException lockException) { + LOG.debug("Job encryption for job: {} was not possible because row updated in mean time - either no longer necessary or will be done later again.", + job.getUUID()); + return false; + } + + } + +} diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubConfigurationModelAccess.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubConfigurationModelAccess.java new file mode 100644 index 0000000000..e0c63017c3 --- /dev/null +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubConfigurationModelAccess.java @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.job; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import com.mercedesbenz.sechub.commons.encryption.InitializationVector; +import com.mercedesbenz.sechub.commons.model.JSONConverter; +import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModel; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionService; + +@Service +public class SecHubConfigurationModelAccess { + + private static final Logger LOG = LoggerFactory.getLogger(SecHubConfigurationModelAccess.class); + + @Autowired + @Lazy + ScheduleEncryptionService encryptionService; + + /** + * Resolves SecHub configuration model + * + * @param job + * @return model or null if job contains no encrypted configuration + */ + public SecHubConfigurationModel resolveUnencryptedConfiguration(ScheduleSecHubJob job) { + String json = resolveUnencryptedConfigurationasJson(job); + + SecHubConfigurationModel configuration = JSONConverter.get().fromJSON(SecHubConfigurationModel.class, json); + + return configuration; + } + + /** + * Resolves SecHub configuration model + * + * @param job + * @return model or null if job contains no encrypted configuration + */ + public String resolveUnencryptedConfigurationasJson(ScheduleSecHubJob job) { + if (job == null) { + throw new IllegalArgumentException("job parameter may not be null!"); + } + byte[] encryptedConfiguration = job.getEncryptedConfiguration(); + if (encryptedConfiguration == null) { + LOG.debug("No encrypted sechub configuration found for job: {}!", job.getUUID()); + return null; + } + Long encryptionCipherPoolId = job.getEncryptionCipherPoolId(); + InitializationVector initialVector = new InitializationVector(job.getEncryptionInitialVectorData()); + + String json = encryptionService.decryptToString(job.getEncryptedConfiguration(), encryptionCipherPoolId, initialVector); + + return json; + } +} diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobFactory.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobFactory.java index b04e116171..a5be4f1a64 100644 --- a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobFactory.java +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobFactory.java @@ -16,6 +16,8 @@ import com.mercedesbenz.sechub.commons.model.ModuleGroup; import com.mercedesbenz.sechub.commons.model.ScanType; import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModelSupport; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionResult; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionService; import com.mercedesbenz.sechub.sharedkernel.UserContextService; import com.mercedesbenz.sechub.sharedkernel.configuration.SecHubConfiguration; @@ -30,6 +32,9 @@ public class SecHubJobFactory { @Autowired SecHubConfigurationModelSupport modelSupport; + @Autowired + ScheduleEncryptionService encryptionService; + private static final Logger LOG = LoggerFactory.getLogger(SecHubJobFactory.class); /** @@ -45,10 +50,16 @@ public ScheduleSecHubJob createJob(@Valid SecHubConfiguration configuration) { throw new IllegalStateException("No user logged in - illegal access!"); } + ScheduleEncryptionResult scheduleEncryptionResult = encryptionService.encryptWithLatestCipher(configuration.toJSON()); + ScheduleSecHubJob job = new ScheduleSecHubJob(); try { job.projectId = configuration.getProjectId(); - job.jsonConfiguration = configuration.toJSON(); + + job.encryptedConfiguration = scheduleEncryptionResult.getEncryptedData(); + job.encryptionInitialVectorData = scheduleEncryptionResult.getInitialVector().getInitializationBytes(); + job.encryptionCipherPoolId = scheduleEncryptionResult.getCipherPoolId(); + job.owner = userId; job.created = LocalDateTime.now(); diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobInfoForUserService.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobInfoForUserService.java index c4ad924ea9..42de4cd5d3 100644 --- a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobInfoForUserService.java +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobInfoForUserService.java @@ -17,9 +17,9 @@ import org.springframework.stereotype.Service; import com.mercedesbenz.sechub.commons.model.SecHubConfigurationMetaData; +import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModel; import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModelValidationResult; import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModelValidator; -import com.mercedesbenz.sechub.commons.model.SecHubScanConfiguration; import com.mercedesbenz.sechub.domain.schedule.ScheduleAssertService; import com.mercedesbenz.sechub.sharedkernel.MustBeDocumented; import com.mercedesbenz.sechub.sharedkernel.Step; @@ -55,6 +55,9 @@ public class SecHubJobInfoForUserService { @Autowired SecHubConfigurationModelValidator modelValidator; + @Autowired + SecHubConfigurationModelAccess configurationModelAccess; + @Value("${sechub.project.joblist.size.max:" + DEFAULT_MAXIMUM_LIMIT + "}") @MustBeDocumented("Maximum limit for job information list entries per page") int maximumSize = DEFAULT_MAXIMUM_LIMIT; @@ -189,12 +192,16 @@ private SecHubJobInfoForUserListPage transformToListPage(Page } private void attachJobMetaData(ScheduleSecHubJob job, SecHubJobInfoForUser infoForUser) { - String json = job.getJsonConfiguration(); - if (json == null) { + + // we fetch the unencrypted configuration - but we do only store meta data which + // contains no + // sensitive information. + SecHubConfigurationModel configuration = configurationModelAccess.resolveUnencryptedConfiguration(job); + if (configuration == null) { LOG.error("No sechub configuration found for job: {}. Cannot resolve meta data!", job.getUUID()); return; } - SecHubScanConfiguration configuration = SecHubScanConfiguration.createFromJSON(json); + Optional metaDataOpt = configuration.getMetaData(); if (metaDataOpt.isPresent()) { infoForUser.setMetaData(metaDataOpt.get()); diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobRepository.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobRepository.java index ce642399fc..1bd19158e4 100644 --- a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobRepository.java +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobRepository.java @@ -30,9 +30,13 @@ public interface SecHubJobRepository extends JpaRepository getJob(UUID id); - Optional nextJobIdToExecuteFirstInFirstOut(); + Optional nextJobIdToExecuteFirstInFirstOut(Set acceptedEncryptiondPoolIds); + + Optional nextJobIdToExecuteForProjectNotYetExecuted(Set acceptedEncryptiondPoolIds); + + Optional nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(Set acceptedEncryptiondPoolIds); + + /** + * Fetches next jobs which have been canceled or have ended but have an + * encryption pool entry which is lower (means older) than the given one. The + * fetched jobs are returned in a random way. + * + * @param encryptionPoolId the higher (newer) encryption pool id + * @param maxAmount maximum amount of jobs to return + * @return list of jobs, never null + */ + List nextCanceledOrEndedJobsWithEncryptionPoolIdLowerThan(Long encryptionPoolId, int maxAmount); - Optional nextJobIdToExecuteForProjectNotYetExecuted(); + @Lock(LockModeType.NONE) + public long countCanceledOrEndedJobsWithEncryptionPoolIdLowerThan(Long encryptionPoolId); - Optional nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(); + /** + * Collects a distinct list of all encryption pool ids which are used by any job + * in any state. + * + * @return set of encryption pool ids, never null + */ + List collectAllUsedEncryptionPoolIdsInsideJobs(); } diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobRepositoryImpl.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobRepositoryImpl.java index e3b270f350..7af121d6a5 100644 --- a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobRepositoryImpl.java +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobRepositoryImpl.java @@ -3,7 +3,9 @@ import static com.mercedesbenz.sechub.domain.schedule.job.ScheduleSecHubJob.*; +import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.UUID; import com.mercedesbenz.sechub.commons.model.job.ExecutionState; @@ -19,11 +21,14 @@ public class SecHubJobRepositoryImpl implements SecHubJobRepositoryCustom { private static final String PARAM_UUID = "p_uuid"; private static final String PARAM_EXECUTION_STATE = "p_exec_state"; private static final String PARAM_EXECUTION_STATE_SUB = "p_sub_exec_state"; + private static final String PARAM_ENCRYPTION_POOL_ID = "p_encrypt_pool_id"; + private static final String PARAM_ENCRYPTION_POOL_IDS = "p_encrypt_pool_ids"; /* @formatter:off */ static final String JPQL_STRING_SELECT_BY_EXECUTION_STATE = "select j from " + CLASS_NAME + " j" + " where j." + PROPERTY_EXECUTION_STATE + " = :" + PARAM_EXECUTION_STATE + + " and j."+ PROPERTY_ENCRYPTION_POOL_ID +" in (:"+PARAM_ENCRYPTION_POOL_IDS+")"+ " order by j." + PROPERTY_CREATED; static final String JPQL_STRING_SELECT_BY_JOB_ID = @@ -38,6 +43,7 @@ public class SecHubJobRepositoryImpl implements SecHubJobRepositoryCustom { static final String JPQL_STRING_SELECT_JOB_WHERE_NOT_YET_RUNNING_SAME_PROJECT = "select j from " + CLASS_NAME + " j" + " where j." + PROPERTY_EXECUTION_STATE + " = :" + PARAM_EXECUTION_STATE + + " and j."+ PROPERTY_ENCRYPTION_POOL_ID +" in (:"+PARAM_ENCRYPTION_POOL_IDS+")"+ " and j." + PROPERTY_PROJECT_ID + " not in ( " + SUB_JPQL_STRING_SELECT_PROJECTS_WITH_RUNNING_JOBS + " )" + " order by " + PROPERTY_CREATED; @@ -51,10 +57,25 @@ public class SecHubJobRepositoryImpl implements SecHubJobRepositoryCustom { static final String JPQL_STRING_SELECT_JOB_WHERE_NOT_YET_RUNNING_SAME_PROJECT_IN_SAME_GROUP = "select j from " + CLASS_NAME + " j" + " where j." + PROPERTY_EXECUTION_STATE + " = :" + PARAM_EXECUTION_STATE + + " and j."+ PROPERTY_ENCRYPTION_POOL_ID +" in (:"+PARAM_ENCRYPTION_POOL_IDS+")"+ " and j." + PROPERTY_PROJECT_ID + " not in ( " + SUB_JPQL_STRING_SELECT_PROJECTS_WITH_RUNNING_JOBS_AND_SAME_MODULE_GROUP + " )" + " order by " + PROPERTY_CREATED; + static final String JPQL_STRING_SELECT_RANDOM_JOB_CANCELED_OR_ENDED_WHERE_POOL_ID_IS_SMALLER_THAN_GIVEN_ONE = + "select j from " + CLASS_NAME + " j" + + " where (j." + PROPERTY_EXECUTION_STATE + " = " + ExecutionState.ENDED + " or j." + PROPERTY_EXECUTION_STATE + " = " +ExecutionState.CANCELED +")"+ + " and j." + PROPERTY_ENCRYPTION_POOL_ID + " < :" + PARAM_ENCRYPTION_POOL_ID + + " order by random()"; + + static final String JPQL_STRING_COUNT_JOBS_CANCELED_OR_ENDED_WHERE_POOL_ID_IS_SMALLER_THAN_GIVEN_ONE = + "select count(j) from " + CLASS_NAME + " j" + + " where (j." + PROPERTY_EXECUTION_STATE + " = " + ExecutionState.ENDED + " or j." + PROPERTY_EXECUTION_STATE + " = " +ExecutionState.CANCELED +")"+ + " and j." + PROPERTY_ENCRYPTION_POOL_ID + " < :" + PARAM_ENCRYPTION_POOL_ID; + + static final String JPQL_STRING_FETCH_ALL_USED_ENCRYPTION_POOL_IDS_IN_JOBS = + "select DISTINCT j."+PROPERTY_ENCRYPTION_POOL_ID+" from " + CLASS_NAME + " j"; + /* @formatter:on */ @@ -75,9 +96,10 @@ public Optional getJob(UUID id) { } @Override - public Optional nextJobIdToExecuteFirstInFirstOut() { + public Optional nextJobIdToExecuteFirstInFirstOut(Set acceptedEncryptiondPoolIds) { Query query = em.createQuery(JPQL_STRING_SELECT_BY_EXECUTION_STATE); query.setParameter(PARAM_EXECUTION_STATE, ExecutionState.READY_TO_START); + query.setParameter(PARAM_ENCRYPTION_POOL_IDS, acceptedEncryptiondPoolIds); query.setMaxResults(1); // we use OPTIMISTIC_FORCE_INCREMENT write lock - so only one POD will be able // to execute next job... @@ -88,10 +110,11 @@ public Optional nextJobIdToExecuteFirstInFirstOut() { } @Override - public Optional nextJobIdToExecuteForProjectNotYetExecuted() { + public Optional nextJobIdToExecuteForProjectNotYetExecuted(Set acceptedEncryptiondPoolIds) { Query query = em.createQuery(JPQL_STRING_SELECT_JOB_WHERE_NOT_YET_RUNNING_SAME_PROJECT); query.setParameter(PARAM_EXECUTION_STATE, ExecutionState.READY_TO_START); query.setParameter(PARAM_EXECUTION_STATE_SUB, ExecutionState.STARTED); + query.setParameter(PARAM_ENCRYPTION_POOL_IDS, acceptedEncryptiondPoolIds); query.setMaxResults(1); query.setLockMode(LockModeType.OPTIMISTIC_FORCE_INCREMENT); @@ -107,14 +130,39 @@ private Optional getUUIDFromJob(Optional job) { } @Override - public Optional nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted() { + public Optional nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(Set acceptedEncryptiondPoolIds) { Query query = em.createQuery(JPQL_STRING_SELECT_JOB_WHERE_NOT_YET_RUNNING_SAME_PROJECT_IN_SAME_GROUP); query.setParameter(PARAM_EXECUTION_STATE, ExecutionState.READY_TO_START); query.setParameter(PARAM_EXECUTION_STATE_SUB, ExecutionState.STARTED); + query.setParameter(PARAM_ENCRYPTION_POOL_IDS, acceptedEncryptiondPoolIds); query.setMaxResults(1); query.setLockMode(LockModeType.OPTIMISTIC_FORCE_INCREMENT); return getUUIDFromJob(typedQuerySupport.getSingleResultAsOptional(query)); } + @Override + public List nextCanceledOrEndedJobsWithEncryptionPoolIdLowerThan(Long encryptionPoolId, int maxAmount) { + Query query = em.createQuery(JPQL_STRING_SELECT_RANDOM_JOB_CANCELED_OR_ENDED_WHERE_POOL_ID_IS_SMALLER_THAN_GIVEN_ONE); + query.setParameter(PARAM_ENCRYPTION_POOL_ID, encryptionPoolId); + query.setMaxResults(maxAmount); + query.setLockMode(LockModeType.OPTIMISTIC_FORCE_INCREMENT); + + return typedQuerySupport.getList(query); + } + + public long countCanceledOrEndedJobsWithEncryptionPoolIdLowerThan(Long encryptionPoolId) { + Query query = em.createQuery(JPQL_STRING_COUNT_JOBS_CANCELED_OR_ENDED_WHERE_POOL_ID_IS_SMALLER_THAN_GIVEN_ONE); + query.setParameter(PARAM_ENCRYPTION_POOL_ID, encryptionPoolId); + query.setMaxResults(1); + return (Long) query.getSingleResult(); + } + + @SuppressWarnings("unchecked") + @Override + public List collectAllUsedEncryptionPoolIdsInsideJobs() { + Query query = em.createQuery(JPQL_STRING_FETCH_ALL_USED_ENCRYPTION_POOL_IDS_IN_JOBS); + return query.getResultList(); + } + } diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobTransactionService.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobTransactionService.java index 35ca02314e..2ca60c7998 100644 --- a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobTransactionService.java +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobTransactionService.java @@ -3,6 +3,7 @@ import static com.mercedesbenz.sechub.sharedkernel.util.Assert.*; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -44,4 +45,16 @@ public void updateExecutionStateInOwnTransaction(UUID sechubJobUUID, ExecutionSt LOG.info("Job :{} has now execution state: {}", sechubJobUUID, job.getExecutionState()); } + public void saveInOwnTransaction(ScheduleSecHubJob job) { + repository.save(job); + } + + public long countCanceledOrEndedJobsWithEncryptionPoolIdLowerThan(Long latestPoolid) { + return repository.countCanceledOrEndedJobsWithEncryptionPoolIdLowerThan(latestPoolid); + } + + public List nextCanceledOrEndedJobsWithEncryptionPoolIdLowerThan(Long latestPoolid, int updateBlockSize) { + return repository.nextCanceledOrEndedJobsWithEncryptionPoolIdLowerThan(latestPoolid, updateBlockSize); + } + } diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/strategy/FirstComeFirstServeSchedulerStrategy.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/strategy/FirstComeFirstServeSchedulerStrategy.java index b66a14773c..a56b4d4bfb 100644 --- a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/strategy/FirstComeFirstServeSchedulerStrategy.java +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/strategy/FirstComeFirstServeSchedulerStrategy.java @@ -2,11 +2,13 @@ package com.mercedesbenz.sechub.domain.schedule.strategy; import java.util.Optional; +import java.util.Set; import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionService; import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobRepository; @Component @@ -15,6 +17,9 @@ public class FirstComeFirstServeSchedulerStrategy implements SchedulerStrategy { @Autowired public SecHubJobRepository jobRepository; + @Autowired + ScheduleEncryptionService encryptionService; + @Override public SchedulerStrategyId getSchedulerId() { return SchedulerStrategyId.FIRST_COME_FIRST_SERVE; @@ -22,8 +27,9 @@ public SchedulerStrategyId getSchedulerId() { @Override public UUID nextJobId() { + Set supportedPoolIds = encryptionService.getCurrentEncryptionPoolIds(); - Optional nextJob = jobRepository.nextJobIdToExecuteFirstInFirstOut(); + Optional nextJob = jobRepository.nextJobIdToExecuteFirstInFirstOut(supportedPoolIds); if (!nextJob.isPresent()) { return null; } diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/strategy/OnlyOneScanPerProjectAndModuleGroupAtSameTimeStrategy.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/strategy/OnlyOneScanPerProjectAndModuleGroupAtSameTimeStrategy.java index 8dba927e75..67125fdee1 100644 --- a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/strategy/OnlyOneScanPerProjectAndModuleGroupAtSameTimeStrategy.java +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/strategy/OnlyOneScanPerProjectAndModuleGroupAtSameTimeStrategy.java @@ -2,11 +2,13 @@ package com.mercedesbenz.sechub.domain.schedule.strategy; import java.util.Optional; +import java.util.Set; import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionService; import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobRepository; /** @@ -52,6 +54,9 @@ public class OnlyOneScanPerProjectAndModuleGroupAtSameTimeStrategy implements Sc @Autowired SecHubJobRepository jobRepository; + @Autowired + ScheduleEncryptionService encryptionService; + @Override public SchedulerStrategyId getSchedulerId() { return SchedulerStrategyId.ONE_SCAN_PER_PROJECT_AND_MODULE_GROUP; @@ -59,7 +64,9 @@ public SchedulerStrategyId getSchedulerId() { @Override public UUID nextJobId() { - Optional nextJob = jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(); + Set supportedPoolIds = encryptionService.getCurrentEncryptionPoolIds(); + + Optional nextJob = jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(supportedPoolIds); if (!nextJob.isPresent()) { return null; } diff --git a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/strategy/OnlyOneScanPerProjectAtSameTimeStrategy.java b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/strategy/OnlyOneScanPerProjectAtSameTimeStrategy.java index 572c3824a8..cb76a706f7 100644 --- a/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/strategy/OnlyOneScanPerProjectAtSameTimeStrategy.java +++ b/sechub-schedule/src/main/java/com/mercedesbenz/sechub/domain/schedule/strategy/OnlyOneScanPerProjectAtSameTimeStrategy.java @@ -2,11 +2,13 @@ package com.mercedesbenz.sechub.domain.schedule.strategy; import java.util.Optional; +import java.util.Set; import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionService; import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobRepository; @Component @@ -15,6 +17,9 @@ public class OnlyOneScanPerProjectAtSameTimeStrategy implements SchedulerStrateg @Autowired SecHubJobRepository jobRepository; + @Autowired + ScheduleEncryptionService encryptionService; + @Override public SchedulerStrategyId getSchedulerId() { return SchedulerStrategyId.ONE_SCAN_PER_PROJECT; @@ -22,7 +27,9 @@ public SchedulerStrategyId getSchedulerId() { @Override public UUID nextJobId() { - Optional nextJob = jobRepository.nextJobIdToExecuteForProjectNotYetExecuted(); + Set supportedPoolIds = encryptionService.getCurrentEncryptionPoolIds(); + + Optional nextJob = jobRepository.nextJobIdToExecuteForProjectNotYetExecuted(supportedPoolIds); if (!nextJob.isPresent()) { return null; } diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/autocleanup/ScheduleAutoCleanupServiceTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/autocleanup/ScheduleAutoCleanupServiceTest.java index 139f22b00b..787046919d 100644 --- a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/autocleanup/ScheduleAutoCleanupServiceTest.java +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/autocleanup/ScheduleAutoCleanupServiceTest.java @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.domain.schedule.autocleanup; +import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -12,11 +13,13 @@ import org.mockito.ArgumentCaptor; import com.mercedesbenz.sechub.domain.schedule.config.SchedulerConfigService; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleCipherPoolCleanupService; import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobDataRepository; import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobRepository; import com.mercedesbenz.sechub.sharedkernel.TimeCalculationService; import com.mercedesbenz.sechub.sharedkernel.autocleanup.AutoCleanupResult; import com.mercedesbenz.sechub.sharedkernel.autocleanup.AutoCleanupResultInspector; +import com.mercedesbenz.sechub.test.TestCanaryException; class ScheduleAutoCleanupServiceTest { @@ -26,6 +29,7 @@ class ScheduleAutoCleanupServiceTest { private SecHubJobDataRepository jobDataRepository; private TimeCalculationService timeCalculationService; private AutoCleanupResultInspector inspector; + private ScheduleCipherPoolCleanupService encryptionPoolCleanupService; @BeforeEach void beforeEach() { @@ -36,12 +40,64 @@ void beforeEach() { jobDataRepository = mock(SecHubJobDataRepository.class); timeCalculationService = mock(TimeCalculationService.class); inspector = mock(AutoCleanupResultInspector.class); + encryptionPoolCleanupService = mock(ScheduleCipherPoolCleanupService.class); serviceToTest.configService = configService; serviceToTest.jobRepository = jobRepository; serviceToTest.jobDataRepository = jobDataRepository; serviceToTest.timeCalculationService = timeCalculationService; serviceToTest.inspector = inspector; + serviceToTest.encryptionPoolCleanupService = encryptionPoolCleanupService; + } + + @Test + void auto_cleanup_triggers_encryption_pool_cleanup() throws Exception { + + /* prepare */ + when(configService.getAutoCleanupInDays()).thenReturn(1L); + LocalDateTime cleanTime = LocalDateTime.now().minusDays(1L); + when(timeCalculationService.calculateNowMinusDays(any())).thenReturn(cleanTime); + + /* execute */ + serviceToTest.cleanup(); + + /* test */ + verify(encryptionPoolCleanupService).cleanupCipherPoolDataIfNecessaryAndPossible(); + + } + + @Test + void when_jobDataRepository_deleteJobDataOlderThan_throws_exception_encryption_pool_cleanup_is_not_done() throws Exception { + + /* prepare */ + when(configService.getAutoCleanupInDays()).thenReturn(1L); + LocalDateTime cleanTime = LocalDateTime.now().minusDays(1L); + when(timeCalculationService.calculateNowMinusDays(any())).thenReturn(cleanTime); + when(jobDataRepository.deleteJobDataOlderThan(cleanTime)).thenThrow(TestCanaryException.class); + + /* execute */ + assertThatThrownBy(()->serviceToTest.cleanup()).isInstanceOf(TestCanaryException.class); + + /* test */ + verify(encryptionPoolCleanupService,never()).cleanupCipherPoolDataIfNecessaryAndPossible(); + + } + + @Test + void when_jobRepository_deleteJobsOlderThan_throws_exception_encryption_pool_cleanup_is_not_done() throws Exception { + + /* prepare */ + when(configService.getAutoCleanupInDays()).thenReturn(1L); + LocalDateTime cleanTime = LocalDateTime.now().minusDays(1L); + when(timeCalculationService.calculateNowMinusDays(any())).thenReturn(cleanTime); + when(jobRepository.deleteJobsOlderThan(cleanTime)).thenThrow(TestCanaryException.class); + + /* execute */ + assertThatThrownBy(()->serviceToTest.cleanup()).isInstanceOf(TestCanaryException.class); + + /* test */ + verify(encryptionPoolCleanupService,never()).cleanupCipherPoolDataIfNecessaryAndPossible(); + } @Test @@ -62,6 +118,8 @@ void cleanup_executes_NOT_delete_job_information_for_0_days() { verify(jobDataRepository, never()).deleteJobDataOlderThan(any()); // check inspection as expected: never because not executed verify(inspector, never()).inspect(any()); + // check not encryption pool cleanup is done + verify(encryptionPoolCleanupService, never()).cleanupCipherPoolDataIfNecessaryAndPossible(); } @Test diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherAlgorithmTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherAlgorithmTest.java new file mode 100644 index 0000000000..2c0a5e0af2 --- /dev/null +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherAlgorithmTest.java @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import static org.assertj.core.api.Assertions.*; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherType; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherAlgorithm; + +class ScheduleCipherAlgorithmTest { + + /* + * We have separated the database enumeration from the encryption parts - for + * different reasons. The test will ensure that the types are as expected. + */ + @ParameterizedTest + @ArgumentsSource(CipherAlgorithmTestData.class) + void databaseAlgorithmHasExpectedInternalType(SecHubCipherAlgorithm algorithmInDb, PersistentCipherType internalType) { + assertThat(algorithmInDb.getType()).isEqualTo(internalType); + } + + private static class CipherAlgorithmTestData implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + /* @formatter:off */ + return Stream.of( + Arguments.of(SecHubCipherAlgorithm.NONE, PersistentCipherType.NONE), + Arguments.of(SecHubCipherAlgorithm.AES_GCM_SIV_128, PersistentCipherType.AES_GCM_SIV_128), + Arguments.of(SecHubCipherAlgorithm.AES_GCM_SIV_256, PersistentCipherType.AES_GCM_SIV_256) + ) + ; + /* @formatter:on */ + } + + } +} diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolCleanupServiceTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolCleanupServiceTest.java new file mode 100644 index 0000000000..0ca426a1ff --- /dev/null +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolCleanupServiceTest.java @@ -0,0 +1,276 @@ +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobRepository; + +class ScheduleCipherPoolCleanupServiceTest { + + private static final long POOL_ID_0 = 0L; + private static final long POOL_ID_3 = 3L; + private static final long POOL_ID_2 = 2L; + private static final long POOL_ID_1 = 1L; + private ScheduleCipherPoolCleanupService serviceToTest; + private ScheduleEncryptionService encryptionService; + private SecHubJobRepository jobRepository; + private ScheduleCipherPoolDataRepository poolDataRepository; + private ScheduleLatestCipherPoolDataCalculator latestCipherPoolDataCalculator; + private SecHubOutdatedEncryptionPoolSupport outdatedEncryptionPoolSupport; + + @BeforeEach + public void beforeEach() throws Exception { + serviceToTest = new ScheduleCipherPoolCleanupService(); + + encryptionService = mock(ScheduleEncryptionService.class); + jobRepository = mock(SecHubJobRepository.class); + poolDataRepository = mock(ScheduleCipherPoolDataRepository.class); + latestCipherPoolDataCalculator = mock(ScheduleLatestCipherPoolDataCalculator.class); + outdatedEncryptionPoolSupport = mock(SecHubOutdatedEncryptionPoolSupport.class); + + serviceToTest.encryptionService = encryptionService; + serviceToTest.jobRepository = jobRepository; + serviceToTest.poolDataRepository = poolDataRepository; + serviceToTest.latestCipherPoolDataCalculator = latestCipherPoolDataCalculator; + serviceToTest.outdatedEncryptionPoolSupport = outdatedEncryptionPoolSupport; + } + + @Test + void when_cluster_could_be_outdated_no_further_inspection_is_done() throws Exception { + + /* prepare */ + when(outdatedEncryptionPoolSupport.isOutdatedEncryptionPoolPossibleInCluster()).thenReturn(true); + + /* execute */ + serviceToTest.cleanupCipherPoolDataIfNecessaryAndPossible(); + + /* test */ + verify(outdatedEncryptionPoolSupport).isOutdatedEncryptionPoolPossibleInCluster(); + + verifyNoInteractions(encryptionService); + verifyNoInteractions(jobRepository); + verifyNoInteractions(poolDataRepository); + verifyNoInteractions(latestCipherPoolDataCalculator); + + } + + @Test + void no_pool_data_found_nothing_else_is_done() throws Exception { + + /* prepare */ + when(outdatedEncryptionPoolSupport.isOutdatedEncryptionPoolPossibleInCluster()).thenReturn(false); + List list = new ArrayList<>(); + when(poolDataRepository.findAll()).thenReturn(list); + + /* execute */ + serviceToTest.cleanupCipherPoolDataIfNecessaryAndPossible(); + + /* test */ + verify(outdatedEncryptionPoolSupport).isOutdatedEncryptionPoolPossibleInCluster(); + verify(poolDataRepository).findAll(); + + verifyNoInteractions(encryptionService); + verifyNoInteractions(jobRepository); + verifyNoInteractions(latestCipherPoolDataCalculator); + + } + + @Test + void pool_entries_exist_but_encryption_pool_has_not_latest_no_jobs_are_inspected() throws Exception { + + /* prepare */ + when(outdatedEncryptionPoolSupport.isOutdatedEncryptionPoolPossibleInCluster()).thenReturn(false); + List list = new ArrayList<>(); + ScheduleCipherPoolData poolData1 = createTestPoolData(POOL_ID_1); + list.add(poolData1); + + ScheduleCipherPoolData poolData2 = createTestPoolData(POOL_ID_2); + list.add(poolData2); + + when(poolDataRepository.findAll()).thenReturn(list); + when(latestCipherPoolDataCalculator.calculateLatestPoolData(list)).thenReturn(poolData2); + + when(encryptionService.getLatestCipherPoolId()).thenReturn(POOL_ID_1); // different than calculated + + /* execute */ + serviceToTest.cleanupCipherPoolDataIfNecessaryAndPossible(); + + /* test */ + verify(encryptionService).getLatestCipherPoolId(); + verify(outdatedEncryptionPoolSupport).isOutdatedEncryptionPoolPossibleInCluster(); + verify(poolDataRepository).findAll(); + verify(latestCipherPoolDataCalculator).calculateLatestPoolData(list); + + verifyNoInteractions(jobRepository); + + } + + @Test + void no_unused_pool_entries() throws Exception { + + /* prepare */ + when(outdatedEncryptionPoolSupport.isOutdatedEncryptionPoolPossibleInCluster()).thenReturn(false); + List list = new ArrayList<>(); + ScheduleCipherPoolData poolData1 = createTestPoolData(POOL_ID_1); + list.add(poolData1); + + ScheduleCipherPoolData poolData2 = createTestPoolData(POOL_ID_2); + list.add(poolData2); + + when(poolDataRepository.findAll()).thenReturn(list); + when(latestCipherPoolDataCalculator.calculateLatestPoolData(list)).thenReturn(poolData2); + + when(encryptionService.getLatestCipherPoolId()).thenReturn(POOL_ID_2); + + List collectList = List.of(POOL_ID_1, POOL_ID_2); + when(jobRepository.collectAllUsedEncryptionPoolIdsInsideJobs()).thenReturn(collectList); + + /* execute */ + serviceToTest.cleanupCipherPoolDataIfNecessaryAndPossible(); + + /* test */ + verify(outdatedEncryptionPoolSupport).isOutdatedEncryptionPoolPossibleInCluster(); + verify(poolDataRepository).findAll(); + verify(latestCipherPoolDataCalculator).calculateLatestPoolData(list); + + verify(jobRepository).collectAllUsedEncryptionPoolIdsInsideJobs(); + + verify(poolDataRepository, never()).delete(any(ScheduleCipherPoolData.class)); + ; + + } + + @Test + void unused_pool_entry_latest_is_used() throws Exception { + + /* prepare */ + when(outdatedEncryptionPoolSupport.isOutdatedEncryptionPoolPossibleInCluster()).thenReturn(false); + List list = new ArrayList<>(); + ScheduleCipherPoolData poolData1 = createTestPoolData(POOL_ID_1); + list.add(poolData1); + + ScheduleCipherPoolData poolData2 = createTestPoolData(POOL_ID_2); + list.add(poolData2); + + ScheduleCipherPoolData poolData3 = createTestPoolData(POOL_ID_3); + list.add(poolData3); + + when(poolDataRepository.findAll()).thenReturn(list); + when(latestCipherPoolDataCalculator.calculateLatestPoolData(list)).thenReturn(poolData3); + + when(encryptionService.getLatestCipherPoolId()).thenReturn(POOL_ID_3); + + List collectList = List.of(POOL_ID_2, POOL_ID_3); + when(jobRepository.collectAllUsedEncryptionPoolIdsInsideJobs()).thenReturn(collectList); + + /* execute */ + serviceToTest.cleanupCipherPoolDataIfNecessaryAndPossible(); + + /* test */ + verify(outdatedEncryptionPoolSupport).isOutdatedEncryptionPoolPossibleInCluster(); + verify(poolDataRepository).findAll(); + verify(latestCipherPoolDataCalculator).calculateLatestPoolData(list); + + verify(jobRepository).collectAllUsedEncryptionPoolIdsInsideJobs(); + + verify(poolDataRepository).delete(poolData1); + verify(poolDataRepository, never()).delete(poolData2); + verify(poolDataRepository, never()).delete(poolData3); + + } + + @Test + void unused_pool_entry_latest_is_NOT_used() throws Exception { + + /* prepare */ + when(outdatedEncryptionPoolSupport.isOutdatedEncryptionPoolPossibleInCluster()).thenReturn(false); + List list = new ArrayList<>(); + ScheduleCipherPoolData poolData1 = createTestPoolData(POOL_ID_1); + list.add(poolData1); + + ScheduleCipherPoolData poolData2 = createTestPoolData(POOL_ID_2); + list.add(poolData2); + + ScheduleCipherPoolData poolData3 = createTestPoolData(POOL_ID_3); + list.add(poolData3); + + when(poolDataRepository.findAll()).thenReturn(list); + when(latestCipherPoolDataCalculator.calculateLatestPoolData(list)).thenReturn(poolData3); + + when(encryptionService.getLatestCipherPoolId()).thenReturn(POOL_ID_3); + + List collectList = List.of(POOL_ID_2); + when(jobRepository.collectAllUsedEncryptionPoolIdsInsideJobs()).thenReturn(collectList); + + /* execute */ + serviceToTest.cleanupCipherPoolDataIfNecessaryAndPossible(); + + /* test */ + verify(outdatedEncryptionPoolSupport).isOutdatedEncryptionPoolPossibleInCluster(); + verify(poolDataRepository).findAll(); + verify(latestCipherPoolDataCalculator).calculateLatestPoolData(list); + + verify(jobRepository).collectAllUsedEncryptionPoolIdsInsideJobs(); + + verify(poolDataRepository).delete(poolData1); + verify(poolDataRepository, never()).delete(poolData2); + verify(poolDataRepository, never()).delete(poolData3);// is not used, but latest ,so not deleted + + } + + @Test + void two_unused_pool_entries_latest_is_NOT_used() throws Exception { + + /* prepare */ + when(outdatedEncryptionPoolSupport.isOutdatedEncryptionPoolPossibleInCluster()).thenReturn(false); + List list = new ArrayList<>(); + ScheduleCipherPoolData poolData0 = createTestPoolData(POOL_ID_0); + list.add(poolData0); + + ScheduleCipherPoolData poolData1 = createTestPoolData(POOL_ID_1); + list.add(poolData1); + + ScheduleCipherPoolData poolData2 = createTestPoolData(POOL_ID_2); + list.add(poolData2); + + ScheduleCipherPoolData poolData3 = createTestPoolData(POOL_ID_3); + list.add(poolData3); + + when(poolDataRepository.findAll()).thenReturn(list); + when(latestCipherPoolDataCalculator.calculateLatestPoolData(list)).thenReturn(poolData3); + + when(encryptionService.getLatestCipherPoolId()).thenReturn(POOL_ID_3); + + List collectList = List.of(POOL_ID_2); + when(jobRepository.collectAllUsedEncryptionPoolIdsInsideJobs()).thenReturn(collectList); + + /* execute */ + serviceToTest.cleanupCipherPoolDataIfNecessaryAndPossible(); + + /* test */ + verify(outdatedEncryptionPoolSupport).isOutdatedEncryptionPoolPossibleInCluster(); + verify(poolDataRepository).findAll(); + verify(latestCipherPoolDataCalculator).calculateLatestPoolData(list); + + verify(jobRepository).collectAllUsedEncryptionPoolIdsInsideJobs(); + + verify(poolDataRepository).delete(poolData0); + verify(poolDataRepository).delete(poolData1); + verify(poolDataRepository, never()).delete(poolData2); + verify(poolDataRepository, never()).delete(poolData3);// is not used, but latest ,so not deleted + + } + + private ScheduleCipherPoolData createTestPoolData(long poolId) { + ScheduleCipherPoolData poolData1 = mock(ScheduleCipherPoolData.class, "Test pool data:" + poolId); + when(poolData1.getId()).thenReturn(poolId); + return poolData1; + } + +} diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataProviderSpringBootTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataProviderSpringBootTest.java new file mode 100644 index 0000000000..b9af586b00 --- /dev/null +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataProviderSpringBootTest.java @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherAlgorithm; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherPasswordSourceType; + +@SpringBootTest(classes = ScheduleCipherPoolDataProvider.class) +class ScheduleCipherPoolDataProviderSpringBootTest { + + @Autowired + private ScheduleCipherPoolDataProvider providerToTest; + + @MockBean + private ScheduleCipherPoolDataRepository repository; + + @BeforeEach + void beforeEach() { + } + + @Test + void provider_creates_fallback_when_nothing_found() { + + /* prepare */ + when(repository.findAll()).thenReturn(Collections.emptyList()); + + /* execute */ + List result = providerToTest.ensurePoolDataAvailable(); + + /* test */ + assertThat(result).isNotEmpty().hasSize(1); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ScheduleCipherPoolData.class); + verify(repository).save(argumentCaptor.capture()); + + ScheduleCipherPoolData savedFallback = argumentCaptor.getValue(); + + assertThat(savedFallback).isNotNull(); + + assertThat(savedFallback.getAlgorithm()).isEqualTo(SecHubCipherAlgorithm.NONE); + assertThat(savedFallback.getTestEncrypted()).isNotNull(); + assertThat(savedFallback.getCreated()).isNotNull(); + assertThat(savedFallback.getCreatedFrom()).describedAs("Created by system means always null because no user involved").isNull(); + assertThat(savedFallback.getId()).describedAs("Fallback is created as first entry when none exists, will always use the first possible one") + .isEqualTo(0); + assertThat(savedFallback.getPasswordSourceType()).isEqualTo(SecHubCipherPasswordSourceType.NONE); + assertThat(savedFallback.getPasswordSourceData()).isNull(); + assertThat(savedFallback.getTestEncrypted()).isEqualTo("fallback".getBytes(Charset.forName("UTF-8"))); + assertThat(savedFallback.getTestInitialVector()).isNull(); + assertThat(savedFallback.getTestText()).isEqualTo("fallback"); + + } + + @Test + void provider_returns_found_data_without_fallback_when_data_available() { + + ScheduleCipherPoolData data1 = mock(ScheduleCipherPoolData.class); + ScheduleCipherPoolData data2 = mock(ScheduleCipherPoolData.class); + + /* prepare */ + when(repository.findAll()).thenReturn(List.of(data1, data2)); + + /* execute */ + List result = providerToTest.ensurePoolDataAvailable(); + + /* test */ + assertThat(result).hasSize(2).contains(data1, data2); + + } + + @Test + void isContainingExactlyGivenPoolIds_null_throws_illegal_argument() { + + /* execute +test */ + assertThatThrownBy(() -> providerToTest.isContainingExactlyGivenPoolIds(null)).isInstanceOf(IllegalArgumentException.class); + + } + + @ParameterizedTest + @ArgumentsSource(SimplePoolIdSetArgumentsProvider.class) + void isContainingExactlyGivenPoolIds_contains_same_as_expected(Set poolIdsInDbAndCurrentlyInMemory) { + + /* prepare */ + when(repository.fetchAllCipherPoolIds()).thenReturn(poolIdsInDbAndCurrentlyInMemory); + + /* execute */ + boolean result = providerToTest.isContainingExactlyGivenPoolIds(poolIdsInDbAndCurrentlyInMemory); + + /* test */ + assertThat(result).isTrue(); + } + + @ParameterizedTest + @ArgumentsSource(DifferentPoolIdSetArgumentsProvider.class) + void isContainingExactlyGivenPoolIds_not_same(Set currentPoolIds, Set poolIdsInDatabase) { + + /* prepare */ + when(repository.fetchAllCipherPoolIds()).thenReturn(poolIdsInDatabase); + + /* execute */ + boolean result = providerToTest.isContainingExactlyGivenPoolIds(currentPoolIds); + + /* test */ + assertThat(result).isFalse(); + } + + static class SimplePoolIdSetArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + return Stream.of(Arguments.of(Set.of()), Arguments.of(Set.of(0L)), Arguments.of(Set.of(0L, 1L, 2L)), Arguments.of(Set.of(0L, 1L, 2L, 3L, 5L)), + Arguments.of(Set.of(3L, 5L))); + } + + } + + static class DifferentPoolIdSetArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + return Stream.of(Arguments.of(Set.of(), Set.of(1L)), Arguments.of(Set.of(0L), Set.of()), Arguments.of(Set.of(0L), Set.of(1L)), + Arguments.of(Set.of(0L, 1L, 2L), Set.of(0L, 1l)), Arguments.of(Set.of(0L, 1L, 2L, 3L, 5L), Set.of(0L, 1L, 2L, 3L, 6L)), + Arguments.of(Set.of(3L, 5L), Set.of(5L, 6L))); + } + + } + +} diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataRepositoryDBTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataRepositoryDBTest.java new file mode 100644 index 0000000000..3553ee3efb --- /dev/null +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataRepositoryDBTest.java @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import static org.assertj.core.api.Assertions.*; + +import java.nio.charset.Charset; +import java.time.LocalDateTime; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherAlgorithm; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherPasswordSourceType; + +@RunWith(SpringRunner.class) +@DataJpaTest +@ContextConfiguration(classes = { ScheduleCipherPoolDataRepository.class, ScheduleCipherPoolDataRepositoryDBTest.SimpleTestConfiguration.class }) +class ScheduleCipherPoolDataRepositoryDBTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private ScheduleCipherPoolDataRepository repositoryToTest; + + @Test + void fetchAllCipherPoolIds_nothing_found_returns_empty_set() { + + /* execute */ + Set result = repositoryToTest.fetchAllCipherPoolIds(); + + /* test */ + assertThat(result).isNotNull().isEmpty(); + } + + @Test + void fetchAllCipherPoolIds_one_entry() throws Exception { + + /* prepare */ + Set createdIds = new LinkedHashSet<>(); + + createdIds.add(createEntry()); + + /* execute */ + Set result = repositoryToTest.fetchAllCipherPoolIds(); + + /* test */ + assertThat(result).isNotNull().isNotEmpty().containsAll(createdIds).hasSize(1); + + } + + @Test + void fetchAllCipherPoolIds_two_entries() throws Exception { + /* prepare */ + Set createdIds = new LinkedHashSet<>(); + + createdIds.add(createEntry()); + createdIds.add(createEntry()); + + /* execute */ + Set result = repositoryToTest.fetchAllCipherPoolIds(); + + /* test */ + assertThat(result).isNotNull().isNotEmpty().containsAll(createdIds); + + } + + private long createEntry() { + ScheduleCipherPoolData data1 = new ScheduleCipherPoolData(); + data1.secHubCipherPasswordSourceType = SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE; + data1.created = LocalDateTime.now(); + data1.algorithm = SecHubCipherAlgorithm.NONE; + data1.testEncrypted = "test".getBytes(Charset.forName("UTF-8")); + data1.testInitialVector = new byte[] {}; + data1.testText = "test"; + + ScheduleCipherPoolData result = entityManager.persist(data1); + return result.getId(); + } + + @TestConfiguration + @EnableAutoConfiguration + public static class SimpleTestConfiguration { + + } +} diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataTransactionServiceTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataTransactionServiceTest.java new file mode 100644 index 0000000000..890febc8fd --- /dev/null +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleCipherPoolDataTransactionServiceTest.java @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ScheduleCipherPoolDataTransactionServiceTest { + + private ScheduleCipherPoolDataTransactionService serviceToTest; + private ScheduleCipherPoolDataRepository repository; + + @BeforeEach + void beforeEach() { + + repository = mock(ScheduleCipherPoolDataRepository.class); + + serviceToTest = new ScheduleCipherPoolDataTransactionService(); + + serviceToTest.repository = repository; + + } + + @Test + void storeInOwnTransaction_calls_repository_save() throws Exception { + + /* prepare */ + ScheduleCipherPoolData data = mock(ScheduleCipherPoolData.class); + + /* execute */ + serviceToTest.storeInOwnTransaction(data); + + /* test */ + verify(repository).save(data); + + } + + @Test + void storeInOwnTransaction_returns_repository_object() throws Exception { + /* prepare */ + ScheduleCipherPoolData data = mock(ScheduleCipherPoolData.class); + when(repository.save(data)).thenReturn(data); + + /* execute */ + ScheduleCipherPoolData result = serviceToTest.storeInOwnTransaction(data); + + /* test */ + assertThat(result).isEqualTo(data); + + } + +} diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionPoolFactoryTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionPoolFactoryTest.java new file mode 100644 index 0000000000..a05529b9dd --- /dev/null +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionPoolFactoryTest.java @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.nio.charset.Charset; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import com.mercedesbenz.sechub.commons.encryption.EncryptionSupport; +import com.mercedesbenz.sechub.commons.encryption.InitializationVector; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipher; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherFactory; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherType; +import com.mercedesbenz.sechub.commons.encryption.SecretKeyProvider; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherAlgorithm; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherPasswordSourceType; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubSecretKeyProviderFactory; + +class ScheduleEncryptionPoolFactoryTest { + private ScheduleEncryptionPoolFactory factoryToTest; + private PersistentCipherFactory cipherFactory; + private EncryptionSupport encryptionSupport; + private SecHubSecretKeyProviderFactory secHubSecretKeyProviderFactory; + private SecretKeyProvider noneSecretKeyProvider; + private InitializationVector noneInitVector1; + private PersistentCipher noneCipher1; + private byte[] noneInitVector1Bytes;; + + @BeforeEach + void beforeEach() { + factoryToTest = new ScheduleEncryptionPoolFactory(); + + cipherFactory = mock(PersistentCipherFactory.class); + encryptionSupport = mock(EncryptionSupport.class); + secHubSecretKeyProviderFactory = mock(SecHubSecretKeyProviderFactory.class); + + factoryToTest.cipherFactory = cipherFactory; + factoryToTest.encryptionSupport = encryptionSupport; + factoryToTest.secHubSecretKeyProviderFactory = secHubSecretKeyProviderFactory; + + noneSecretKeyProvider = mock(SecretKeyProvider.class, "none-secret-keyprovider"); + when(secHubSecretKeyProviderFactory.createSecretKeyProvider(PersistentCipherType.NONE, SecHubCipherPasswordSourceType.NONE, null)) + .thenReturn(noneSecretKeyProvider); + + noneCipher1 = mock(PersistentCipher.class, "none-cipher1"); + when(cipherFactory.createCipher(noneSecretKeyProvider, PersistentCipherType.NONE)).thenReturn(noneCipher1); + when(noneCipher1.createNewInitializationVector()).thenReturn(noneInitVector1); + noneInitVector1 = mock(InitializationVector.class); + noneInitVector1Bytes = new byte[] {}; + when(noneInitVector1.getInitializationBytes()).thenReturn(noneInitVector1Bytes); + + } + + @Test + void createEncryptionPool_null_throws_illegal_argument() throws Exception { + + assertThatThrownBy(() -> factoryToTest.createEncryptionPool(null)).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("never be null"); + } + + @Test + void createEncryptionPool_empty_map_is_accepted() throws Exception { + + /* execute */ + ScheduleEncryptionPool pool = factoryToTest.createEncryptionPool(Collections.emptyList()); + + /* test */ + assertThat(pool).isNotNull(); + assertThat(pool.getCipherForPoolId(Long.valueOf(0))).isNull(); + + } + + @Test + void createEncryptionPool_map_with_valid_entry0_is_accepted() throws Exception { + + /* prepare */ + String plainText = "testdata-plaintext"; + byte[] noneEncrypted = plainText.getBytes(Charset.forName("UTF-8")); + List list = new ArrayList<>(); + + // create valid cipher pool data + ScheduleCipherPoolData data1 = new ScheduleCipherPoolData(); + data1.algorithm = SecHubCipherAlgorithm.NONE; + data1.secHubCipherPasswordSourceType = SecHubCipherPasswordSourceType.NONE; + data1.created = LocalDateTime.now(); + data1.createdFrom = "user1"; + data1.id = Long.valueOf(0); + data1.testEncrypted = noneEncrypted; + data1.testText = plainText; + data1.testInitialVector = noneInitVector1Bytes; + + list.add(data1); + + // simulate encryption - needed for internal validation + when(encryptionSupport.decryptString(eq(noneEncrypted), eq(noneCipher1), any(InitializationVector.class))).thenReturn(plainText); + + /* execute */ + ScheduleEncryptionPool pool = factoryToTest.createEncryptionPool(list); + + /* test */ + assertThat(pool).isNotNull(); + assertThat(pool.getCipherForPoolId(Long.valueOf(0))).isSameAs(noneCipher1); + + ArgumentCaptor initVectorCaptor = ArgumentCaptor.forClass(InitializationVector.class); + verify(encryptionSupport).decryptString(eq(noneEncrypted), eq(noneCipher1), initVectorCaptor.capture()); + + InitializationVector usedVector = initVectorCaptor.getValue(); + assertThat(usedVector.getInitializationBytes()).isEqualTo(noneInitVector1Bytes); + + } + + @Test + void createEncryptionPool_map_with_valid_entry0_but_wrong_decryption_is_NOT_accepted_throws_exception() throws Exception { + + /* prepare */ + String plainText = "testdata-plaintext"; + byte[] noneEncrypted = plainText.getBytes(Charset.forName("UTF-8")); + List list = new ArrayList<>(); + + // create valid cipher pool data + ScheduleCipherPoolData data1 = new ScheduleCipherPoolData(); + data1.algorithm = SecHubCipherAlgorithm.NONE; + data1.secHubCipherPasswordSourceType = SecHubCipherPasswordSourceType.NONE; + data1.created = LocalDateTime.now(); + data1.createdFrom = "user1"; + data1.id = Long.valueOf(0); + data1.testEncrypted = noneEncrypted; + data1.testText = plainText; + data1.testInitialVector = noneInitVector1Bytes; + + list.add(data1); + + // simulate encryption - needed for internal validation + when(encryptionSupport.decryptString(eq(noneEncrypted), eq(noneCipher1), any(InitializationVector.class))).thenReturn("wrong decrypted"); + + /* execute + test @formatter:off */ + assertThatThrownBy(() -> factoryToTest.createEncryptionPool(list)). + isInstanceOf(ScheduleEncryptionException.class). + hasMessageContaining("cipher pool"). + hasMessageContaining("cannot be handled"); + /* @formatter:on */ + + } + +} diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionPoolTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionPoolTest.java new file mode 100644 index 0000000000..1a0e24194b --- /dev/null +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionPoolTest.java @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import com.mercedesbenz.sechub.commons.encryption.PersistentCipher; + +class ScheduleEncryptionPoolTest { + + @Test + void constructor_from_map_results_in_expected_getCipherForPoolId_results() { + /* prepare */ + Map map = new HashMap<>(); + PersistentCipher cipher1 = mock(PersistentCipher.class); + PersistentCipher cipher2 = mock(PersistentCipher.class); + map.put(Long.valueOf(1), cipher1); + map.put(Long.valueOf(2), cipher2); + + /* execute */ + ScheduleEncryptionPool poolToTest = new ScheduleEncryptionPool(map); + + /* test */ + assertThat(poolToTest.getCipherForPoolId(Long.valueOf(0))).isNull(); + assertThat(poolToTest.getCipherForPoolId(Long.valueOf(1))).isEqualTo(cipher1); + assertThat(poolToTest.getCipherForPoolId(Long.valueOf(2))).isEqualTo(cipher2); + assertThat(poolToTest.getCipherForPoolId(Long.valueOf(3))).isNull(); + } + + @ParameterizedTest + @ValueSource(longs = { 0, -1, 99 }) + void constructor_from_null_map_results_in_pool_but_getCipherForPoolId_returns_always_null(long value) { + /* prepare */ + Map map = null; + + /* execute */ + ScheduleEncryptionPool poolToTest = new ScheduleEncryptionPool(map); + + /* test */ + assertThat(poolToTest.getCipherForPoolId(Long.valueOf(value))).isNull(); + } + + @ParameterizedTest + @ValueSource(longs = { 0, -1, 99 }) + void constructor_from_empty_map_results_in_pool_but_getCipherForPoolId_returns_always_null(long value) { + /* prepare */ + Map map = new HashMap<>(); + + /* execute */ + ScheduleEncryptionPool poolToTest = new ScheduleEncryptionPool(map); + + /* test */ + assertThat(poolToTest.getCipherForPoolId(Long.valueOf(value))).isNull(); + } + + @Test + void getAllPoolIds_returns_keys_of_pool_map() { + /* prepare */ + Map map = new HashMap<>(); + PersistentCipher cipher1 = mock(PersistentCipher.class); + PersistentCipher cipher2 = mock(PersistentCipher.class); + map.put(Long.valueOf(1), cipher1); + map.put(Long.valueOf(2), cipher2); + + ScheduleEncryptionPool poolToTest = new ScheduleEncryptionPool(map); + + /* execute */ + assertThat(poolToTest.getAllPoolIds()).contains(1L, 2L).hasSize(2); + } +} diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionResultTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionResultTest.java new file mode 100644 index 0000000000..0fcc622ea4 --- /dev/null +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionResultTest.java @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import static org.assertj.core.api.Assertions.*; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import com.mercedesbenz.sechub.commons.encryption.EncryptionResult; +import com.mercedesbenz.sechub.commons.encryption.InitializationVector; + +class ScheduleEncryptionResultTest { + + @ParameterizedTest + @ArgumentsSource(IllegalSchedulerEncryptionResultParameters.class) + void constructor_with_invalid_arguments_throws_illegal_argument_exception(Long poolId, EncryptionResult encryptionResult) { + + assertThatThrownBy(() -> new ScheduleEncryptionResult(poolId, encryptionResult)).isInstanceOf(IllegalArgumentException.class); + + } + + @Test + void enryption_result_with_valid_parameters_contains_the_information() { + Long poolId = Long.valueOf(0); + EncryptionResult encryptionResult = createValidEncryptionResult(); + + /* execute */ + ScheduleEncryptionResult scheduleEncryptionResult = new ScheduleEncryptionResult(poolId, encryptionResult); + + /* test */ + assertThat(scheduleEncryptionResult).isNotNull(); + assertThat(scheduleEncryptionResult.getEncryptedData()).isEqualTo(encryptionResult.getEncryptedData()); + assertThat(scheduleEncryptionResult.getInitialVector()).isEqualTo(encryptionResult.getInitialVector()); + assertThat(scheduleEncryptionResult.getCipherPoolId()).isEqualTo(poolId); + + } + + private static class IllegalSchedulerEncryptionResultParameters implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + return Stream.of(Arguments.of(null, null), Arguments.of(Long.valueOf(0), null), Arguments.of(null, createValidEncryptionResult()) + + ); + } + + } + + private static EncryptionResult createValidEncryptionResult() { + return new EncryptionResult("xxx".getBytes(), new InitializationVector("vector".getBytes())); + } + +} diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionRotationServiceTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionRotationServiceTest.java new file mode 100644 index 0000000000..a6d72a38e1 --- /dev/null +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionRotationServiceTest.java @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherAlgorithm; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherPasswordSourceType; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionData; + +class ScheduleEncryptionRotationServiceTest { + + private ScheduleEncryptionRotationService serviceToTest; + private ScheduleEncryptionService encryptionService; + private ScheduleCipherPoolDataTransactionService transactionService; + + @BeforeEach + void beforeEach() { + + encryptionService = mock(ScheduleEncryptionService.class); + + serviceToTest = new ScheduleEncryptionRotationService(); + + transactionService = mock(ScheduleCipherPoolDataTransactionService.class); + serviceToTest.transactionService = transactionService; + serviceToTest.encryptionService = encryptionService; + + } + + @Test + void createInitialCipherPoolData_stores_initial_cipher_pooldata_from_encryption_service_together_with_user_info() throws Exception { + + /* prepare */ + SecHubEncryptionData data = new SecHubEncryptionData(); + data.setAlgorithm(SecHubCipherAlgorithm.AES_GCM_SIV_128); + data.setPasswordSourceData("data1"); + data.setPasswordSourceType(SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE); + + ScheduleCipherPoolData createdPoolData = new ScheduleCipherPoolData(); + when(encryptionService.createInitialCipherPoolData(eq(data), any())).thenReturn(createdPoolData); + when(transactionService.storeInOwnTransaction(createdPoolData)).thenReturn(createdPoolData); + + /* execute */ + serviceToTest.startEncryptionRotation(data, "user1"); + + /* test */ + ArgumentCaptor entityCaptor = ArgumentCaptor.forClass(ScheduleCipherPoolData.class); + verify(transactionService).storeInOwnTransaction(entityCaptor.capture()); + + ScheduleCipherPoolData persistedPoolData = entityCaptor.getValue(); + assertThat(persistedPoolData.getCreatedFrom()).isEqualTo("user1"); // the stored entity contains the user information given to service + + } + +} diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionServiceTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionServiceTest.java new file mode 100644 index 0000000000..d908c9e2ec --- /dev/null +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionServiceTest.java @@ -0,0 +1,571 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import com.mercedesbenz.sechub.commons.encryption.EncryptionResult; +import com.mercedesbenz.sechub.commons.encryption.EncryptionSupport; +import com.mercedesbenz.sechub.commons.encryption.InitializationVector; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipher; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherFactory; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherType; +import com.mercedesbenz.sechub.domain.schedule.ScheduleShutdownService; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherAlgorithm; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherPasswordSourceType; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionData; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubSecretKeyProviderFactory; +import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessage; +import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessageService; +import com.mercedesbenz.sechub.sharedkernel.messaging.MessageID; + +class ScheduleEncryptionServiceTest { + + private ScheduleEncryptionService serviceToTest; + private SecHubSecretKeyProviderFactory secHubSecretKeyProviderFactory; + private EncryptionSupport encryptionSupport; + private PersistentCipherFactory cipherFactory; + private ScheduleEncryptionPoolFactory scheduleEncryptionPoolFactory; + private ScheduleEncryptionPool scheduleEncryptionPool; + private PersistentCipher fakedNoneCipher; + private ScheduleLatestCipherPoolDataCalculator scheduleLatestCipherPoolDataCalculator; + private ScheduleCipherPoolDataProvider poolDataProvider; + private PersistentCipher fakedAES256Cipher; + private DomainMessageService domainMessageService; + private SecHubOutdatedEncryptionPoolSupport outdatedEncryptionPoolSupport; + private ScheduleShutdownService shutdownService; + + @BeforeEach + void beforeEach() throws Exception { + serviceToTest = new ScheduleEncryptionService(); + + secHubSecretKeyProviderFactory = mock(SecHubSecretKeyProviderFactory.class); + when(secHubSecretKeyProviderFactory.createSecretKeyProvider(eq(PersistentCipherType.NONE), eq(SecHubCipherPasswordSourceType.NONE), any())) + .thenReturn(null); + + fakedNoneCipher = mock(PersistentCipher.class, "faked none cipher"); + fakedAES256Cipher = mock(PersistentCipher.class, "faked AES256 cipher"); + cipherFactory = mock(PersistentCipherFactory.class); + + when(cipherFactory.createCipher(null, PersistentCipherType.NONE)).thenReturn(fakedNoneCipher); + when(cipherFactory.createCipher(null, PersistentCipherType.AES_GCM_SIV_256)).thenReturn(fakedAES256Cipher); + + encryptionSupport = mock(EncryptionSupport.class); + + domainMessageService = mock(DomainMessageService.class); + + scheduleEncryptionPoolFactory = mock(ScheduleEncryptionPoolFactory.class); + scheduleEncryptionPool = mock(ScheduleEncryptionPool.class); + scheduleLatestCipherPoolDataCalculator = mock(ScheduleLatestCipherPoolDataCalculator.class); + poolDataProvider = mock(ScheduleCipherPoolDataProvider.class); + outdatedEncryptionPoolSupport = mock(SecHubOutdatedEncryptionPoolSupport.class); + shutdownService = mock(ScheduleShutdownService.class); + + serviceToTest.poolDataProvider = poolDataProvider; + serviceToTest.encryptionSupport = encryptionSupport; + serviceToTest.scheduleEncryptionPoolFactory = scheduleEncryptionPoolFactory; + serviceToTest.latestCipherPoolDataCalculator = scheduleLatestCipherPoolDataCalculator; + serviceToTest.secretKeyProviderFactory = secHubSecretKeyProviderFactory; + serviceToTest.cipherFactory = cipherFactory; + serviceToTest.domainMessageService = domainMessageService; + serviceToTest.outdatedEncryptionPoolSupport = outdatedEncryptionPoolSupport; + serviceToTest.shutdownService = shutdownService; + + when(scheduleEncryptionPoolFactory.createEncryptionPool(any())).thenReturn(scheduleEncryptionPool); + } + + @Test + void application_started_triggers_encryption_pool_refresh_event() throws Exception { + /* prepare */ + Long latestCipherPoolId = Long.valueOf(1); + when(scheduleLatestCipherPoolDataCalculator.calculateLatestPoolId(anyList())).thenReturn(latestCipherPoolId); + when(scheduleEncryptionPool.getCipherForPoolId(latestCipherPoolId)).thenReturn(fakedNoneCipher); + + /* execute */ + serviceToTest.applicationStarted(); + + /* test */ + assertEncryptionPoolInitEventSent(); + + } + + @Test + void createInitialCipherPoolData_working_as_expected() throws Exception { + + /* prepare */ + String testText = "testtext"; + + SecHubEncryptionData data = new SecHubEncryptionData(); + data.setAlgorithm(SecHubCipherAlgorithm.AES_GCM_SIV_256); + data.setPasswordSourceType(SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE); + data.setPasswordSourceData("SECRET_1"); + + byte[] initBytes = UUID.randomUUID().toString().getBytes(); + + EncryptionResult result = mock(EncryptionResult.class); + byte[] encryptedBytes = "faked-encryped-content".getBytes(Charset.forName("UTF-8")); + when(result.getEncryptedData()).thenReturn(encryptedBytes); + when(result.getInitialVector()).thenReturn(new InitializationVector(initBytes)); + + when(encryptionSupport.encryptString(eq(testText), eq(fakedAES256Cipher))).thenReturn(result); + when(encryptionSupport.decryptString(eq(encryptedBytes), eq(fakedAES256Cipher), eq(new InitializationVector(initBytes)))).thenReturn(testText); + + /* execute */ + ScheduleCipherPoolData createdPoolData = serviceToTest.createInitialCipherPoolData(data, testText); + + /* test */ + ArgumentCaptor testTextCaptor = ArgumentCaptor.forClass(String.class); + verify(encryptionSupport).encryptString(testTextCaptor.capture(), eq(fakedAES256Cipher)); + + String encryptedTestText = testTextCaptor.getValue(); + assertThat(encryptedTestText).isEqualTo(testText); + + assertThat(createdPoolData.getPasswordSourceType()).isEqualTo(SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE); + assertThat(createdPoolData.getTestEncrypted()).isEqualTo(encryptedBytes); + assertThat(createdPoolData.getPasswordSourceData()).isEqualTo("SECRET_1"); + assertThat(createdPoolData.getTestInitialVector()).isEqualTo(initBytes); + assertThat(createdPoolData.getTestText()).isEqualTo(testText); + assertThat(createdPoolData.getAlgorithm()).isEqualTo(SecHubCipherAlgorithm.AES_GCM_SIV_256); + assertThat(createdPoolData.getCreatedFrom()).isNull();// the user is NOT set, as defined in JavaDoc of method + assertThat(createdPoolData.getCreated()).isNotNull(); + + } + + @Test + void createInitialCipherPoolData_null_from_encryption_support_throws_exception() throws Exception { + + /* prepare */ + String testText = "testtext"; + + SecHubEncryptionData data = new SecHubEncryptionData(); + data.setAlgorithm(SecHubCipherAlgorithm.AES_GCM_SIV_256); + data.setPasswordSourceType(SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE); + data.setPasswordSourceData("SECRET_1"); + + byte[] initBytes = UUID.randomUUID().toString().getBytes(); + + EncryptionResult result = mock(EncryptionResult.class); + byte[] encryptedBytes = "faked-encryped-content".getBytes(Charset.forName("UTF-8")); + when(result.getEncryptedData()).thenReturn(encryptedBytes); + when(result.getInitialVector()).thenReturn(new InitializationVector(initBytes)); + + when(encryptionSupport.encryptString(eq(testText), eq(fakedAES256Cipher))).thenReturn(result); + when(encryptionSupport.decryptString(eq(encryptedBytes), eq(fakedAES256Cipher), eq(new InitializationVector(initBytes)))).thenReturn(null); + + /* execute + test */ + assertThatThrownBy(() -> serviceToTest.createInitialCipherPoolData(data, testText)).isInstanceOf(ScheduleEncryptionException.class) + .hasMessageContaining("decrypted value is null"); + + } + + @Test + void createInitialCipherPoolData_decrypted_test_text_not_as_origin_throws_exception() throws Exception { + + /* prepare */ + String testText = "testtext"; + + SecHubEncryptionData data = new SecHubEncryptionData(); + data.setAlgorithm(SecHubCipherAlgorithm.AES_GCM_SIV_256); + data.setPasswordSourceType(SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE); + data.setPasswordSourceData("SECRET_1"); + + byte[] initBytes = UUID.randomUUID().toString().getBytes(); + + EncryptionResult result = mock(EncryptionResult.class); + byte[] encryptedBytes = "faked-encryped-content".getBytes(Charset.forName("UTF-8")); + when(result.getEncryptedData()).thenReturn(encryptedBytes); + when(result.getInitialVector()).thenReturn(new InitializationVector(initBytes)); + + when(encryptionSupport.encryptString(eq(testText), eq(fakedAES256Cipher))).thenReturn(result); + when(encryptionSupport.decryptString(eq(encryptedBytes), eq(fakedAES256Cipher), eq(new InitializationVector(initBytes)))).thenReturn("wrong-result"); + + /* execute + test */ + assertThatThrownBy(() -> serviceToTest.createInitialCipherPoolData(data, testText)).isInstanceOf(ScheduleEncryptionException.class) + .hasMessageContaining("decrypted value is not origin test text"); + + } + + @Test + void refresh_pooldata_provider_says_same_poolids_last_cipher_id_kept() throws Exception { + + /* prepare */ + when(scheduleEncryptionPool.getAllPoolIds()).thenReturn(Set.of(5L)); + + // simulate originally setup done on startup; + long latestCipherPoolId = 4L; + serviceToTest.scheduleEncryptionPool = scheduleEncryptionPool; + serviceToTest.latestCipherPoolId = latestCipherPoolId;// just other value; + + // important part: contains exactly .... + when(poolDataProvider.isContainingExactlyGivenPoolIds(any())).thenReturn(true); + + /* execute */ + serviceToTest.refreshEncryptionPoolAndLatestPoolIdIfNecessary(); + + /* test */ + assertThat(serviceToTest.latestCipherPoolId).isEqualTo(latestCipherPoolId); // still same + assertNoDomainMessageEventSent(); + + } + + @Test + void refresh_pooldata_provider_says_same_poolids_no_new_pool_created_or_used() throws Exception { + + /* prepare */ + when(scheduleEncryptionPool.getAllPoolIds()).thenReturn(Set.of(5L)); + + // simulate originally setup done on startup; + long latestCipherPoolId = 4L; + serviceToTest.scheduleEncryptionPool = scheduleEncryptionPool; + serviceToTest.latestCipherPoolId = latestCipherPoolId;// just other value; + + // important part: contains exactly .... + when(poolDataProvider.isContainingExactlyGivenPoolIds(any())).thenReturn(true); + + /* execute */ + serviceToTest.refreshEncryptionPoolAndLatestPoolIdIfNecessary(); + + /* test */ + verify(scheduleEncryptionPoolFactory, never()).createEncryptionPool(any()); // never a new pool is created! + verify(scheduleEncryptionPool, never()).getCipherForPoolId(anyLong()); + assertThat(serviceToTest.scheduleEncryptionPool).isSameAs(scheduleEncryptionPool);// not changed + assertNoDomainMessageEventSent(); + + } + + @Test + void refresh_when_pool_ids_are_NOT_same_latest_cipher_pool_id_is_changed() throws Exception { + + /* prepare */ + ScheduleCipherPoolData poolData1 = mock(ScheduleCipherPoolData.class); + when(poolData1.getId()).thenReturn(Long.valueOf(4)); + List poolDataList = List.of(poolData1); + + when(poolDataProvider.ensurePoolDataAvailable()).thenReturn(poolDataList); + long oldCipherPoolId = 4L; + when(scheduleEncryptionPoolFactory.createEncryptionPool(poolDataList)).thenReturn(scheduleEncryptionPool); + + long newCipherPoolId = 5L; + when(scheduleEncryptionPool.getAllPoolIds()).thenReturn(Set.of(oldCipherPoolId, newCipherPoolId)); + when(scheduleEncryptionPool.getCipherForPoolId(newCipherPoolId)).thenReturn(fakedNoneCipher); + when(scheduleLatestCipherPoolDataCalculator.calculateLatestPoolId(poolDataList)).thenReturn(newCipherPoolId); + + // simulate originally setup done on startup; + serviceToTest.scheduleEncryptionPool = scheduleEncryptionPool; + serviceToTest.latestCipherPoolId = oldCipherPoolId; + + // important part: contains exactly .... + when(poolDataProvider.isContainingExactlyGivenPoolIds(any())).thenReturn(false); + + /* check preconditions */ + assertThat(serviceToTest.latestCipherPoolId).isEqualTo(oldCipherPoolId); + + /* execute */ + serviceToTest.refreshEncryptionPoolAndLatestPoolIdIfNecessary(); + + /* test */ + assertThat(serviceToTest.latestCipherPoolId).isEqualTo(newCipherPoolId); + + } + + @Test + void refresh_when_pool_ids_have_changed_a_new_pool_is_created_and_used() throws Exception { + + /* prepare */ + ScheduleCipherPoolData poolData1 = mock(ScheduleCipherPoolData.class); + when(poolData1.getId()).thenReturn(Long.valueOf(4)); + List poolDataList = List.of(poolData1); + + when(poolDataProvider.ensurePoolDataAvailable()).thenReturn(poolDataList); + long oldCipherPoolId = 4L; + + long newCipherPoolId = 5L; + when(scheduleEncryptionPool.getAllPoolIds()).thenReturn(Set.of(oldCipherPoolId, newCipherPoolId)); + + ScheduleEncryptionPool newEncryptionPool = mock(ScheduleEncryptionPool.class); + when(scheduleEncryptionPoolFactory.createEncryptionPool(poolDataList)).thenReturn(newEncryptionPool); + when(newEncryptionPool.getCipherForPoolId(newCipherPoolId)).thenReturn(fakedNoneCipher); + when(scheduleLatestCipherPoolDataCalculator.calculateLatestPoolId(poolDataList)).thenReturn(newCipherPoolId); + + // simulate originally setup done on startup; + serviceToTest.scheduleEncryptionPool = scheduleEncryptionPool; + serviceToTest.latestCipherPoolId = oldCipherPoolId; + + // important part: contains exactly .... + when(poolDataProvider.isContainingExactlyGivenPoolIds(any())).thenReturn(false); + + /* check preconditions */ + assertThat(serviceToTest.latestCipherPoolId).isEqualTo(oldCipherPoolId); + + /* execute */ + serviceToTest.refreshEncryptionPoolAndLatestPoolIdIfNecessary(); + + /* test */ + verify(scheduleEncryptionPoolFactory).createEncryptionPool(poolDataList); // a new pool is created! + assertThat(serviceToTest.scheduleEncryptionPool).isEqualTo(newEncryptionPool); // and set + assertEncryptionPoolInitEventSent(); // event must be sent! + + } + + @Test + void refresh_when_pool_ids_have_changed_but_new_pool_creation_fails_but_not_outdated_old_setup_is_kept() throws Exception { + + /* prepare */ + ScheduleCipherPoolData poolData1 = mock(ScheduleCipherPoolData.class); + when(poolData1.getId()).thenReturn(Long.valueOf(4)); + List poolDataList = List.of(poolData1); + + when(poolDataProvider.ensurePoolDataAvailable()).thenReturn(poolDataList); + long oldCipherPoolId = 4L; + + long newCipherPoolId = 5L; + when(scheduleEncryptionPool.getAllPoolIds()).thenReturn(Set.of(oldCipherPoolId, newCipherPoolId)); + + when(scheduleEncryptionPoolFactory.createEncryptionPool(poolDataList)) + .thenThrow(new ScheduleEncryptionException("was not able to create new encryption pool")); + when(scheduleLatestCipherPoolDataCalculator.calculateLatestPoolId(poolDataList)).thenReturn(newCipherPoolId); + + when(outdatedEncryptionPoolSupport.isOutdatedEncryptionStillAllowedOnThisClusterMember()).thenReturn(true); + // simulate originally setup done on startup; + serviceToTest.scheduleEncryptionPool = scheduleEncryptionPool; + serviceToTest.latestCipherPoolId = oldCipherPoolId; + + // important part: contains exactly .... + when(poolDataProvider.isContainingExactlyGivenPoolIds(any())).thenReturn(false); + + /* check preconditions */ + assertThat(serviceToTest.latestCipherPoolId).isEqualTo(oldCipherPoolId); + + /* execute */ + serviceToTest.refreshEncryptionPoolAndLatestPoolIdIfNecessary(); + + /* test */ + verify(scheduleEncryptionPoolFactory).createEncryptionPool(poolDataList); // it is tried to create a new pool + assertThat(serviceToTest.latestCipherPoolId).isEqualTo(oldCipherPoolId); // not changed + assertThat(serviceToTest.scheduleEncryptionPool).isEqualTo(scheduleEncryptionPool); // not changed + + assertNoDomainMessageEventSent(); // no event may be sent! + + verify(shutdownService, never()).shutdownApplication(); // no shutdown triggered + + } + + @Test + void refresh_when_pool_ids_have_changed_but_new_pool_creation_fails_but_too_long_outdated_shutdown_triggered() throws Exception { + + /* prepare */ + ScheduleCipherPoolData poolData1 = mock(ScheduleCipherPoolData.class); + when(poolData1.getId()).thenReturn(Long.valueOf(4)); + List poolDataList = List.of(poolData1); + + when(poolDataProvider.ensurePoolDataAvailable()).thenReturn(poolDataList); + long oldCipherPoolId = 4L; + + long newCipherPoolId = 5L; + when(scheduleEncryptionPool.getAllPoolIds()).thenReturn(Set.of(oldCipherPoolId, newCipherPoolId)); + + when(scheduleEncryptionPoolFactory.createEncryptionPool(poolDataList)) + .thenThrow(new ScheduleEncryptionException("was not able to create new encryption pool")); + when(scheduleLatestCipherPoolDataCalculator.calculateLatestPoolId(poolDataList)).thenReturn(newCipherPoolId); + when(outdatedEncryptionPoolSupport.isOutdatedEncryptionStillAllowedOnThisClusterMember()).thenReturn(false); + + // simulate originally setup done on startup; + serviceToTest.scheduleEncryptionPool = scheduleEncryptionPool; + serviceToTest.latestCipherPoolId = oldCipherPoolId; + + // important part: contains exactly .... + when(poolDataProvider.isContainingExactlyGivenPoolIds(any())).thenReturn(false); + + /* check preconditions */ + assertThat(serviceToTest.latestCipherPoolId).isEqualTo(oldCipherPoolId); + + /* execute */ + serviceToTest.refreshEncryptionPoolAndLatestPoolIdIfNecessary(); + + /* test */ + verify(scheduleEncryptionPoolFactory).createEncryptionPool(poolDataList); // it is tried to create a new pool + assertThat(serviceToTest.latestCipherPoolId).isEqualTo(oldCipherPoolId); // not changed + assertThat(serviceToTest.scheduleEncryptionPool).isEqualTo(scheduleEncryptionPool); // not changed + + assertNoDomainMessageEventSent(); // no event may be sent! + verify(shutdownService).shutdownApplication(); + + } + + @ParameterizedTest + @ValueSource(longs = { 0, 1, 3270 }) + void applicationStarted_latest_cipher_pool_id_is_resolved(long value) throws Exception { + + /* prepare */ + Long latestCipherPoolId = Long.valueOf(value); + when(scheduleLatestCipherPoolDataCalculator.calculateLatestPoolId(anyList())).thenReturn(latestCipherPoolId); + when(scheduleEncryptionPool.getCipherForPoolId(latestCipherPoolId)).thenReturn(fakedNoneCipher); + + /* execute */ + serviceToTest.applicationStarted(); + + /* test */ + assertThat(serviceToTest.latestCipherPoolId).isEqualTo(latestCipherPoolId); + + } + + @ParameterizedTest + @ValueSource(longs = { 0, 1, 3270 }) + void applicationStarted_encryption_pool_has_not_latest_pool_id_inside_throws_illegal_state(long value) { + /* prepare */ + Long latestPoolId = Long.valueOf(value); + when(scheduleEncryptionPool.getCipherForPoolId(latestPoolId)).thenReturn(null); + when(scheduleLatestCipherPoolDataCalculator.calculateLatestPoolId(anyList())).thenReturn(latestPoolId); + + /* execute + test */ + assertThatThrownBy(() -> serviceToTest.applicationStarted()).isInstanceOf(IllegalStateException.class) + .hasMessage("Encryption pool has no entry for latest cipher pool id: %d", latestPoolId); + + } + + @Test + void applicationStarted_encryption_pool_is_created_by_factory_with_data_from_pooldataprovider() throws Exception { + /* prepare */ + Long latestCipherPoolId = Long.valueOf(12); + List list = new ArrayList<>(); + + when(scheduleLatestCipherPoolDataCalculator.calculateLatestPoolId(anyList())).thenReturn(latestCipherPoolId); + when(scheduleEncryptionPool.getCipherForPoolId(latestCipherPoolId)).thenReturn(fakedNoneCipher); + when(poolDataProvider.ensurePoolDataAvailable()).thenReturn(list); + when(scheduleEncryptionPoolFactory.createEncryptionPool(list)).thenReturn(scheduleEncryptionPool); + + /* execute + test */ + serviceToTest.applicationStarted(); + + verify(poolDataProvider).ensurePoolDataAvailable(); + verify(scheduleEncryptionPoolFactory).createEncryptionPool(list); + verify(scheduleEncryptionPool).getCipherForPoolId(latestCipherPoolId); // the created pool is used + } + + @Test + void encryptWithLatestCipher_calls_encryptionsupport_with_latest_cipher_and_uses_result() throws Exception { + /* prepare */ + String stringToEncrypt = "please-encrypt-me"; + Long latestCipherPoolId = Long.valueOf(12); + + when(scheduleLatestCipherPoolDataCalculator.calculateLatestPoolId(anyList())).thenReturn(latestCipherPoolId); + when(scheduleEncryptionPool.getCipherForPoolId(latestCipherPoolId)).thenReturn(fakedNoneCipher); + + // fake encryption by cipher + EncryptionResult encryptionResultFromSupport = mock(EncryptionResult.class); + byte[] encryptedData = "i-am-encrypted".getBytes(); + InitializationVector initialVector = mock(InitializationVector.class); + when(encryptionResultFromSupport.getEncryptedData()).thenReturn(encryptedData); + when(encryptionResultFromSupport.getInitialVector()).thenReturn(initialVector); + + when(encryptionSupport.encryptString(stringToEncrypt, fakedNoneCipher)).thenReturn(encryptionResultFromSupport); + + // simulate spring startup - necessary to get pool filled. + serviceToTest.applicationStarted(); + + /* execute */ + ScheduleEncryptionResult result = serviceToTest.encryptWithLatestCipher(stringToEncrypt); + + /* test */ + verify(encryptionSupport).encryptString(stringToEncrypt, fakedNoneCipher); + + assertThat(result.getCipherPoolId()).isEqualTo(latestCipherPoolId); + assertThat(result.getEncryptedData()).isEqualTo(encryptedData); + assertThat(result.getInitialVector()).isEqualTo(initialVector); + + } + + @Test + void decrypt_with_latest_cipher_for_decryption() throws Exception { + /* prepare */ + byte[] encryptedData = "i-am-encrypted".getBytes(); + String decryptedString = "please-encrypt-me"; + Long latestCipherPoolId = Long.valueOf(12); + Long usedCipherPoolId = latestCipherPoolId; + PersistentCipher cipherForDecryption = fakedNoneCipher; + + // necessary to get startup not failing for missing latest pool id: + when(scheduleLatestCipherPoolDataCalculator.calculateLatestPoolId(anyList())).thenReturn(latestCipherPoolId); + when(scheduleEncryptionPool.getCipherForPoolId(latestCipherPoolId)).thenReturn(fakedNoneCipher); + + // fake encryption by cipher + EncryptionResult encryptionResultFromSupport = mock(EncryptionResult.class); + InitializationVector initialVector = mock(InitializationVector.class); + when(encryptionResultFromSupport.getEncryptedData()).thenReturn(encryptedData); + when(encryptionResultFromSupport.getInitialVector()).thenReturn(initialVector); + + when(encryptionSupport.decryptString(encryptedData, cipherForDecryption, initialVector)).thenReturn(decryptedString); + + // simulate spring startup - necessary to get pool filled. + serviceToTest.applicationStarted(); + + /* execute */ + String decrypted = serviceToTest.decryptToString(encryptedData, usedCipherPoolId, initialVector); + + /* test */ + verify(encryptionSupport).decryptString(encryptedData, cipherForDecryption, initialVector); + assertThat(decrypted).isEqualTo(decryptedString); + + } + + @Test + void decrypt_with_other_cipher_for_decryption() throws Exception { + /* prepare */ + byte[] encryptedData = "i-am-encrypted".getBytes(); + String decryptedString = "please-encrypt-me"; + Long latestCipherPoolId = Long.valueOf(12); + Long usedCipherPoolId = Long.valueOf(1); + PersistentCipher cipherForDecryption = mock(PersistentCipher.class, "cipher for decryption"); + + // necessary to get startup not failing for missing latest pool id: + when(scheduleLatestCipherPoolDataCalculator.calculateLatestPoolId(anyList())).thenReturn(latestCipherPoolId); + when(scheduleEncryptionPool.getCipherForPoolId(latestCipherPoolId)).thenReturn(fakedNoneCipher); + + // but we use here an "older" cipher pool entry + when(scheduleEncryptionPool.getCipherForPoolId(usedCipherPoolId)).thenReturn(cipherForDecryption); + + // fake encryption by cipher + EncryptionResult encryptionResultFromSupport = mock(EncryptionResult.class); + InitializationVector initialVector = mock(InitializationVector.class); + when(encryptionResultFromSupport.getEncryptedData()).thenReturn(encryptedData); + when(encryptionResultFromSupport.getInitialVector()).thenReturn(initialVector); + + when(encryptionSupport.decryptString(encryptedData, cipherForDecryption, initialVector)).thenReturn(decryptedString); + + // simulate spring startup - necessary to get pool filled. + serviceToTest.applicationStarted(); + + /* execute */ + String decrypted = serviceToTest.decryptToString(encryptedData, usedCipherPoolId, initialVector); + + /* test */ + verify(encryptionSupport).decryptString(encryptedData, cipherForDecryption, initialVector); + assertThat(decrypted).isEqualTo(decryptedString); + + } + + void assertNoDomainMessageEventSent() { + verify(domainMessageService, never()).sendAsynchron(any(DomainMessage.class)); + } + + void assertEncryptionPoolInitEventSent() { + ArgumentCaptor captor = ArgumentCaptor.forClass(DomainMessage.class); + verify(domainMessageService).sendAsynchron(captor.capture()); + + DomainMessage sentDomainMessage = captor.getValue(); + assertThat(sentDomainMessage.getMessageId()).isEqualTo(MessageID.SCHEDULE_ENCRYPTION_POOL_INITIALIZED); + } +} \ No newline at end of file diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionStatusServiceTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionStatusServiceTest.java new file mode 100644 index 0000000000..7c6639bd61 --- /dev/null +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleEncryptionStatusServiceTest.java @@ -0,0 +1,186 @@ +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import static org.assertj.core.api.Assertions.*; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; + +import java.time.LocalDateTime; +import java.util.Iterator; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.mercedesbenz.sechub.commons.model.job.ExecutionState; +import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobRepository; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherAlgorithm; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubCipherPasswordSourceType; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubDomainEncryptionData; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubDomainEncryptionStatus; + +class ScheduleEncryptionStatusServiceTest { + + private ScheduleEncryptionStatusService serviceToTest; + private ScheduleCipherPoolDataRepository poolDataRepository; + private SecHubJobRepository jobRepository; + + @BeforeEach + void beforeEach() { + + poolDataRepository = mock(ScheduleCipherPoolDataRepository.class); + jobRepository = mock(SecHubJobRepository.class); + + serviceToTest = new ScheduleEncryptionStatusService(); + serviceToTest.poolDataRepository = poolDataRepository; + serviceToTest.jobRepository = jobRepository; + } + + @Test + void createEncryptionStatus__no_pool_data() { + /* prepare */ + when(poolDataRepository.findAll()).thenReturn(List.of()); + + /* execute */ + SecHubDomainEncryptionStatus status = serviceToTest.createEncryptionStatus(); + + /* test */ + assertThat(status).isNotNull(); + assertThat(status.getData()).isEmpty(); + + } + + @Test + void createEncryptionStatus__one_pool_data() { + + /* prepare */ + ScheduleCipherPoolData poolData1 = mock(ScheduleCipherPoolData.class); + LocalDateTime creationTime = LocalDateTime.now(); + String user = "user1"; + String passwordSourceData = "the-data"; + SecHubCipherPasswordSourceType passwordSourceType = SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE; + long poolid = 33L; + + long count1Initializing = 1L; + long count1Canceled = 20L; + long count1Ended = 1023L; + + when(poolData1.getId()).thenReturn(poolid); + when(poolData1.getAlgorithm()).thenReturn(SecHubCipherAlgorithm.AES_GCM_SIV_128); + when(poolData1.getCreated()).thenReturn(creationTime); + when(poolData1.getCreatedFrom()).thenReturn(user); + when(poolData1.getPasswordSourceData()).thenReturn(passwordSourceData); + when(poolData1.getPasswordSourceType()).thenReturn(passwordSourceType); + + when(poolDataRepository.findAll()).thenReturn(List.of(poolData1)); + when(jobRepository.countJobsInExecutionStateAndEncryptedWithPoolId(ExecutionState.INITIALIZING, poolid)).thenReturn(count1Initializing); + when(jobRepository.countJobsInExecutionStateAndEncryptedWithPoolId(ExecutionState.CANCELED, poolid)).thenReturn(count1Canceled); + when(jobRepository.countJobsInExecutionStateAndEncryptedWithPoolId(ExecutionState.ENDED, poolid)).thenReturn(count1Ended); + + /* execute */ + SecHubDomainEncryptionStatus status = serviceToTest.createEncryptionStatus(); + + /* test */ + assertThat(status).isNotNull(); + assertThat(status.getData()).isNotEmpty().hasSize(1); + + SecHubDomainEncryptionData data1 = status.getData().iterator().next(); + assertThat(data1.getId()).isEqualTo(String.valueOf(poolid)); + assertThat(data1.getCreated()).isEqualTo(creationTime); + assertThat(data1.getCreatedFrom()).isEqualTo(user); + assertThat(data1.getPasswordSource().getType()).isEqualTo(passwordSourceType); + assertThat(data1.getPasswordSource().getData()).isEqualTo(passwordSourceData); + + assertThat(data1.getUsage()).containsEntry("job.state.canceled", count1Canceled); + assertThat(data1.getUsage()).containsEntry("job.state.initializing", count1Initializing); + assertThat(data1.getUsage()).containsEntry("job.state.ended", count1Ended); + + } + + @Test + void createEncryptionStatus__two_pool_data() { + + /* prepare */ + ScheduleCipherPoolData poolData0 = mock(ScheduleCipherPoolData.class); + LocalDateTime creationTime0 = LocalDateTime.now(); + String user0 = "user0"; + String passwordSourceData0 = "the-data0"; + SecHubCipherPasswordSourceType passwordSourceType0 = SecHubCipherPasswordSourceType.NONE; + long poolid0 = 12; + + long count0Initializing = 10L; + long count0Canceled = 30L; + long count0Ended = 2023L; + long count0CancelRequest = 3; + + when(poolData0.getId()).thenReturn(poolid0); + when(poolData0.getAlgorithm()).thenReturn(SecHubCipherAlgorithm.AES_GCM_SIV_128); + when(poolData0.getCreated()).thenReturn(creationTime0); + when(poolData0.getCreatedFrom()).thenReturn(user0); + when(poolData0.getPasswordSourceData()).thenReturn(passwordSourceData0); + when(poolData0.getPasswordSourceType()).thenReturn(passwordSourceType0); + + when(jobRepository.countJobsInExecutionStateAndEncryptedWithPoolId(ExecutionState.INITIALIZING, poolid0)).thenReturn(count0Initializing); + when(jobRepository.countJobsInExecutionStateAndEncryptedWithPoolId(ExecutionState.CANCELED, poolid0)).thenReturn(count0Canceled); + when(jobRepository.countJobsInExecutionStateAndEncryptedWithPoolId(ExecutionState.ENDED, poolid0)).thenReturn(count0Ended); + when(jobRepository.countJobsInExecutionStateAndEncryptedWithPoolId(ExecutionState.CANCEL_REQUESTED, poolid0)).thenReturn(count0CancelRequest); + + ScheduleCipherPoolData poolData1 = mock(ScheduleCipherPoolData.class); + LocalDateTime creationTime1 = LocalDateTime.now(); + String user = "user1"; + String passwordSourceData1 = "the-data"; + SecHubCipherPasswordSourceType passwordSourceType1 = SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE; + long poolid1 = 33L; + + long count1Initializing = 1L; + long count1Canceled = 20L; + long count1Ended = 1023L; + + when(poolData1.getId()).thenReturn(poolid1); + when(poolData1.getAlgorithm()).thenReturn(SecHubCipherAlgorithm.AES_GCM_SIV_128); + when(poolData1.getCreated()).thenReturn(creationTime1); + when(poolData1.getCreatedFrom()).thenReturn(user); + when(poolData1.getPasswordSourceData()).thenReturn(passwordSourceData1); + when(poolData1.getPasswordSourceType()).thenReturn(passwordSourceType1); + + when(jobRepository.countJobsInExecutionStateAndEncryptedWithPoolId(ExecutionState.INITIALIZING, poolid1)).thenReturn(count1Initializing); + when(jobRepository.countJobsInExecutionStateAndEncryptedWithPoolId(ExecutionState.CANCELED, poolid1)).thenReturn(count1Canceled); + when(jobRepository.countJobsInExecutionStateAndEncryptedWithPoolId(ExecutionState.ENDED, poolid1)).thenReturn(count1Ended); + + when(poolDataRepository.findAll()).thenReturn(List.of(poolData0, poolData1)); + + /* execute */ + SecHubDomainEncryptionStatus status = serviceToTest.createEncryptionStatus(); + + /* test */ + assertThat(status).isNotNull(); + assertThat(status.getData()).isNotEmpty().hasSize(2); + + Iterator iterator = status.getData().iterator(); + SecHubDomainEncryptionData data0 = iterator.next(); + assertThat(data0.getId()).isEqualTo(String.valueOf(poolid0)); + assertThat(data0.getCreated()).isEqualTo(creationTime0); + assertThat(data0.getCreatedFrom()).isEqualTo(user0); + assertThat(data0.getPasswordSource().getType()).isEqualTo(passwordSourceType0); + assertThat(data0.getPasswordSource().getData()).isEqualTo(passwordSourceData0); + + assertThat(data0.getUsage()).containsEntry("job.state.canceled", count0Canceled); + assertThat(data0.getUsage()).containsEntry("job.state.initializing", count0Initializing); + assertThat(data0.getUsage()).containsEntry("job.state.ended", count0Ended); + assertThat(data0.getUsage()).containsEntry("job.state.cancel_requested", count0CancelRequest); + + SecHubDomainEncryptionData data1 = iterator.next(); + assertThat(data1.getId()).isEqualTo(String.valueOf(poolid1)); + assertThat(data1.getCreated()).isEqualTo(creationTime1); + assertThat(data1.getCreatedFrom()).isEqualTo(user); + assertThat(data1.getPasswordSource().getType()).isEqualTo(passwordSourceType1); + assertThat(data1.getPasswordSource().getData()).isEqualTo(passwordSourceData1); + + assertThat(data1.getUsage()).containsEntry("job.state.canceled", count1Canceled); + assertThat(data1.getUsage()).containsEntry("job.state.initializing", count1Initializing); + assertThat(data1.getUsage()).containsEntry("job.state.ended", count1Ended); + + } + +} diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleLatestCipherPoolDataCalculatorTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleLatestCipherPoolDataCalculatorTest.java new file mode 100644 index 0000000000..a28b5a754b --- /dev/null +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleLatestCipherPoolDataCalculatorTest.java @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +class ScheduleLatestCipherPoolDataCalculatorTest { + + @Test + void null_list_results_in_latest_pool_id_null() { + + /* prepare */ + ScheduleLatestCipherPoolDataCalculator calculatorToTest = new ScheduleLatestCipherPoolDataCalculator(); + + List entries = null; + + /* execute */ + Long result = calculatorToTest.calculateLatestPoolId(entries); + + /* test */ + assertThat(result).isNull(); + } + + @Test + void null_list_results_in_latest_pool_id_null_object_variant() { + + /* prepare */ + ScheduleLatestCipherPoolDataCalculator calculatorToTest = new ScheduleLatestCipherPoolDataCalculator(); + + List entries = null; + + /* execute */ + ScheduleCipherPoolData result = calculatorToTest.calculateLatestPoolData(entries); + + /* test */ + assertThat(result).isNull(); + } + + @Test + void empty_list_results_in_latest_pool_id_null() { + + /* prepare */ + ScheduleLatestCipherPoolDataCalculator calculatorToTest = new ScheduleLatestCipherPoolDataCalculator(); + + List entries = new ArrayList<>(); + + /* execute */ + Long result = calculatorToTest.calculateLatestPoolId(entries); + + /* test */ + assertThat(result).isNull(); + } + + @Test + void empty_list_results_in_latest_pool_id_null_object_variant() { + + /* prepare */ + ScheduleLatestCipherPoolDataCalculator calculatorToTest = new ScheduleLatestCipherPoolDataCalculator(); + + List entries = new ArrayList<>(); + + /* execute */ + ScheduleCipherPoolData result = calculatorToTest.calculateLatestPoolData(entries); + + /* test */ + assertThat(result).isNull(); + } + + @Test + void two_entries_newer_one_resolved() { + + /* prepare */ + ScheduleLatestCipherPoolDataCalculator calculatorToTest = new ScheduleLatestCipherPoolDataCalculator(); + + // create valid cipher pool data + ScheduleCipherPoolData data1 = new ScheduleCipherPoolData(); + data1.id = Long.valueOf(0); + data1.created = LocalDateTime.now().minusDays(1); + + ScheduleCipherPoolData data2 = new ScheduleCipherPoolData(); + data2.id = Long.valueOf(1); + data2.created = LocalDateTime.now(); + + List entries = new ArrayList<>(); + entries.add(data1); + entries.add(data2); + + /* execute */ + Long result = calculatorToTest.calculateLatestPoolId(entries); + + /* test */ + assertThat(result).describedAs("data2 is the newer one").isEqualTo(data2.id); + } + + @Test + void three_entries_newer_resolved_no_matter_that_inside_middle() { + + /* prepare */ + ScheduleLatestCipherPoolDataCalculator calculatorToTest = new ScheduleLatestCipherPoolDataCalculator(); + + // create valid cipher pool data + ScheduleCipherPoolData data1 = new ScheduleCipherPoolData(); + data1.id = Long.valueOf(0); + data1.created = LocalDateTime.now().minusDays(2); + + ScheduleCipherPoolData data2 = new ScheduleCipherPoolData(); + data2.id = Long.valueOf(1); + data2.created = LocalDateTime.now().minusDays(1); + + ScheduleCipherPoolData data3 = new ScheduleCipherPoolData(); + data3.id = Long.valueOf(2); + data3.created = LocalDateTime.now(); + + List entries = new ArrayList<>(); + entries.add(data1); + entries.add(data3); + entries.add(data2); + + /* execute */ + Long result = calculatorToTest.calculateLatestPoolId(entries); + + /* test */ + assertThat(result).describedAs("data3 is the newer one - even when added in middle of list").isEqualTo(data3.id); + } + + @Test + void three_entries_newer_resolved_no_matter_that_inside_middle_object_variant() { + + /* prepare */ + ScheduleLatestCipherPoolDataCalculator calculatorToTest = new ScheduleLatestCipherPoolDataCalculator(); + + // create valid cipher pool data + ScheduleCipherPoolData data1 = new ScheduleCipherPoolData(); + data1.id = Long.valueOf(0); + data1.created = LocalDateTime.now().minusDays(2); + + ScheduleCipherPoolData data2 = new ScheduleCipherPoolData(); + data2.id = Long.valueOf(1); + data2.created = LocalDateTime.now().minusDays(1); + + ScheduleCipherPoolData data3 = new ScheduleCipherPoolData(); + data3.id = Long.valueOf(2); + data3.created = LocalDateTime.now(); + + List entries = new ArrayList<>(); + entries.add(data1); + entries.add(data3); + entries.add(data2); + + /* execute */ + ScheduleCipherPoolData result = calculatorToTest.calculateLatestPoolData(entries); + + /* test */ + assertThat(result).describedAs("data3 is the newer one - even when added in middle of list").isEqualTo(data3); + } + +} diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleRefreshEncryptionServiceSetupTriggerServiceTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleRefreshEncryptionServiceSetupTriggerServiceTest.java new file mode 100644 index 0000000000..90720d95d4 --- /dev/null +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/ScheduleRefreshEncryptionServiceSetupTriggerServiceTest.java @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.Test; + +class ScheduleRefreshEncryptionServiceSetupTriggerServiceTest { + + private ScheduleRefreshEncryptionServiceSetupTriggerService serviceToTest; + private ScheduleEncryptionService encryptionService; + + @Test + void triggers_refresh_on_encryption_service() { + /* prepare */ + encryptionService = mock(ScheduleEncryptionService.class); + + serviceToTest = new ScheduleRefreshEncryptionServiceSetupTriggerService(); + serviceToTest.encryptionService = encryptionService; + + /* execute */ + serviceToTest.triggerEncryptionSetupRefresh(); + + /* test */ + verify(encryptionService).refreshEncryptionPoolAndLatestPoolIdIfNecessary(); + } + +} diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/SecHubOutdatedEncryptionPoolSupportTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/SecHubOutdatedEncryptionPoolSupportTest.java new file mode 100644 index 0000000000..d4ffe93502 --- /dev/null +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/encryption/SecHubOutdatedEncryptionPoolSupportTest.java @@ -0,0 +1,370 @@ +package com.mercedesbenz.sechub.domain.schedule.encryption; + +import static java.time.LocalDateTime.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import com.mercedesbenz.sechub.sharedkernel.SystemTimeProvider; + +class SecHubOutdatedEncryptionPoolSupportTest { + + private SecHubOutdatedEncryptionPoolSupport supportToTest; + private SystemTimeProvider systemtimeProvider; + private ScheduleCipherPoolDataProvider poolDataProvider; + private ScheduleLatestCipherPoolDataCalculator latestCipherPoolDataCalculator; + + @BeforeEach + void beforeEach() { + systemtimeProvider = mock(SystemTimeProvider.class); + poolDataProvider = mock(ScheduleCipherPoolDataProvider.class); + latestCipherPoolDataCalculator = mock(ScheduleLatestCipherPoolDataCalculator.class); + + supportToTest = new SecHubOutdatedEncryptionPoolSupport(); + supportToTest.systemtimeProvider = systemtimeProvider; + supportToTest.poolDataProvider = poolDataProvider; + supportToTest.latestCipherPoolDataCalculator = latestCipherPoolDataCalculator; + } + + @ParameterizedTest + @ArgumentsSource(OutdatedEncryptionPoolOnThisClusterMemberAllowedArgumentProvider.class) + @ArgumentsSource(OutdatedEncryptionPoolOnThisClusterMemberNotAllowedArgumentProvider.class) + void outdated_encryption_on_cluster_member_NOT_allowed_when_no_latest_pooldata(LocalDateTime now, long outdatedAcceptedMillis, + LocalDateTime latestPoolIdCreationTimestamp) throws Exception { + + /* prepare */ + when(systemtimeProvider.getNow()).thenReturn(now); + + List list = List.of(); // empty list... + when(poolDataProvider.ensurePoolDataAvailable()).thenReturn(list); + when(latestCipherPoolDataCalculator.calculateLatestPoolData(list)).thenReturn(null); + + supportToTest.acceptOutdatedEncryptionPoolInMilliseconds = outdatedAcceptedMillis; + + /* execute */ + boolean result = supportToTest.isOutdatedEncryptionStillAllowedOnThisClusterMember(); + + /* test */ + assertThat(result).isFalse(); + + } + + @ParameterizedTest + @ArgumentsSource(OutdatedEncryptionPoolOnThisClusterMemberNotAllowedArgumentProvider.class) + void outdated_encryption_on_cluster_member_NOT_allowed(LocalDateTime now, long outdatedAcceptedMillis, + LocalDateTime latestPoolIdCreationTimestamp) throws Exception { + + /* prepare */ + when(systemtimeProvider.getNow()).thenReturn(now); + + ScheduleCipherPoolData latestPoolData = mock(ScheduleCipherPoolData.class); + when(latestPoolData.getCreated()).thenReturn(latestPoolIdCreationTimestamp); + List list = List.of(latestPoolData); + when(poolDataProvider.ensurePoolDataAvailable()).thenReturn(list); + when(latestCipherPoolDataCalculator.calculateLatestPoolData(list)).thenReturn(latestPoolData); + + supportToTest.acceptOutdatedEncryptionPoolInMilliseconds = outdatedAcceptedMillis; + + /* execute */ + boolean result = supportToTest.isOutdatedEncryptionStillAllowedOnThisClusterMember(); + + /* test */ + assertThat(result).isFalse(); + + } + + @ParameterizedTest + @ArgumentsSource(OutdatedEncryptionPoolOnThisClusterMemberAllowedArgumentProvider.class) + void outdated_encryption_on_cluster_member_allowed(LocalDateTime now, long outdatedAcceptedMillis, + LocalDateTime latestPoolIdCreationTimestamp) throws Exception { + + /* prepare */ + when(systemtimeProvider.getNow()).thenReturn(now); + + ScheduleCipherPoolData latestPoolData = mock(ScheduleCipherPoolData.class); + when(latestPoolData.getCreated()).thenReturn(latestPoolIdCreationTimestamp); + List list = List.of(latestPoolData); + when(poolDataProvider.ensurePoolDataAvailable()).thenReturn(list); + when(latestCipherPoolDataCalculator.calculateLatestPoolData(list)).thenReturn(latestPoolData); + + supportToTest.acceptOutdatedEncryptionPoolInMilliseconds = outdatedAcceptedMillis; + + /* execute */ + boolean result = supportToTest.isOutdatedEncryptionStillAllowedOnThisClusterMember(); + + /* test */ + assertThat(result).isTrue(); + + } + + @ParameterizedTest + @ArgumentsSource(OutdatedEncryptionPoolInClusterPossibleArgumentProvider.class) + @ArgumentsSource(OutdatedEncryptionPoolInClusterNotPossibleArgumentProvider.class) + void outdated_encryption_pool_in_cluster_NOT_possible_when_no_latest_pooldata(LocalDateTime now, long outdatedAcceptedMillis, + LocalDateTime latestPoolIdCreationTimestamp, long refreshIntervalMillis, long refreshInitialDelayMillis) throws Exception { + + /* prepare */ + when(systemtimeProvider.getNow()).thenReturn(now); + + List list = List.of(); // empty list... + when(poolDataProvider.ensurePoolDataAvailable()).thenReturn(list); + when(latestCipherPoolDataCalculator.calculateLatestPoolData(list)).thenReturn(null); + + supportToTest.acceptOutdatedEncryptionPoolInMilliseconds = outdatedAcceptedMillis; + supportToTest.encryptionRefreshFixedDelayInMilliseconds = refreshIntervalMillis; + supportToTest.encryptionRefreshInitialDelayInMilliseconds = refreshInitialDelayMillis; + + /* execute */ + boolean result = supportToTest.isOutdatedEncryptionPoolPossibleInCluster(); + + /* test */ + assertThat(result).isFalse(); + + } + + @ParameterizedTest + @ArgumentsSource(OutdatedEncryptionPoolInClusterNotPossibleArgumentProvider.class) + void outdated_encryption_pool_in_cluster__NOT_possible(LocalDateTime now, long outdatedAcceptedMillis, + LocalDateTime latestPoolIdCreationTimestamp, long refreshIntervalMillis, long refreshInitialDelayMillis) throws Exception { + + /* prepare */ + when(systemtimeProvider.getNow()).thenReturn(now); + + ScheduleCipherPoolData latestPoolData = mock(ScheduleCipherPoolData.class); + when(latestPoolData.getCreated()).thenReturn(latestPoolIdCreationTimestamp); + List list = List.of(latestPoolData); + when(poolDataProvider.ensurePoolDataAvailable()).thenReturn(list); + when(latestCipherPoolDataCalculator.calculateLatestPoolData(list)).thenReturn(latestPoolData); + + supportToTest.acceptOutdatedEncryptionPoolInMilliseconds = outdatedAcceptedMillis; + supportToTest.encryptionRefreshFixedDelayInMilliseconds = refreshIntervalMillis; + supportToTest.encryptionRefreshInitialDelayInMilliseconds = refreshInitialDelayMillis; + + /* execute */ + boolean result = supportToTest.isOutdatedEncryptionPoolPossibleInCluster(); + + /* test */ + assertThat(result).isFalse(); + + } + + @ParameterizedTest + @ArgumentsSource(OutdatedEncryptionPoolInClusterPossibleArgumentProvider.class) + void outdated_encryption_pool_in_cluster__possible(LocalDateTime now, long outdatedAcceptedMillis, + LocalDateTime latestPoolIdCreationTimestamp, long refreshIntervalMillis, long refreshInitialDelayMillis) throws Exception { + + /* prepare */ + when(systemtimeProvider.getNow()).thenReturn(now); + + ScheduleCipherPoolData latestPoolData = mock(ScheduleCipherPoolData.class); + when(latestPoolData.getCreated()).thenReturn(latestPoolIdCreationTimestamp); + List list = List.of(latestPoolData); + when(poolDataProvider.ensurePoolDataAvailable()).thenReturn(list); + when(latestCipherPoolDataCalculator.calculateLatestPoolData(list)).thenReturn(latestPoolData); + + supportToTest.acceptOutdatedEncryptionPoolInMilliseconds = outdatedAcceptedMillis; + supportToTest.encryptionRefreshFixedDelayInMilliseconds = refreshIntervalMillis; + supportToTest.encryptionRefreshInitialDelayInMilliseconds = refreshInitialDelayMillis; + + /* execute */ + boolean result = supportToTest.isOutdatedEncryptionPoolPossibleInCluster(); + + /* test */ + assertThat(result).isTrue(); + + } + + static class OutdatedEncryptionPoolOnThisClusterMemberAllowedArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + /* @formatter:off */ + return Stream.of( + /*create pool entry now + * |------120s----------------------| + * | | + * |------------------------ accept outdated >120s--->X + */ + Arguments.of(now(), acceptOutdated(121), now().minusSeconds(120)), + Arguments.of(now(), acceptOutdated(210), now().minusSeconds(120)), + Arguments.of(now(), acceptOutdated(10000), now().minusSeconds(120)) + ); + /* @formatter:on */ + + } + + } + + static class OutdatedEncryptionPoolOnThisClusterMemberNotAllowedArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + /* @formatter:off */ + return Stream.of( + /*create pool entry now + * |------120s----------------------| + * | | + * |----- accept outdated < 120s->X + */ + Arguments.of(now(), acceptOutdated(119), now().minusSeconds(120)), + Arguments.of(now(), acceptOutdated(21), now().minusSeconds(120)), + Arguments.of(now(), acceptOutdated(0), now().minusSeconds(120)) + ); + /* @formatter:on */ + + } + + } + + static class OutdatedEncryptionPoolInClusterPossibleArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + /* @formatter:off */ + return Stream.of( + /*create pool entry now + * |------20s-----------------------| + * | |<--5s+1s(+1)--->| (7s) + * |<-----13s----->| + * | V + * | last refresh point (calculated) + * <------- > 13----> acceptance time is bigger -> still possible + */ + Arguments.of(now(), acceptOutdated(14), now().minusSeconds(20), refreshInterval(5), refresInitialDelay(1)), + Arguments.of(now(), acceptOutdated(15), now().minusSeconds(20), refreshInterval(5), refresInitialDelay(1)), + Arguments.of(now(), acceptOutdated(100), now().minusSeconds(20), refreshInterval(5), refresInitialDelay(1)), + + /*create pool entry now + * |------60s-----------------------| + * | |<--25s+10s(+1)->| (36s) + * |<-----24s----->| + * | V + * | last refresh point (calculated) + * <------- > 24----> acceptance time is bigger -> still possible + */ + Arguments.of(now(), acceptOutdated(25), now().minusSeconds(60), refreshInterval(25), refresInitialDelay(10)), + Arguments.of(now(), acceptOutdated(37), now().minusSeconds(60), refreshInterval(25), refresInitialDelay(10)), + Arguments.of(now(), acceptOutdated(60), now().minusSeconds(60), refreshInterval(25), refresInitialDelay(10)), + + /*create pool entry now + * |------120s----------------------| + * | |<--15s+3s(+1)- >| (19s) + * |<-----101s---->| + * | V + * | last refresh point (calculated) + * <------- > 101----> acceptance time is bigger -> still possible + */ + Arguments.of(now(), acceptOutdated(102), now().minusSeconds(120), refreshInterval(15), refresInitialDelay(3)), + Arguments.of(now(), acceptOutdated(210), now().minusSeconds(120), refreshInterval(15), refresInitialDelay(3)), + Arguments.of(now(), acceptOutdated(10000), now().minusSeconds(120), refreshInterval(15), refresInitialDelay(3)) + ); + /* @formatter:on */ + + } + + } + + static class OutdatedEncryptionPoolInClusterNotPossibleArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + /* @formatter:off */ + return Stream.of( + /*create pool entry now + * |------20s-----------------------| + * | |<--5s+1s(+1)--->| + * |<-----13s----->| + * | V + * | last refresh point (calculated) + * <----- < 13--> acceptance time is lower -> NOT possible + * + */ + Arguments.of(now(), acceptOutdated(12), now().minusSeconds(20), refreshInterval(5), refresInitialDelay(1)), + Arguments.of(now(), acceptOutdated(11), now().minusSeconds(20), refreshInterval(5), refresInitialDelay(1)), + Arguments.of(now(), acceptOutdated(10), now().minusSeconds(20), refreshInterval(5), refresInitialDelay(1)), + + /*create pool entry now + * |------60s-----------------------| + * | |<--25s+10s(+1)->| (36s) + * |<-----24s----->| + * | V + * | last refresh point (calculated) + * <------- < 24----> acceptance time is lower -> NOT possible + */ + Arguments.of(now(), acceptOutdated(23), now().minusSeconds(60), refreshInterval(25), refresInitialDelay(10)), + Arguments.of(now(), acceptOutdated(12), now().minusSeconds(60), refreshInterval(25), refresInitialDelay(10)), + Arguments.of(now(), acceptOutdated(1), now().minusSeconds(60), refreshInterval(25), refresInitialDelay(10)), + + /*create pool entry now + * |------120s----------------------| + * | |<--15s+3s(+1)- >| (19s) + * |<-----101s---->| + * | V + * | last refresh point (calculated) + * <------- < 101----> acceptance time is lower -> NOT possible + */ + Arguments.of(now(), acceptOutdated(100), now().minusSeconds(120), refreshInterval(15), refresInitialDelay(3)), + Arguments.of(now(), acceptOutdated(18), now().minusSeconds(120), refreshInterval(15), refresInitialDelay(3)), + Arguments.of(now(), acceptOutdated(10), now().minusSeconds(120), refreshInterval(15), refresInitialDelay(3)), + Arguments.of(now(), acceptOutdated(0), now().minusSeconds(120), refreshInterval(15), refresInitialDelay(3)), + + /* create pool entry now + * v v + * |------1s------------------------| + * | <--21s+5s(+1)- >| (27s)| + * -----------------------|<-----1s----------------------->| + * | + * | V (26 seconds before creation time) (-26s) + * | last refresh point (calculated) + * <------- < -26----> acceptance time is lower -> NOT possible + */ + Arguments.of(now(), acceptOutdated(10), now().minusSeconds(1), refreshInterval(21), refresInitialDelay(5)), + Arguments.of(now(), acceptOutdated(25), now().minusSeconds(1), refreshInterval(21), refresInitialDelay(5)) + ); + /* @formatter:on */ + + } + + } + + /*** + * Some syntax sugar for test data - define accepted outdated time in SECONDS + * + * @param seconds define seconds + * + * @return milliseconds + */ + private static long acceptOutdated(long seconds) { + return seconds * 1000; + } + + /** + * Some syntax sugar for test data - define refresh interval in SECONDS * @param + * seconds define seconds + * + * @return milliseconds + */ + private static long refreshInterval(long seconds) { + return seconds * 1000; + } + + /** + * Some syntax sugar for test data - define refresh initial delay in SECONDS + * + * @param seconds define seconds + * @return milliseconds + */ + private static long refresInitialDelay(long seconds) { + return seconds * 1000; + } +} diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/JobCreator.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/JobCreator.java index 6e2558708a..e36d04f339 100644 --- a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/JobCreator.java +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/JobCreator.java @@ -8,10 +8,15 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import com.mercedesbenz.sechub.commons.encryption.EncryptionConstants; +import com.mercedesbenz.sechub.commons.encryption.EncryptionResult; +import com.mercedesbenz.sechub.commons.encryption.InitializationVector; import com.mercedesbenz.sechub.commons.model.ModuleGroup; import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModelSupport; import com.mercedesbenz.sechub.commons.model.job.ExecutionResult; import com.mercedesbenz.sechub.commons.model.job.ExecutionState; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionResult; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionService; import com.mercedesbenz.sechub.sharedkernel.UserContextService; import com.mercedesbenz.sechub.sharedkernel.configuration.SecHubConfiguration; import com.mercedesbenz.sechub.test.SechubTestComponent; @@ -25,6 +30,7 @@ public class JobCreator { private String projectId; private UserContextService userContextService; private SecHubConfigurationModelSupport modelSupport; + private ScheduleEncryptionService encryptionService; private JobCreator(String projectId, TestEntityManager entityManager) { this.modelSupport = new SecHubConfigurationModelSupport(); @@ -33,10 +39,20 @@ private JobCreator(String projectId, TestEntityManager entityManager) { this.jobFactory = new SecHubJobFactory(); userContextService = mock(UserContextService.class); + encryptionService = mock(ScheduleEncryptionService.class); this.jobFactory.userContextService = userContextService; this.jobFactory.modelSupport = modelSupport; + this.jobFactory.encryptionService = encryptionService; when(userContextService.getUserId()).thenReturn("loggedInUser"); + doAnswer(invocation -> { + String textToEncrypt = invocation.getArgument(0); + + EncryptionResult encryptionResult = new EncryptionResult(textToEncrypt.getBytes(EncryptionConstants.UTF8_CHARSET_ENCODING), + new InitializationVector(new byte[] {})); + ScheduleEncryptionResult result = new ScheduleEncryptionResult(Long.valueOf(0), encryptionResult); + return result; + }).when(encryptionService).encryptWithLatestCipher(any()); newJob(); } @@ -75,6 +91,11 @@ public JobCreator project(String projectId) { return this; } + public JobCreator encryptionPoolId(Long encryptionCipherPoolId) { + job.encryptionCipherPoolId = encryptionCipherPoolId; + return this; + } + /** * Creates the job and returns builder agani * diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/JobFactoryTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/JobFactoryTest.java index c97c5e2256..b180e7517b 100644 --- a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/JobFactoryTest.java +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/JobFactoryTest.java @@ -1,46 +1,97 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.domain.schedule.job; -import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import com.mercedesbenz.sechub.commons.encryption.InitializationVector; import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModelSupport; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionResult; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionService; import com.mercedesbenz.sechub.sharedkernel.UserContextService; import com.mercedesbenz.sechub.sharedkernel.configuration.SecHubConfiguration; -import com.mercedesbenz.sechub.test.junit4.ExpectedExceptionFactory; public class JobFactoryTest { + private static final String UNENCRYPTED_CONFIGURATION = "i-am-the-origin-configuration"; + private static final Long LATEST_CIPHER_POOL_ID = Long.valueOf(1234); + private static final byte[] ENCRYPTED_CONFIGURATION_DATA = "encrypted-data".getBytes(); + private static byte[] ENCRYPTION_INITIAL_VECTOR = "initial-vector".getBytes(); + private SecHubJobFactory factoryToTest; private SecHubConfiguration configuration; - @Rule - public ExpectedException expected = ExpectedExceptionFactory.none(); + private ScheduleEncryptionService encryptionService; + + @BeforeEach + void beforeEach() { - @Before - public void before() { factoryToTest = new SecHubJobFactory(); + + encryptionService = mock(ScheduleEncryptionService.class); + configuration = mock(SecHubConfiguration.class); + when(configuration.toJSON()).thenReturn(UNENCRYPTED_CONFIGURATION); + factoryToTest.userContextService = mock(UserContextService.class); factoryToTest.modelSupport = mock(SecHubConfigurationModelSupport.class); + factoryToTest.encryptionService = encryptionService; + + ScheduleEncryptionResult encryptionResult = mock(ScheduleEncryptionResult.class); + when(encryptionResult.getCipherPoolId()).thenReturn(LATEST_CIPHER_POOL_ID); + when(encryptionResult.getEncryptedData()).thenReturn(ENCRYPTED_CONFIGURATION_DATA); + when(encryptionResult.getInitialVector()).thenReturn(new InitializationVector(ENCRYPTION_INITIAL_VECTOR)); + + when(encryptionService.encryptWithLatestCipher(UNENCRYPTED_CONFIGURATION)).thenReturn(encryptionResult); + + when(factoryToTest.userContextService.getUserId()).thenReturn("user1"); } @Test - public void new_jobs_have_creation_time_stamp() throws Exception { - /* prepare */ - when(factoryToTest.userContextService.getUserId()).thenReturn("hugo"); + void new_jobs_have_creation_time_stamp() throws Exception { + + /* execute */ + ScheduleSecHubJob job = factoryToTest.createJob(configuration); + + /* test */ + assertThat(job).isNotNull(); + assertThat(job.getCreated()).isNotNull(); + + } + + @Test + void new_jobs_have_configuration_encrypted_by_encryption_service() throws Exception { + + /* execute */ + ScheduleSecHubJob job = factoryToTest.createJob(configuration); + + /* test */ + assertThat(job.getEncryptedConfiguration()).isEqualTo(ENCRYPTED_CONFIGURATION_DATA); + + } + + @Test + void new_jobs_have_encryption_initial_vector_from_encryption_service() throws Exception { /* execute */ ScheduleSecHubJob job = factoryToTest.createJob(configuration); /* test */ - assertNotNull(job); - assertNotNull(job.getCreated()); + assertThat(job.getEncryptionInitialVectorData()).isEqualTo(ENCRYPTION_INITIAL_VECTOR); + + } + + @Test + void new_jobs_have_lastet_encryption_pool_id_from_encryption_service() throws Exception { + + /* execute */ + ScheduleSecHubJob job = factoryToTest.createJob(configuration); + + /* test */ + assertThat(job.getEncryptionCipherPoolId()).isEqualTo(LATEST_CIPHER_POOL_ID); } @@ -49,11 +100,8 @@ public void factory_throws_illegal_state_exception_when_no_user() throws Excepti /* prepare */ when(factoryToTest.userContextService.getUserId()).thenReturn(null); - /* test */ - expected.expect(IllegalStateException.class); - /* execute */ - factoryToTest.createJob(configuration); + assertThatThrownBy(()-> factoryToTest.createJob(configuration)).isInstanceOf(IllegalStateException.class); } diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobFactoryTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobFactorySpringBootTest.java similarity index 74% rename from sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobFactoryTest.java rename to sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobFactorySpringBootTest.java index 8280db56fb..1e90cb0f67 100644 --- a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobFactoryTest.java +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobFactorySpringBootTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +import java.nio.charset.Charset; import java.util.Collections; import org.junit.jupiter.api.BeforeEach; @@ -12,27 +13,51 @@ import org.junit.jupiter.params.provider.EmptySource; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import com.mercedesbenz.sechub.commons.encryption.InitializationVector; import com.mercedesbenz.sechub.commons.model.ModuleGroup; import com.mercedesbenz.sechub.commons.model.ScanType; import com.mercedesbenz.sechub.commons.model.SecHubConfigurationModelSupport; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionResult; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionService; import com.mercedesbenz.sechub.sharedkernel.UserContextService; import com.mercedesbenz.sechub.sharedkernel.configuration.SecHubConfiguration; -class SecHubJobFactoryTest { +@SpringBootTest(classes = SecHubJobFactory.class) +class SecHubJobFactorySpringBootTest { + @Autowired private SecHubJobFactory factoryToTest; + + @MockBean private UserContextService userContextService; + + @MockBean private SecHubConfigurationModelSupport modelSupport; + @MockBean(name = "encryption service") + private ScheduleEncryptionService encryptionService; + + private InitializationVector initialVector; + + private byte[] encryptedData; + + private Long cipherPoolId; + @BeforeEach void beforeEach() { - userContextService = mock(UserContextService.class); - modelSupport = mock(SecHubConfigurationModelSupport.class); - - factoryToTest = new SecHubJobFactory(); - factoryToTest.userContextService = userContextService; - factoryToTest.modelSupport = modelSupport; + encryptedData = "i-am-encrypted".getBytes(Charset.forName("UTF-8")); + cipherPoolId = Long.valueOf(0); + initialVector = new InitializationVector("init-vector".getBytes()); + + ScheduleEncryptionResult result = mock(ScheduleEncryptionResult.class); + when(encryptionService.encryptWithLatestCipher(any())).thenReturn(result); + when(result.getInitialVector()).thenReturn(initialVector); + when(result.getEncryptedData()).thenReturn(encryptedData); + when(result.getCipherPoolId()).thenReturn(cipherPoolId); } @ParameterizedTest @@ -78,7 +103,7 @@ void createJob_sets_creation_time() { } @Test - void createJob_sets_configuration_as_json() { + void createJob_sets_configuration_encrypted() { /* prepare */ when(userContextService.getUserId()).thenReturn("user1"); SecHubConfiguration configuration = mock(SecHubConfiguration.class); @@ -89,7 +114,8 @@ void createJob_sets_configuration_as_json() { /* test */ assertNotNull(result); - assertEquals("pseudo-json", result.getJsonConfiguration()); + assertEquals(encryptedData, result.getEncryptedConfiguration()); + verify(encryptionService).encryptWithLatestCipher("pseudo-json"); } @Test diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobInfoForUserServiceTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobInfoForUserServiceTest.java index 9435fcba90..2944f9fa7b 100644 --- a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobInfoForUserServiceTest.java +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobInfoForUserServiceTest.java @@ -37,6 +37,7 @@ class SecHubJobInfoForUserServiceTest { private SecHubJobInfoForUserService serviceToTest; private SecHubJobRepository jobRepository; private ScheduleAssertService assertService; + private SecHubConfigurationModelAccess configurationModelAccess; @BeforeEach void beforeEach() { @@ -44,12 +45,15 @@ void beforeEach() { jobRepository = mock(SecHubJobRepository.class); assertService = mock(ScheduleAssertService.class); + configurationModelAccess = mock(SecHubConfigurationModelAccess.class); + serviceToTest = new SecHubJobInfoForUserService(); serviceToTest.jobRepository = jobRepository; serviceToTest.assertService = assertService; serviceToTest.metaDataTransformer = new SecHubConfigurationMetaDataMapTransformer(); serviceToTest.modelValidator = new SecHubConfigurationModelValidator(); + serviceToTest.configurationModelAccess = configurationModelAccess; } @@ -290,7 +294,9 @@ private ScheduleSecHubJob createJob(TrafficLight trafficLight, ExecutionResult r SecHubConfigurationMetaData metaData = new SecHubConfigurationMetaData(); metaData.getLabels().put("testlabel1", "testvalue1"); config.setMetaData(metaData); - sechubJob.jsonConfiguration = config.toJSON(); + + // simulate encryption done by job creation factory + when(configurationModelAccess.resolveUnencryptedConfiguration(sechubJob)).thenReturn(config); } return sechubJob; diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobRepositoryDBTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobRepositoryDBTest.java index 99e6633331..fe87a3f8d4 100644 --- a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobRepositoryDBTest.java +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/job/SecHubJobRepositoryDBTest.java @@ -4,6 +4,7 @@ import static com.mercedesbenz.sechub.commons.model.job.ExecutionState.*; import static com.mercedesbenz.sechub.domain.schedule.job.JobCreator.*; import static com.mercedesbenz.sechub.test.FlakyOlderThanTestWorkaround.*; +import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*; import java.time.LocalDateTime; @@ -11,13 +12,21 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.EmptySource; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; @@ -38,6 +47,8 @@ @ContextConfiguration(classes = { SecHubJobRepository.class, SecHubJobRepositoryDBTest.SimpleTestConfiguration.class }) public class SecHubJobRepositoryDBTest { + private static final Set FIRST_ENCRYPTION_POOL_ENTRY_ONLY = Set.of(0L); + @Autowired private TestEntityManager entityManager; @@ -51,6 +62,208 @@ void before() { jobCreator = jobCreator("p0", entityManager); } + @Test + void collectAllUsedEncryptionPoolIdsInsideJobs_no_jobs_found() { + /* execute */ + List result = jobRepository.collectAllUsedEncryptionPoolIdsInsideJobs(); + + /* test */ + assertThat(result).isNotNull().isEmpty(); + } + + @ParameterizedTest + @EnumSource(ExecutionState.class) + void collectAllUsedEncryptionPoolIdsInsideJobs_only_two_jobs_found_same_poolid(ExecutionState stateOfJob) { + /* prepare */ + int poolId = 1234; + createJobUsingEncryptionPoolId(poolId, stateOfJob); + createJobUsingEncryptionPoolId(poolId, stateOfJob); + + /* execute */ + List result = jobRepository.collectAllUsedEncryptionPoolIdsInsideJobs(); + + /* test */ + assertThat(result).isNotNull().hasSize(1); + Long value = result.iterator().next(); + + assertThat(value).isEqualTo(poolId); + } + + @Test + void collectAllUsedEncryptionPoolIdsInsideJobs_multiple_jobs_mixed_poolids() { + /* prepare */ + createJobUsingEncryptionPoolId(0, ExecutionState.INITIALIZING); + createJobUsingEncryptionPoolId(1, ExecutionState.READY_TO_START); + createJobUsingEncryptionPoolId(1, ExecutionState.CANCEL_REQUESTED); + createJobUsingEncryptionPoolId(3, ExecutionState.ENDED); + createJobUsingEncryptionPoolId(176, ExecutionState.STARTED); + createJobUsingEncryptionPoolId(176, ExecutionState.CANCELED); + + /* execute */ + List result = jobRepository.collectAllUsedEncryptionPoolIdsInsideJobs(); + + /* test */ + assertThat(result).isNotNull().hasSize(4).contains(0L, 1L, 3L, 176L); + } + + @ParameterizedTest + @ValueSource(ints = { 0, 1, 2, 10 }) + void countCanceledOrEndedJobsWithEncryptionPoolIdLowerThan_works_as_expected(int expectedResultCount) { + /* prepare */ + jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.INITIALIZING).create(); + jobCreator.project("p1").module(ModuleGroup.STATIC).being(ExecutionState.READY_TO_START).create(); + jobCreator.project("p1").module(ModuleGroup.STATIC).being(ExecutionState.STARTED).create(); + jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.CANCEL_REQUESTED).create(); + + // generate data + if (expectedResultCount > 0) { + jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.ENDED).create(); + } + if (expectedResultCount > 1) { + for (int i = 1; i < expectedResultCount; i++) { + jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.CANCELED).create(); + } + } + + /* execute */ + long result = jobRepository.countCanceledOrEndedJobsWithEncryptionPoolIdLowerThan(1L); + + /* test */ + assertEquals(result, expectedResultCount); + } + + @ParameterizedTest + @ValueSource(ints = { 1, 2, 4 }) + void nextCanceledOrEndedJobsWithEncryptionPoolIdLowerThan_one_ended_only_single_entry_always_returned(int amount) { + /* prepare */ + ScheduleSecHubJob newJob1 = jobCreator.project("p1").module(ModuleGroup.STATIC).being(ExecutionState.STARTED).create(); + ScheduleSecHubJob newJob2 = jobCreator.project("p1").module(ModuleGroup.STATIC).being(ExecutionState.READY_TO_START).create(); + ScheduleSecHubJob newJob3 = jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.INITIALIZING).create(); + ScheduleSecHubJob newJob4 = jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.ENDED).create(); + ScheduleSecHubJob newJob5 = jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.CANCEL_REQUESTED).create(); + + // check preconditions + assertEquals(0, newJob1.getEncryptionCipherPoolId()); + assertEquals(0, newJob2.getEncryptionCipherPoolId()); + assertEquals(0, newJob3.getEncryptionCipherPoolId()); + assertEquals(0, newJob4.getEncryptionCipherPoolId()); + assertEquals(0, newJob5.getEncryptionCipherPoolId()); + + /* execute */ + List list = jobRepository.nextCanceledOrEndedJobsWithEncryptionPoolIdLowerThan(1L, amount); + + /* test */ + assertFalse(list.isEmpty()); + assertTrue(list.contains(newJob4)); // only this, because others are in wrong state + assertEquals(1, list.size()); + } + + @ParameterizedTest + @ValueSource(ints = { 1, 2, 4 }) + void nextCanceledOrEndedJobsWithEncryptionPoolIdLowerThan_one_is_lower_ended_only_single_entry_always_returned(int amount) { + /* prepare */ + ScheduleSecHubJob newJob1 = jobCreator.project("p1").module(ModuleGroup.STATIC).encryptionPoolId(0L).being(ExecutionState.ENDED).create(); + ScheduleSecHubJob newJob2 = jobCreator.project("p1").module(ModuleGroup.STATIC).encryptionPoolId(1L).being(ExecutionState.ENDED).create(); + ScheduleSecHubJob newJob3 = jobCreator.project("p2").module(ModuleGroup.STATIC).encryptionPoolId(1L).being(ExecutionState.ENDED).create(); + ScheduleSecHubJob newJob4 = jobCreator.project("p2").module(ModuleGroup.STATIC).encryptionPoolId(2L).being(ExecutionState.ENDED).create(); + ScheduleSecHubJob newJob5 = jobCreator.project("p2").module(ModuleGroup.STATIC).encryptionPoolId(3L).being(ExecutionState.ENDED).create(); + + // check preconditions + assertEquals(0, newJob1.getEncryptionCipherPoolId()); + assertEquals(1, newJob2.getEncryptionCipherPoolId()); + assertEquals(1, newJob3.getEncryptionCipherPoolId()); + assertEquals(2, newJob4.getEncryptionCipherPoolId()); + assertEquals(3, newJob5.getEncryptionCipherPoolId()); + + /* execute */ + List list = jobRepository.nextCanceledOrEndedJobsWithEncryptionPoolIdLowerThan(1L, amount); + + /* test */ + assertFalse(list.isEmpty()); + assertTrue(list.contains(newJob1)); // only this, because others are already with higher pool id + assertEquals(1, list.size()); + } + + @ParameterizedTest + @ValueSource(ints = { 2, 10, 100 }) + void nextCanceledOrEndedJobsWithEncryptionPoolIdLowerThan_one_ended_one_canceled_entries_always_returned(int amount) { + /* prepare */ + ScheduleSecHubJob newJob1 = jobCreator.project("p1").module(ModuleGroup.STATIC).being(ExecutionState.STARTED).create(); + ScheduleSecHubJob newJob2 = jobCreator.project("p1").module(ModuleGroup.STATIC).being(ExecutionState.READY_TO_START).create(); + ScheduleSecHubJob newJob3 = jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.INITIALIZING).create(); + ScheduleSecHubJob newJob4 = jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.ENDED).create(); + ScheduleSecHubJob newJob5 = jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.CANCEL_REQUESTED).create(); + ScheduleSecHubJob newJob6 = jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.CANCELED).create(); + + // check preconditions + assertEquals(0, newJob1.getEncryptionCipherPoolId()); + assertEquals(0, newJob2.getEncryptionCipherPoolId()); + assertEquals(0, newJob3.getEncryptionCipherPoolId()); + assertEquals(0, newJob4.getEncryptionCipherPoolId()); + assertEquals(0, newJob5.getEncryptionCipherPoolId()); + assertEquals(0, newJob6.getEncryptionCipherPoolId()); + + /* execute */ + List list = jobRepository.nextCanceledOrEndedJobsWithEncryptionPoolIdLowerThan(1L, amount); + + /* test */ + assertFalse(list.isEmpty()); + assertTrue(list.contains(newJob4)); + assertTrue(list.contains(newJob6)); + assertEquals(2, list.size()); + } + + @Test + void nextCanceledOrEndedJobsWithEncryptionPoolIdLowerThan_randomization_works() { + /* prepare */ + ScheduleSecHubJob newJob1 = jobCreator.project("p1").module(ModuleGroup.STATIC).being(ExecutionState.ENDED).create(); + ScheduleSecHubJob newJob2 = jobCreator.project("p1").module(ModuleGroup.STATIC).being(ExecutionState.CANCELED).create(); + ScheduleSecHubJob newJob3 = jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.ENDED).create(); + ScheduleSecHubJob newJob4 = jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.CANCELED).create(); + ScheduleSecHubJob newJob5 = jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.ENDED).create(); + ScheduleSecHubJob newJob6 = jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.CANCELED).create(); + + // check preconditions + assertEquals(0, newJob1.getEncryptionCipherPoolId()); + assertEquals(0, newJob2.getEncryptionCipherPoolId()); + assertEquals(0, newJob3.getEncryptionCipherPoolId()); + assertEquals(0, newJob4.getEncryptionCipherPoolId()); + assertEquals(0, newJob5.getEncryptionCipherPoolId()); + assertEquals(0, newJob6.getEncryptionCipherPoolId()); + + Map map = new LinkedHashMap<>(); + + /* execute */ + for (int i = 0; i < 30; i++) { + List list = jobRepository.nextCanceledOrEndedJobsWithEncryptionPoolIdLowerThan(1L, 1); + for (ScheduleSecHubJob job : list) { + AtomicInteger atomic = map.computeIfAbsent(job.getUUID(), (uuid) -> new AtomicInteger(0)); + atomic.incrementAndGet(); + } + } + + /* test */ + assertEquals(6, map.size()); // when calling n times, we expect every entry is contained + System.out.println("map:" + map); + + } + + @Test + void nextCanceledOrEndedJobsWithEncryptionPoolIdLowerThan_2_entries_always_returned() { + /* prepare */ + jobCreator.project("p1").module(ModuleGroup.STATIC).being(ExecutionState.STARTED).create(); + jobCreator.project("p1").module(ModuleGroup.STATIC).being(ExecutionState.READY_TO_START).create(); + jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.CANCEL_REQUESTED).create(); + ScheduleSecHubJob newJob4 = jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.ENDED).create(); + + /* execute */ + List list = jobRepository.nextCanceledOrEndedJobsWithEncryptionPoolIdLowerThan(1L, 10); + + /* test */ + assertFalse(list.isEmpty()); + assertTrue(list.contains(newJob4)); + } + @Test void findAll_with_specifications_for_project_id_and_data_2_data_but_only_one_matches() { @@ -352,17 +565,13 @@ void delete_job_with_data_deletes_data() { assertNull(result); } - private ScheduleSecHubJobData findDataOrNullByJobUUID(String key, UUID jobUUID) { - return entityManager.find(ScheduleSecHubJobData.class, new ScheduleSecHubJobDataId(jobUUID, key)); - } - @Test void custom_query_nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted() { /* prepare */ ScheduleSecHubJob newJob = jobCreator.being(ExecutionState.READY_TO_START).create(); /* execute */ - Optional uuid = jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(); + Optional uuid = jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(FIRST_ENCRYPTION_POOL_ENTRY_ONLY); /* test */ assertTrue(uuid.isPresent()); @@ -375,7 +584,7 @@ void custom_query_nextJobIdToExecuteForProjectNotYetExecuted_one_available() { ScheduleSecHubJob newJob = jobCreator.being(ExecutionState.READY_TO_START).create(); /* execute */ - Optional uuid = jobRepository.nextJobIdToExecuteForProjectNotYetExecuted(); + Optional uuid = jobRepository.nextJobIdToExecuteForProjectNotYetExecuted(Set.of(0L)); /* test */ assertTrue(uuid.isPresent()); @@ -394,7 +603,7 @@ void custom_query_nextJobIdToExecuteForProjectNotYetExecuted_2_projects_1_projec assertTrue(newJob3.created.isAfter(newJob2.created)); /* execute */ - Optional uuid = jobRepository.nextJobIdToExecuteForProjectNotYetExecuted(); + Optional uuid = jobRepository.nextJobIdToExecuteForProjectNotYetExecuted(Set.of(0L)); /* test */ assertTrue(uuid.isPresent()); @@ -413,7 +622,7 @@ void custom_query_nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted_2_pro assertTrue(newJob3.created.isAfter(newJob2.created)); /* execute */ - Optional uuid = jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(); + Optional uuid = jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(FIRST_ENCRYPTION_POOL_ENTRY_ONLY); /* test */ assertTrue(uuid.isPresent()); @@ -432,7 +641,7 @@ void custom_query_nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted_2_pro assertTrue(newJob3.created.isAfter(newJob2.created)); /* execute */ - Optional uuid = jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(); + Optional uuid = jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(FIRST_ENCRYPTION_POOL_ENTRY_ONLY); /* test */ assertTrue(uuid.isPresent()); @@ -453,7 +662,7 @@ void custom_query_nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted_3_pro assertTrue(newJob4.created.isAfter(newJob3.created)); /* execute */ - Optional uuid = jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(); + Optional uuid = jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(FIRST_ENCRYPTION_POOL_ENTRY_ONLY); /* test */ assertTrue(uuid.isPresent()); @@ -474,7 +683,7 @@ void custom_query_nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted_3_pro assertTrue(newJob4.created.isAfter(newJob3.created)); /* execute */ - Optional uuid = jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(); + Optional uuid = jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(FIRST_ENCRYPTION_POOL_ENTRY_ONLY); /* test */ assertTrue(uuid.isPresent()); @@ -495,7 +704,7 @@ void custom_query_nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted_4_pro assertTrue(newJob4.created.isAfter(newJob3.created)); /* execute */ - Optional uuid = jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(); + Optional uuid = jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(FIRST_ENCRYPTION_POOL_ENTRY_ONLY); /* test */ assertTrue(uuid.isPresent()); @@ -520,7 +729,7 @@ void custom_query_nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted_3_pro assertTrue(newJob6.created.isAfter(newJob5.created)); /* execute */ - Optional uuid = jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(); + Optional uuid = jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(FIRST_ENCRYPTION_POOL_ENTRY_ONLY); /* test */ assertTrue(uuid.isPresent()); @@ -533,7 +742,7 @@ void custom_query_nextJobIdToExecuteFirstInFirstOut() { ScheduleSecHubJob newJob = jobCreator.being(ExecutionState.READY_TO_START).create(); /* execute */ - Optional uuid = jobRepository.nextJobIdToExecuteFirstInFirstOut(); + Optional uuid = jobRepository.nextJobIdToExecuteFirstInFirstOut(FIRST_ENCRYPTION_POOL_ENTRY_ONLY); /* test */ assertTrue(uuid.isPresent()); @@ -717,7 +926,7 @@ void findByUUID__the_job_is_returned_when_existing() { @Test void findNextJobToExecute__and_no_jobs_available_at_all_null_is_returned_when_existing() { - assertFalse(jobRepository.nextJobIdToExecuteFirstInFirstOut().isPresent()); + assertFalse(jobRepository.nextJobIdToExecuteFirstInFirstOut(FIRST_ENCRYPTION_POOL_ENTRY_ONLY).isPresent()); } @Test @@ -726,7 +935,7 @@ void findNextJobToExecute__and_no_executable_job_available_at_all_null_is_return jobCreator.newJob().being(STARTED).create(); /* execute + test */ - assertFalse(jobRepository.nextJobIdToExecuteFirstInFirstOut().isPresent()); + assertFalse(jobRepository.nextJobIdToExecuteFirstInFirstOut(FIRST_ENCRYPTION_POOL_ENTRY_ONLY).isPresent()); } @@ -749,16 +958,210 @@ void findNextJobToExecute__the_first_job_in_state_READY_TO_START_is_returned_whe newJob().being(READY_TO_START).create(); /* execute */ - Optional optional = jobRepository.nextJobIdToExecuteFirstInFirstOut(); + Optional optional = jobRepository.nextJobIdToExecuteFirstInFirstOut(FIRST_ENCRYPTION_POOL_ENTRY_ONLY); assertTrue(optional.isPresent()); UUID jobUUID = optional.get(); /* test @formatter:on*/ - assertNotNull(jobUUID); + assertThat(jobUUID).isNotNull(); assertEquals(expectedNextJob.getUUID(), jobUUID); } + @ParameterizedTest + @ArgumentsSource(NextJobIdToExecuteWithEncryptionPoolTestDataArgumentProvider.class) + void nextJobIdToExecuteForProjectNotYetExecuted_2_projects_1_project_running(Set currentEncryptionPoolIds, TestResultVariant expectedVariant) { + /* prepare */ + ScheduleSecHubJob newJob1 = jobCreator.project("p1").module(ModuleGroup.STATIC).being(ExecutionState.STARTED).create(); + ScheduleSecHubJob newJob2 = jobCreator.project("p1").module(ModuleGroup.STATIC).being(ExecutionState.READY_TO_START).create(); + ScheduleSecHubJob newJob3p0 = jobCreator.project("p2").encryptionPoolId(0L).module(ModuleGroup.STATIC).being(ExecutionState.READY_TO_START).create(); + ScheduleSecHubJob newJob3p1 = jobCreator.project("p2").encryptionPoolId(1L).module(ModuleGroup.STATIC).being(ExecutionState.READY_TO_START).create(); + ScheduleSecHubJob newJob3p2 = jobCreator.project("p2").encryptionPoolId(2L).module(ModuleGroup.STATIC).being(ExecutionState.READY_TO_START).create(); + ScheduleSecHubJob newJob3p3 = jobCreator.project("p2").encryptionPoolId(3L).module(ModuleGroup.STATIC).being(ExecutionState.READY_TO_START).create(); + + /* check preconditions */ + assertTrue(newJob2.created.isAfter(newJob1.created)); + assertTrue(newJob3p0.created.isAfter(newJob2.created)); + + /* execute */ + Optional optional = jobRepository.nextJobIdToExecuteForProjectNotYetExecuted(currentEncryptionPoolIds); + if (expectedVariant == null || TestResultVariant.NONE.equals(expectedVariant)) { + assertFalse(optional.isPresent()); + return; + } + assertTrue(optional.isPresent()); + + UUID jobUUID = optional.get(); + + /* test @formatter:on*/ + assertThat(jobUUID).isNotNull(); + switch (expectedVariant) { + case POOL_ID_0: + assertThat(jobUUID).isEqualTo(newJob3p0.getUUID()); + break; + case POOL_ID_1: + assertThat(jobUUID).isEqualTo(newJob3p1.getUUID()); + break; + case POOL_ID_2: + assertThat(jobUUID).isEqualTo(newJob3p2.getUUID()); + break; + case POOL_ID_3: + assertThat(jobUUID).isEqualTo(newJob3p3.getUUID()); + break; + default: + throw new IllegalStateException("not implemented for variant: " + expectedVariant); + + } + } + + @ParameterizedTest + @ArgumentsSource(NextJobIdToExecuteWithEncryptionPoolTestDataArgumentProvider.class) + void nextJobIdToExecuteFirstInFirstOut__the_job_with_has_supported_encryption_in_state_READY_TO_START_multi_poolids(Set currentEncryptionPoolIds, + TestResultVariant expectedVariant) { + /* prepare @formatter:off*/ + + jobCreator.newJob().being(STARTED).createAnd(). + newJob().being(CANCEL_REQUESTED).createAnd(). + newJob().being(CANCELED).createAnd(). + newJob().being(ENDED).create(); + ScheduleSecHubJob expectedNextJobWhenPoolId0Supported = + jobCreator.newJob().encryptionPoolId(0L).being(READY_TO_START).create(); + ScheduleSecHubJob expectedNextJobWhenPoolId1Supported = + jobCreator.newJob().encryptionPoolId(1L).being(READY_TO_START).create(); + ScheduleSecHubJob expectedNextJobWhenPoolId2Supported = + jobCreator.newJob().being(READY_TO_START).encryptionPoolId(2L).create(); + ScheduleSecHubJob expectedNextJobWhenPoolId3Supported = + jobCreator.newJob().being(READY_TO_START).encryptionPoolId(3L).create(); + + TestUtil.waitMilliseconds(1); // just enough time to make the next job "older" than former one, so we got no flaky tests when checking jobUUID later + + + jobCreator.newJob().being(STARTED).createAnd(). + newJob().being(READY_TO_START).create(); + + /* execute */ + Optional optional = jobRepository.nextJobIdToExecuteFirstInFirstOut(currentEncryptionPoolIds); + if (expectedVariant==null || TestResultVariant.NONE.equals(expectedVariant)) { + assertFalse(optional.isPresent()); + return; + } + assertTrue(optional.isPresent()); + + UUID jobUUID = optional.get(); + + /* test @formatter:on*/ + assertThat(jobUUID).isNotNull(); + switch (expectedVariant) { + case POOL_ID_0: + assertEquals(expectedNextJobWhenPoolId0Supported.getUUID(), jobUUID); + break; + case POOL_ID_1: + assertEquals(expectedNextJobWhenPoolId1Supported.getUUID(), jobUUID); + break; + case POOL_ID_2: + assertEquals(expectedNextJobWhenPoolId2Supported.getUUID(), jobUUID); + break; + case POOL_ID_3: + assertEquals(expectedNextJobWhenPoolId3Supported.getUUID(), jobUUID); + break; + default: + throw new IllegalStateException("not implemented for variant: " + expectedVariant); + + } + } + + @ParameterizedTest + @ArgumentsSource(NextJobIdToExecuteWithEncryptionPoolTestDataArgumentProvider.class) + void custom_query_nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted_3_projects_in_state_READY_TO_START_multi_poolids( + Set currentEncryptionPoolIds, TestResultVariant expectedVariant) { + /* prepare */ + ScheduleSecHubJob newJob1 = jobCreator.project("p1").module(ModuleGroup.STATIC).being(ExecutionState.CANCEL_REQUESTED).create(); + ScheduleSecHubJob newJob2 = jobCreator.project("p1").module(ModuleGroup.STATIC).being(ExecutionState.INITIALIZING).create(); + ScheduleSecHubJob newJob3 = jobCreator.project("p2").module(ModuleGroup.STATIC).being(ExecutionState.CANCELED).create(); + ScheduleSecHubJob newJob4 = jobCreator.project("p3").module(ModuleGroup.STATIC).being(ExecutionState.ENDED).create(); + ScheduleSecHubJob newJob5 = jobCreator.project("p3").module(ModuleGroup.STATIC).being(ExecutionState.CANCEL_REQUESTED).create(); + + ScheduleSecHubJob newJob6v0 = jobCreator.project("p3").encryptionPoolId(0L).module(ModuleGroup.STATIC).being(ExecutionState.READY_TO_START).create(); + ScheduleSecHubJob newJob6v1 = jobCreator.project("p3").encryptionPoolId(1L).module(ModuleGroup.STATIC).being(ExecutionState.READY_TO_START).create(); + ScheduleSecHubJob newJob6v2 = jobCreator.project("p3").encryptionPoolId(2L).module(ModuleGroup.STATIC).being(ExecutionState.READY_TO_START).create(); + ScheduleSecHubJob newJob6v3 = jobCreator.project("p3").encryptionPoolId(3L).module(ModuleGroup.STATIC).being(ExecutionState.READY_TO_START).create(); + + /* check preconditions */ + assertTrue(newJob2.created.isAfter(newJob1.created)); + assertTrue(newJob3.created.isAfter(newJob2.created)); + assertTrue(newJob4.created.isAfter(newJob3.created)); + assertTrue(newJob5.created.isAfter(newJob4.created)); + assertTrue(newJob6v0.created.isAfter(newJob5.created)); + + /* execute */ + Optional optional = jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(currentEncryptionPoolIds); + + /* test */ + if (expectedVariant == null || TestResultVariant.NONE.equals(expectedVariant)) { + assertThat(optional).isNotPresent(); + return; + } + assertTrue(optional.isPresent()); + + UUID jobUUID = optional.get(); + + /* test @formatter:on*/ + assertThat(jobUUID).isNotNull(); + switch (expectedVariant) { + case POOL_ID_0: + assertEquals(newJob6v0.getUUID(), jobUUID); + break; + case POOL_ID_1: + assertEquals(newJob6v1.getUUID(), jobUUID); + break; + case POOL_ID_2: + assertEquals(newJob6v2.getUUID(), jobUUID); + break; + case POOL_ID_3: + assertEquals(newJob6v3.getUUID(), jobUUID); + break; + default: + throw new IllegalStateException("not implemented for variant: " + expectedVariant); + + } + } + + private enum TestResultVariant { + NONE, POOL_ID_0, POOL_ID_1, POOL_ID_2, POOL_ID_3; + } + + static class NextJobIdToExecuteWithEncryptionPoolTestDataArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + return Stream.of( + /* @formatter:off */ + Arguments.of(Set.of(), TestResultVariant.NONE), + Arguments.of(Set.of(0), TestResultVariant.POOL_ID_0), + Arguments.of(Set.of(1), TestResultVariant.POOL_ID_1), + Arguments.of(Set.of(2), TestResultVariant.POOL_ID_2), + Arguments.of(Set.of(3), TestResultVariant.POOL_ID_3), + Arguments.of(Set.of(1,2), TestResultVariant.POOL_ID_1), + Arguments.of(Set.of(0,1,2,3), TestResultVariant.POOL_ID_0), + Arguments.of(Set.of(1,2,3), TestResultVariant.POOL_ID_1), + Arguments.of(Set.of(3,2,0), TestResultVariant.POOL_ID_0), + Arguments.of(Set.of(0,3),TestResultVariant.POOL_ID_0) + ); + /* @formatter:on */ + + } + + } + + private ScheduleSecHubJobData findDataOrNullByJobUUID(String key, UUID jobUUID) { + return entityManager.find(ScheduleSecHubJobData.class, new ScheduleSecHubJobDataId(jobUUID, key)); + } + + private void createJobUsingEncryptionPoolId(long poolId, ExecutionState state) { + jobCreator.project("p2").module(ModuleGroup.STATIC).being(state).encryptionPoolId(poolId).create(); + + } + private void assertDeleted(int expected, int deleted, DeleteJobTestData testData, LocalDateTime olderThan) { if (deleted == expected) { return; @@ -778,7 +1181,7 @@ private void assertDeleted(int expected, int deleted, DeleteJobTestData testData sb.append(describe(testData.job3_1_day_before_created, testData)); sb.append(describe(testData.job4_now_created, testData)); - fail(sb.toString()); + throw new AssertionError(sb.toString()); } private String describe(ScheduleSecHubJob info, DeleteJobTestData data) { diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/strategy/FirstComeFirstServeSchedulerStrategyTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/strategy/FirstComeFirstServeSchedulerStrategyTest.java index f53f53ca1b..6fa219c6ef 100644 --- a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/strategy/FirstComeFirstServeSchedulerStrategyTest.java +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/strategy/FirstComeFirstServeSchedulerStrategyTest.java @@ -4,12 +4,15 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +import java.util.Collections; import java.util.Optional; +import java.util.Set; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionService; import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobRepository; class FirstComeFirstServeSchedulerStrategyTest { @@ -17,27 +20,33 @@ class FirstComeFirstServeSchedulerStrategyTest { private FirstComeFirstServeSchedulerStrategy strategyToTest; private SecHubJobRepository jobRepository; private UUID jobUUID; + private ScheduleEncryptionService encryptionService; @BeforeEach void beforeEach() { jobUUID = UUID.randomUUID(); jobRepository = mock(SecHubJobRepository.class); + encryptionService = mock(ScheduleEncryptionService.class); + strategyToTest = new FirstComeFirstServeSchedulerStrategy(); strategyToTest.jobRepository = jobRepository; + strategyToTest.encryptionService = encryptionService; } @Test void nextJobId_calls_expected_query_method() { /* prepare */ - when(jobRepository.nextJobIdToExecuteFirstInFirstOut()).thenReturn(Optional.of(jobUUID)); + Set currentEncryptionPoolIds = Collections.emptySet(); + when(encryptionService.getCurrentEncryptionPoolIds()).thenReturn(currentEncryptionPoolIds); + when(jobRepository.nextJobIdToExecuteFirstInFirstOut(currentEncryptionPoolIds)).thenReturn(Optional.of(jobUUID)); /* execute */ UUID result = strategyToTest.nextJobId(); /* test */ assertEquals(jobUUID, result); - verify(jobRepository).nextJobIdToExecuteFirstInFirstOut(); + verify(jobRepository).nextJobIdToExecuteFirstInFirstOut(currentEncryptionPoolIds); } } diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/strategy/OnlyOneScanPerProjectAndModuleGroupAtSameTimeStrategyTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/strategy/OnlyOneScanPerProjectAndModuleGroupAtSameTimeStrategyTest.java index 731c023a13..681ce41f86 100644 --- a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/strategy/OnlyOneScanPerProjectAndModuleGroupAtSameTimeStrategyTest.java +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/strategy/OnlyOneScanPerProjectAndModuleGroupAtSameTimeStrategyTest.java @@ -4,12 +4,15 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +import java.util.Collections; import java.util.Optional; +import java.util.Set; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionService; import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobRepository; class OnlyOneScanPerProjectAndModuleGroupAtSameTimeStrategyTest { @@ -17,27 +20,32 @@ class OnlyOneScanPerProjectAndModuleGroupAtSameTimeStrategyTest { private OnlyOneScanPerProjectAndModuleGroupAtSameTimeStrategy strategyToTest; private SecHubJobRepository jobRepository; private UUID jobUUID; + private ScheduleEncryptionService encryptionService; @BeforeEach void beforeEach() { jobUUID = UUID.randomUUID(); jobRepository = mock(SecHubJobRepository.class); + encryptionService = mock(ScheduleEncryptionService.class); + strategyToTest = new OnlyOneScanPerProjectAndModuleGroupAtSameTimeStrategy(); strategyToTest.jobRepository = jobRepository; + strategyToTest.encryptionService = encryptionService; } @Test void nextJobId_calls_expected_query_method() { /* prepare */ - when(jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted()).thenReturn(Optional.of(jobUUID)); + Set supportedPoolIds = Collections.emptySet(); + when(jobRepository.nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(supportedPoolIds)).thenReturn(Optional.of(jobUUID)); /* execute */ UUID result = strategyToTest.nextJobId(); /* test */ assertEquals(jobUUID, result); - verify(jobRepository).nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(); + verify(jobRepository).nextJobIdToExecuteForProjectAndModuleGroupNotYetExecuted(supportedPoolIds); } } diff --git a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/strategy/OnlyOneScanPerProjectAtSameTimeStrategyTest.java b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/strategy/OnlyOneScanPerProjectAtSameTimeStrategyTest.java index 3ed0fd7a06..5228496bb6 100644 --- a/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/strategy/OnlyOneScanPerProjectAtSameTimeStrategyTest.java +++ b/sechub-schedule/src/test/java/com/mercedesbenz/sechub/domain/schedule/strategy/OnlyOneScanPerProjectAtSameTimeStrategyTest.java @@ -4,12 +4,15 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +import java.util.Collections; import java.util.Optional; +import java.util.Set; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionService; import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobRepository; class OnlyOneScanPerProjectAtSameTimeStrategyTest { @@ -17,27 +20,32 @@ class OnlyOneScanPerProjectAtSameTimeStrategyTest { private OnlyOneScanPerProjectAtSameTimeStrategy strategyToTest; private SecHubJobRepository jobRepository; private UUID jobUUID; + private ScheduleEncryptionService encryptionService; @BeforeEach void beforeEach() { jobUUID = UUID.randomUUID(); jobRepository = mock(SecHubJobRepository.class); + encryptionService = mock(ScheduleEncryptionService.class); + strategyToTest = new OnlyOneScanPerProjectAtSameTimeStrategy(); strategyToTest.jobRepository = jobRepository; + strategyToTest.encryptionService = encryptionService; } @Test void nextJobId_calls_expected_query_method() { /* prepare */ - when(jobRepository.nextJobIdToExecuteForProjectNotYetExecuted()).thenReturn(Optional.of(jobUUID)); + Set set = Collections.emptySet(); + when(jobRepository.nextJobIdToExecuteForProjectNotYetExecuted(set)).thenReturn(Optional.of(jobUUID)); /* execute */ UUID result = strategyToTest.nextJobId(); /* test */ assertEquals(jobUUID, result); - verify(jobRepository).nextJobIdToExecuteForProjectNotYetExecuted(); + verify(jobRepository).nextJobIdToExecuteForProjectNotYetExecuted(set); } } diff --git a/sechub-server/build.gradle b/sechub-server/build.gradle index 47a2e1ed1b..0c3a26b419 100644 --- a/sechub-server/build.gradle +++ b/sechub-server/build.gradle @@ -20,6 +20,7 @@ dependencies { implementation(library.apache_commons_fileupload2_jakarta) testImplementation project(':sechub-testframework') + testImplementation project(':sechub-commons-encryption') // necessary because auf Scheduler smoke test... } task assembleArtifact(type: Zip, group: 'sechub') { diff --git a/sechub-server/src/main/java/com/mercedesbenz/sechub/server/IntegrationTestServerPersistentTestDataCleaner.java b/sechub-server/src/main/java/com/mercedesbenz/sechub/server/IntegrationTestServerPersistentTestDataCleaner.java index b78d88c94f..be92819e58 100644 --- a/sechub-server/src/main/java/com/mercedesbenz/sechub/server/IntegrationTestServerPersistentTestDataCleaner.java +++ b/sechub-server/src/main/java/com/mercedesbenz/sechub/server/IntegrationTestServerPersistentTestDataCleaner.java @@ -20,6 +20,14 @@ * persisted test data (file storage). See `PersistentScenarioTestDataProvider` * inside integration test project for more details. * + * Reason: - builds on servers can be done without gradle clean - local + * integration tests use similar names (can be useful for h2 server mode + SQL + * queries) + * + * If this is not wanted (e.g. for testing locally with a postgres database and + * restarting the server multiple times) a developer can launch the server with + * system property {@value #SKIP_AUTOCLEAN_PROPERTY} having value 'true' + * * @author Albert Tregnaghi * */ @@ -28,13 +36,40 @@ public class IntegrationTestServerPersistentTestDataCleaner { private static final Logger LOG = LoggerFactory.getLogger(IntegrationTestServerPersistentTestDataCleaner.class); + private static final String SKIP_AUTOCLEAN_PROPERTY = "sechub.integrationtest.data.autoclean.skip"; + + @Value("${" + SKIP_AUTOCLEAN_PROPERTY + ":false}") + boolean autoCleanDisabled; + @Value("${sechub.integrationtest.ignore.missing.serverproject:false}") boolean ignoreMissingServerProject; @Bean @Order(100) @Profile(Profiles.INTEGRATIONTEST) - public CommandLineRunner dropIntegrationTestData() { + public CommandLineRunner dropOldIntegrationTestData() { + LOG.info("*".repeat(100)); + LOG.info("* Integration test auto clean"); + LOG.info("* ---------------------------"); + LOG.info("* - cleans growing ids"); + LOG.info("* - cleans all local integration test data"); + LOG.info("* - can be skipped with key: {}", SKIP_AUTOCLEAN_PROPERTY); + if (autoCleanDisabled) { + LOG.info("* - SKIPPED"); + LOG.info("*".repeat(100)); + } else { + LOG.info("* - STARTING"); + LOG.info("*".repeat(100)); + + cleanupOldIntegrationTestData(); + } + + return args -> { + }; + } + + private void cleanupOldIntegrationTestData() { + /* * When we start a new integration test server, we always drop former persisted * integration test data @@ -51,17 +86,18 @@ public CommandLineRunner dropIntegrationTestData() { } } } + File file = new File(parent, "build/sechub/integrationtest"); + String absolutePath = file.toPath().toAbsolutePath().toString(); if (file.exists()) { - LOG.info("Start removing old integration test data from {}", file.getAbsolutePath()); + LOG.info("Start removing old integration test data from {}", absolutePath); if (!FileSystemUtils.deleteRecursively(file)) { throw new IllegalStateException("Not able to destroy former integrationtest data on new integration test server startup!"); } } else { - LOG.info("No persisted integration test data found at {}", file.getAbsolutePath()); + LOG.info("No persisted integration test data found at {}", absolutePath); } - return args -> { - }; + } } diff --git a/sechub-server/src/main/resources/application-h2.properties b/sechub-server/src/main/resources/application-h2.properties index 1c83c4ceae..acc290b183 100644 --- a/sechub-server/src/main/resources/application-h2.properties +++ b/sechub-server/src/main/resources/application-h2.properties @@ -2,3 +2,5 @@ # only used in development or demo mode - so username password does not matter... spring.datasource.driver-class-name=org.h2.Driver spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1 + +spring.flyway.locations=/db/migration/common,/db/migration/h2 \ No newline at end of file diff --git a/sechub-server/src/main/resources/application-integrationtest.yml b/sechub-server/src/main/resources/application-integrationtest.yml index 73055354e3..7e4da64df7 100644 --- a/sechub-server/src/main/resources/application-integrationtest.yml +++ b/sechub-server/src/main/resources/application-integrationtest.yml @@ -21,6 +21,14 @@ sechub: # one check (which is always done) except multiple. This is to avoid flaky tests, # but gives us possiblity to check multiple ones when necessary (default are 60 seconds # and this too long for test...) + schedule: + encryption: + refresh: + initialdelay: 100 + # every 2000 milliseconds in integration tests + delay: 2000 + accept-outdated: + milliseconds: 100 scan: scanconfig: refresh: diff --git a/sechub-server/src/main/resources/application-postgres.properties b/sechub-server/src/main/resources/application-postgres.properties index c072b3e1fe..abc8aeecd9 100644 --- a/sechub-server/src/main/resources/application-postgres.properties +++ b/sechub-server/src/main/resources/application-postgres.properties @@ -9,4 +9,6 @@ spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect # for production you must change it by setting environment entries. E.g. in k8s deployment spring.datasource.url=${POSTGRES_DB_URL} spring.datasource.username=${POSTGRES_DB_USERNAME} -spring.datasource.password=${POSTGRES_DB_PASSWORD} \ No newline at end of file +spring.datasource.password=${POSTGRES_DB_PASSWORD} + +spring.flyway.locations=/db/migration/common,/db/migration/postgres \ No newline at end of file diff --git a/sechub-server/src/main/resources/db/migration/U10__project_metadata.sql b/sechub-server/src/main/resources/db/migration/common/U10__project_metadata.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U10__project_metadata.sql rename to sechub-server/src/main/resources/db/migration/common/U10__project_metadata.sql diff --git a/sechub-server/src/main/resources/db/migration/U11__config_uuid_in_product_result.sql b/sechub-server/src/main/resources/db/migration/common/U11__config_uuid_in_product_result.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U11__config_uuid_in_product_result.sql rename to sechub-server/src/main/resources/db/migration/common/U11__config_uuid_in_product_result.sql diff --git a/sechub-server/src/main/resources/db/migration/U12__project_access_level.sql b/sechub-server/src/main/resources/db/migration/common/U12__project_access_level.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U12__project_access_level.sql rename to sechub-server/src/main/resources/db/migration/common/U12__project_access_level.sql diff --git a/sechub-server/src/main/resources/db/migration/U13__sechub_report_has_more_fields.sql b/sechub-server/src/main/resources/db/migration/common/U13__sechub_report_has_more_fields.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U13__sechub_report_has_more_fields.sql rename to sechub-server/src/main/resources/db/migration/common/U13__sechub_report_has_more_fields.sql diff --git a/sechub-server/src/main/resources/db/migration/U14__sechub_report_type_bugfix.sql b/sechub-server/src/main/resources/db/migration/common/U14__sechub_report_type_bugfix.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U14__sechub_report_type_bugfix.sql rename to sechub-server/src/main/resources/db/migration/common/U14__sechub_report_type_bugfix.sql diff --git a/sechub-server/src/main/resources/db/migration/U15__provide_auto_cleanup.sql b/sechub-server/src/main/resources/db/migration/common/U15__provide_auto_cleanup.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U15__provide_auto_cleanup.sql rename to sechub-server/src/main/resources/db/migration/common/U15__provide_auto_cleanup.sql diff --git a/sechub-server/src/main/resources/db/migration/U16__rename_admin_config_to_adm_config.sql b/sechub-server/src/main/resources/db/migration/common/U16__rename_admin_config_to_adm_config.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U16__rename_admin_config_to_adm_config.sql rename to sechub-server/src/main/resources/db/migration/common/U16__rename_admin_config_to_adm_config.sql diff --git a/sechub-server/src/main/resources/db/migration/U17__add_messages_to_scheduler_status.sql b/sechub-server/src/main/resources/db/migration/common/U17__add_messages_to_scheduler_status.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U17__add_messages_to_scheduler_status.sql rename to sechub-server/src/main/resources/db/migration/common/U17__add_messages_to_scheduler_status.sql diff --git a/sechub-server/src/main/resources/db/migration/U18__add_messages_to_product_result.sql b/sechub-server/src/main/resources/db/migration/common/U18__add_messages_to_product_result.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U18__add_messages_to_product_result.sql rename to sechub-server/src/main/resources/db/migration/common/U18__add_messages_to_product_result.sql diff --git a/sechub-server/src/main/resources/db/migration/U19__add_statistic_tables.sql b/sechub-server/src/main/resources/db/migration/common/U19__add_statistic_tables.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U19__add_statistic_tables.sql rename to sechub-server/src/main/resources/db/migration/common/U19__add_statistic_tables.sql diff --git a/sechub-server/src/main/resources/db/migration/U1__Initial_version.sql b/sechub-server/src/main/resources/db/migration/common/U1__Initial_version.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U1__Initial_version.sql rename to sechub-server/src/main/resources/db/migration/common/U1__Initial_version.sql diff --git a/sechub-server/src/main/resources/db/migration/U20__add_scan_group_to_scheduler_job2.sql b/sechub-server/src/main/resources/db/migration/common/U20__add_scan_group_to_scheduler_job2.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U20__add_scan_group_to_scheduler_job2.sql rename to sechub-server/src/main/resources/db/migration/common/U20__add_scan_group_to_scheduler_job2.sql diff --git a/sechub-server/src/main/resources/db/migration/U21__delete_old_status_keys.sql b/sechub-server/src/main/resources/db/migration/common/U21__delete_old_status_keys.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U21__delete_old_status_keys.sql rename to sechub-server/src/main/resources/db/migration/common/U21__delete_old_status_keys.sql diff --git a/sechub-server/src/main/resources/db/migration/U22__add_schedule_jobdata.sql b/sechub-server/src/main/resources/db/migration/common/U22__add_schedule_jobdata.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U22__add_schedule_jobdata.sql rename to sechub-server/src/main/resources/db/migration/common/U22__add_schedule_jobdata.sql diff --git a/sechub-server/src/main/resources/db/migration/U23__drop_spring_batch_tables.sql b/sechub-server/src/main/resources/db/migration/common/U23__drop_spring_batch_tables.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U23__drop_spring_batch_tables.sql rename to sechub-server/src/main/resources/db/migration/common/U23__drop_spring_batch_tables.sql diff --git a/sechub-server/src/main/resources/db/migration/U24__drop_and_recreate_adm_jobinformation_tables.sql b/sechub-server/src/main/resources/db/migration/common/U24__drop_and_recreate_adm_jobinformation_tables.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U24__drop_and_recreate_adm_jobinformation_tables.sql rename to sechub-server/src/main/resources/db/migration/common/U24__drop_and_recreate_adm_jobinformation_tables.sql diff --git a/sechub-server/src/main/resources/db/migration/U25__enlarge_data_fields.sql b/sechub-server/src/main/resources/db/migration/common/U25__enlarge_data_fields.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U25__enlarge_data_fields.sql rename to sechub-server/src/main/resources/db/migration/common/U25__enlarge_data_fields.sql diff --git a/sechub-server/src/main/resources/db/migration/U26__statistic_indices.sql b/sechub-server/src/main/resources/db/migration/common/U26__statistic_indices.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U26__statistic_indices.sql rename to sechub-server/src/main/resources/db/migration/common/U26__statistic_indices.sql diff --git a/sechub-server/src/main/resources/db/migration/U27__rename_emailAdress_column.sql b/sechub-server/src/main/resources/db/migration/common/U27__rename_emailAdress_column.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U27__rename_emailAdress_column.sql rename to sechub-server/src/main/resources/db/migration/common/U27__rename_emailAdress_column.sql diff --git a/sechub-server/src/main/resources/db/migration/U28__enlarge_project_id.sql b/sechub-server/src/main/resources/db/migration/common/U28__enlarge_project_id.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U28__enlarge_project_id.sql rename to sechub-server/src/main/resources/db/migration/common/U28__enlarge_project_id.sql diff --git a/sechub-server/src/main/resources/db/migration/common/U29__encryption.sql b/sechub-server/src/main/resources/db/migration/common/U29__encryption.sql new file mode 100644 index 0000000000..7be49330ef --- /dev/null +++ b/sechub-server/src/main/resources/db/migration/common/U29__encryption.sql @@ -0,0 +1,14 @@ +-- SPDX-License-Identifier: MIT + +-- Remark: the scripts U31-U29 are only to provide the downgrade per SQL +-- but it IS NOT recommended to do this with encrypted data inside! This will +-- only work if all jobs are encrypted with NoneCipher, otherwise this means configuration data +-- loss because it will not contain valid json + +-- (U30 scripts will have copied encrypted conifg bytes as text to configuration) +ALTER TABLE schedule_sechub_job DROP COLUMN encrypted_configuration; +ALTER TABLE schedule_sechub_job DROP COLUMN encrypt_initial_vector; +ALTER TABLE schedule_sechub_job DROP COLUMN encrypt_pool_data_id; + +-- encryption parts +DROP TABLE schedule_cipher_pool_data; \ No newline at end of file diff --git a/sechub-server/src/main/resources/db/migration/common/U29__encryption_common.sql b/sechub-server/src/main/resources/db/migration/common/U29__encryption_common.sql new file mode 100644 index 0000000000..d49159b18f --- /dev/null +++ b/sechub-server/src/main/resources/db/migration/common/U29__encryption_common.sql @@ -0,0 +1,10 @@ +-- SPDX-License-Identifier: MIT +ALTER TABLE schedule_sechub_job DROP COLUMN encrypted_configuration bytea; +ALTER TABLE schedule_sechub_job DROP COLUMN encrypt_initial_vector bytea; +ALTER TABLE schedule_sechub_job DROP COLUMN encrypt_pool_data_id integer; + +DROP TABLE IF EXISTS schedule_cipher_pool_data; + +ALTER TABLE adm_job_information ADD COLUMN configuration varchar(8912); +ALTER TABLE scan_project_log ADD COLUMN config varchar(8912); +ALTER TABLE scan_report ADD COLUMN config varchar(8912); diff --git a/sechub-server/src/main/resources/db/migration/U2__provide_scheduler_start_stop_and_status.sql b/sechub-server/src/main/resources/db/migration/common/U2__provide_scheduler_start_stop_and_status.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U2__provide_scheduler_start_stop_and_status.sql rename to sechub-server/src/main/resources/db/migration/common/U2__provide_scheduler_start_stop_and_status.sql diff --git a/sechub-server/src/main/resources/db/migration/common/U31__encryption_drop_unencrypted_config.sql b/sechub-server/src/main/resources/db/migration/common/U31__encryption_drop_unencrypted_config.sql new file mode 100644 index 0000000000..2ab7dc4c77 --- /dev/null +++ b/sechub-server/src/main/resources/db/migration/common/U31__encryption_drop_unencrypted_config.sql @@ -0,0 +1,15 @@ +-- SPDX-License-Identifier: MIT + +-- Remark: the scripts U31-U29 are only to provide the downgrade per SQL +-- but it IS NOT recommended to do this with encrypted data inside! This will +-- only work if all jobs are encrypted with NoneCipher, otherwise this means configuration data +-- loss because it will not contain valid json + + +-- revert table structure add old columns again + +ALTER TABLE schedule_sechub_job ADD COLUMN configuration varchar(8192); +-- Only one configuration persistence shall exist inside SecHub database: +ALTER TABLE adm_job_information ADD COLUMN configuration varchar(8192); +ALTER TABLE scan_project_log DROP COLUMN config varchar(8912); +ALTER TABLE scan_report DROP COLUMN config varchar(8912); diff --git a/sechub-server/src/main/resources/db/migration/U3__store_projectid_in_product_results.sql b/sechub-server/src/main/resources/db/migration/common/U3__store_projectid_in_product_results.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U3__store_projectid_in_product_results.sql rename to sechub-server/src/main/resources/db/migration/common/U3__store_projectid_in_product_results.sql diff --git a/sechub-server/src/main/resources/db/migration/U4__provide_scan_project_config.sql b/sechub-server/src/main/resources/db/migration/common/U4__provide_scan_project_config.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U4__provide_scan_project_config.sql rename to sechub-server/src/main/resources/db/migration/common/U4__provide_scan_project_config.sql diff --git a/sechub-server/src/main/resources/db/migration/U5__provide_scanconfig_without_restarts.sql b/sechub-server/src/main/resources/db/migration/common/U5__provide_scanconfig_without_restarts.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U5__provide_scanconfig_without_restarts.sql rename to sechub-server/src/main/resources/db/migration/common/U5__provide_scanconfig_without_restarts.sql diff --git a/sechub-server/src/main/resources/db/migration/U6__provide_metadata_in_product_result.sql b/sechub-server/src/main/resources/db/migration/common/U6__provide_metadata_in_product_result.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U6__provide_metadata_in_product_result.sql rename to sechub-server/src/main/resources/db/migration/common/U6__provide_metadata_in_product_result.sql diff --git a/sechub-server/src/main/resources/db/migration/U7__provide_job_restart_changes.sql b/sechub-server/src/main/resources/db/migration/common/U7__provide_job_restart_changes.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U7__provide_job_restart_changes.sql rename to sechub-server/src/main/resources/db/migration/common/U7__provide_job_restart_changes.sql diff --git a/sechub-server/src/main/resources/db/migration/U8__enlarge_project_config_data_field.sql b/sechub-server/src/main/resources/db/migration/common/U8__enlarge_project_config_data_field.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U8__enlarge_project_config_data_field.sql rename to sechub-server/src/main/resources/db/migration/common/U8__enlarge_project_config_data_field.sql diff --git a/sechub-server/src/main/resources/db/migration/U9__runtime_product_configuration.sql b/sechub-server/src/main/resources/db/migration/common/U9__runtime_product_configuration.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/U9__runtime_product_configuration.sql rename to sechub-server/src/main/resources/db/migration/common/U9__runtime_product_configuration.sql diff --git a/sechub-server/src/main/resources/db/migration/V10__project_metadata.sql b/sechub-server/src/main/resources/db/migration/common/V10__project_metadata.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V10__project_metadata.sql rename to sechub-server/src/main/resources/db/migration/common/V10__project_metadata.sql diff --git a/sechub-server/src/main/resources/db/migration/V11__config_uuid_in_product_results.sql b/sechub-server/src/main/resources/db/migration/common/V11__config_uuid_in_product_results.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V11__config_uuid_in_product_results.sql rename to sechub-server/src/main/resources/db/migration/common/V11__config_uuid_in_product_results.sql diff --git a/sechub-server/src/main/resources/db/migration/V12__project_access_level.sql b/sechub-server/src/main/resources/db/migration/common/V12__project_access_level.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V12__project_access_level.sql rename to sechub-server/src/main/resources/db/migration/common/V12__project_access_level.sql diff --git a/sechub-server/src/main/resources/db/migration/V13__sechub_report_has_more_fields.sql b/sechub-server/src/main/resources/db/migration/common/V13__sechub_report_has_more_fields.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V13__sechub_report_has_more_fields.sql rename to sechub-server/src/main/resources/db/migration/common/V13__sechub_report_has_more_fields.sql diff --git a/sechub-server/src/main/resources/db/migration/V14__sechub_report_type_bugfix.sql b/sechub-server/src/main/resources/db/migration/common/V14__sechub_report_type_bugfix.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V14__sechub_report_type_bugfix.sql rename to sechub-server/src/main/resources/db/migration/common/V14__sechub_report_type_bugfix.sql diff --git a/sechub-server/src/main/resources/db/migration/V15__provide_auto_cleanup.sql b/sechub-server/src/main/resources/db/migration/common/V15__provide_auto_cleanup.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V15__provide_auto_cleanup.sql rename to sechub-server/src/main/resources/db/migration/common/V15__provide_auto_cleanup.sql diff --git a/sechub-server/src/main/resources/db/migration/V16__rename_admin_config_to_adm_config.sql b/sechub-server/src/main/resources/db/migration/common/V16__rename_admin_config_to_adm_config.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V16__rename_admin_config_to_adm_config.sql rename to sechub-server/src/main/resources/db/migration/common/V16__rename_admin_config_to_adm_config.sql diff --git a/sechub-server/src/main/resources/db/migration/V17__add_messages_to_scheduler_status.sql b/sechub-server/src/main/resources/db/migration/common/V17__add_messages_to_scheduler_status.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V17__add_messages_to_scheduler_status.sql rename to sechub-server/src/main/resources/db/migration/common/V17__add_messages_to_scheduler_status.sql diff --git a/sechub-server/src/main/resources/db/migration/V18__add_messages_to_product_result.sql b/sechub-server/src/main/resources/db/migration/common/V18__add_messages_to_product_result.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V18__add_messages_to_product_result.sql rename to sechub-server/src/main/resources/db/migration/common/V18__add_messages_to_product_result.sql diff --git a/sechub-server/src/main/resources/db/migration/V19__add_statistic_tables.sql b/sechub-server/src/main/resources/db/migration/common/V19__add_statistic_tables.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V19__add_statistic_tables.sql rename to sechub-server/src/main/resources/db/migration/common/V19__add_statistic_tables.sql diff --git a/sechub-server/src/main/resources/db/migration/V1__Initial_version.sql b/sechub-server/src/main/resources/db/migration/common/V1__Initial_version.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V1__Initial_version.sql rename to sechub-server/src/main/resources/db/migration/common/V1__Initial_version.sql diff --git a/sechub-server/src/main/resources/db/migration/V20__add_scan_group_to_scheduler_job.sql b/sechub-server/src/main/resources/db/migration/common/V20__add_scan_group_to_scheduler_job.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V20__add_scan_group_to_scheduler_job.sql rename to sechub-server/src/main/resources/db/migration/common/V20__add_scan_group_to_scheduler_job.sql diff --git a/sechub-server/src/main/resources/db/migration/V21__delete_old_adm_status_keys.sql b/sechub-server/src/main/resources/db/migration/common/V21__delete_old_adm_status_keys.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V21__delete_old_adm_status_keys.sql rename to sechub-server/src/main/resources/db/migration/common/V21__delete_old_adm_status_keys.sql diff --git a/sechub-server/src/main/resources/db/migration/V22__add_schedule_jobdata.sql b/sechub-server/src/main/resources/db/migration/common/V22__add_schedule_jobdata.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V22__add_schedule_jobdata.sql rename to sechub-server/src/main/resources/db/migration/common/V22__add_schedule_jobdata.sql diff --git a/sechub-server/src/main/resources/db/migration/V23__drop_spring_batch_tables.sql b/sechub-server/src/main/resources/db/migration/common/V23__drop_spring_batch_tables.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V23__drop_spring_batch_tables.sql rename to sechub-server/src/main/resources/db/migration/common/V23__drop_spring_batch_tables.sql diff --git a/sechub-server/src/main/resources/db/migration/V24__drop_and_recreate_adm_jobinformation_tables.sql b/sechub-server/src/main/resources/db/migration/common/V24__drop_and_recreate_adm_jobinformation_tables.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V24__drop_and_recreate_adm_jobinformation_tables.sql rename to sechub-server/src/main/resources/db/migration/common/V24__drop_and_recreate_adm_jobinformation_tables.sql diff --git a/sechub-server/src/main/resources/db/migration/V25__enlarge_data_fields.sql b/sechub-server/src/main/resources/db/migration/common/V25__enlarge_data_fields.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V25__enlarge_data_fields.sql rename to sechub-server/src/main/resources/db/migration/common/V25__enlarge_data_fields.sql diff --git a/sechub-server/src/main/resources/db/migration/V26__statistic_indices.sql b/sechub-server/src/main/resources/db/migration/common/V26__statistic_indices.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V26__statistic_indices.sql rename to sechub-server/src/main/resources/db/migration/common/V26__statistic_indices.sql diff --git a/sechub-server/src/main/resources/db/migration/V27__rename_emailAdress_column.sql b/sechub-server/src/main/resources/db/migration/common/V27__rename_emailAdress_column.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V27__rename_emailAdress_column.sql rename to sechub-server/src/main/resources/db/migration/common/V27__rename_emailAdress_column.sql diff --git a/sechub-server/src/main/resources/db/migration/V28__enlarge_project_id.sql b/sechub-server/src/main/resources/db/migration/common/V28__enlarge_project_id.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V28__enlarge_project_id.sql rename to sechub-server/src/main/resources/db/migration/common/V28__enlarge_project_id.sql diff --git a/sechub-server/src/main/resources/db/migration/common/V29__encryption_common.sql b/sechub-server/src/main/resources/db/migration/common/V29__encryption_common.sql new file mode 100644 index 0000000000..fead836da0 --- /dev/null +++ b/sechub-server/src/main/resources/db/migration/common/V29__encryption_common.sql @@ -0,0 +1,34 @@ +-- SPDX-License-Identifier: MIT +ALTER TABLE schedule_sechub_job ADD COLUMN encrypted_configuration bytea; +ALTER TABLE schedule_sechub_job ADD COLUMN encrypt_initial_vector bytea; +ALTER TABLE schedule_sechub_job ADD COLUMN encrypt_pool_data_id integer; + +ALTER TABLE schedule_sechub_job ALTER COLUMN configuration DROP NOT NULL; -- now we allow null + +ALTER TABLE schedule_sechub_job RENAME COLUMN configuration to unencrypted_configuration; -- necessary to have no old server version to be able to add accidently new sechub jobs while rolling udpdates are done (e.g. K8s deployment). We accept/force here that an old server create job would crash now... + + +-- encryption parts +CREATE TABLE schedule_cipher_pool_data +( + pool_id integer not null generated by default as identity, + + pool_algorithm varchar(80) not null, + pool_pwd_src_type varchar(80) not null, + pool_pwd_src_data varchar(255), + + pool_test_text varchar(255) not null, + pool_test_initial_vector bytea not null, + pool_test_encrypted bytea not null, + + pool_creation_timestamp timestamp not null, + pool_created_from varchar(60), -- we accept 60 (3 x 20) see UserIdValidation + + version integer, + PRIMARY KEY (pool_id) +); + +-- Only one configuration persistence shall exist inside SecHub database: +ALTER TABLE adm_job_information DROP COLUMN configuration; -- we remove the configuration here +ALTER TABLE scan_project_log DROP COLUMN config; -- we remove the configuration here +ALTER TABLE scan_report DROP COLUMN config; -- we remove the configuration here diff --git a/sechub-server/src/main/resources/db/migration/V2__provide_scheduler_start_stop_and_status.sql b/sechub-server/src/main/resources/db/migration/common/V2__provide_scheduler_start_stop_and_status.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V2__provide_scheduler_start_stop_and_status.sql rename to sechub-server/src/main/resources/db/migration/common/V2__provide_scheduler_start_stop_and_status.sql diff --git a/sechub-server/src/main/resources/db/migration/common/V31__encryption_drop_unencrypted_config.sql b/sechub-server/src/main/resources/db/migration/common/V31__encryption_drop_unencrypted_config.sql new file mode 100644 index 0000000000..bdfa827d6d --- /dev/null +++ b/sechub-server/src/main/resources/db/migration/common/V31__encryption_drop_unencrypted_config.sql @@ -0,0 +1,3 @@ +-- SPDX-License-Identifier: MIT +-- remove old unencrypted config column +ALTER TABLE schedule_sechub_job DROP COLUMN unencrypted_configuration; \ No newline at end of file diff --git a/sechub-server/src/main/resources/db/migration/V3__store_projectid_in_product_results.sql b/sechub-server/src/main/resources/db/migration/common/V3__store_projectid_in_product_results.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V3__store_projectid_in_product_results.sql rename to sechub-server/src/main/resources/db/migration/common/V3__store_projectid_in_product_results.sql diff --git a/sechub-server/src/main/resources/db/migration/V4__provide_scan_project_config.sql b/sechub-server/src/main/resources/db/migration/common/V4__provide_scan_project_config.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V4__provide_scan_project_config.sql rename to sechub-server/src/main/resources/db/migration/common/V4__provide_scan_project_config.sql diff --git a/sechub-server/src/main/resources/db/migration/V5__provide_scanconfig_without_restarts.sql b/sechub-server/src/main/resources/db/migration/common/V5__provide_scanconfig_without_restarts.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V5__provide_scanconfig_without_restarts.sql rename to sechub-server/src/main/resources/db/migration/common/V5__provide_scanconfig_without_restarts.sql diff --git a/sechub-server/src/main/resources/db/migration/V6__provide_metadata_in_product_result.sql b/sechub-server/src/main/resources/db/migration/common/V6__provide_metadata_in_product_result.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V6__provide_metadata_in_product_result.sql rename to sechub-server/src/main/resources/db/migration/common/V6__provide_metadata_in_product_result.sql diff --git a/sechub-server/src/main/resources/db/migration/V7__provide_job_restart_changes.sql b/sechub-server/src/main/resources/db/migration/common/V7__provide_job_restart_changes.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V7__provide_job_restart_changes.sql rename to sechub-server/src/main/resources/db/migration/common/V7__provide_job_restart_changes.sql diff --git a/sechub-server/src/main/resources/db/migration/V8__enlarge_project_config_data_field.sql b/sechub-server/src/main/resources/db/migration/common/V8__enlarge_project_config_data_field.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V8__enlarge_project_config_data_field.sql rename to sechub-server/src/main/resources/db/migration/common/V8__enlarge_project_config_data_field.sql diff --git a/sechub-server/src/main/resources/db/migration/V9__runtime_product_configuration.sql b/sechub-server/src/main/resources/db/migration/common/V9__runtime_product_configuration.sql similarity index 100% rename from sechub-server/src/main/resources/db/migration/V9__runtime_product_configuration.sql rename to sechub-server/src/main/resources/db/migration/common/V9__runtime_product_configuration.sql diff --git a/sechub-server/src/main/resources/db/migration/h2/U30__encryption_first_cipher_h2.sql b/sechub-server/src/main/resources/db/migration/h2/U30__encryption_first_cipher_h2.sql new file mode 100644 index 0000000000..a06266efc1 --- /dev/null +++ b/sechub-server/src/main/resources/db/migration/h2/U30__encryption_first_cipher_h2.sql @@ -0,0 +1,8 @@ +-- SPDX-License-Identifier: MIT + +-- In postgres we do a migration +-- But here, for h2, we do not migrate old data, because +-- h2 is only for testing! +-- Means no job migration done here. + +DELETE FROM schedule_cipher_pool_data; \ No newline at end of file diff --git a/sechub-server/src/main/resources/db/migration/h2/V30__encryption_first_cipher_h2.sql b/sechub-server/src/main/resources/db/migration/h2/V30__encryption_first_cipher_h2.sql new file mode 100644 index 0000000000..a669a0722b --- /dev/null +++ b/sechub-server/src/main/resources/db/migration/h2/V30__encryption_first_cipher_h2.sql @@ -0,0 +1,35 @@ +-- SPDX-License-Identifier: MIT +INSERT INTO schedule_cipher_pool_data ( + pool_id, + + pool_algorithm, + pool_pwd_src_type, + pool_pwd_src_data, + + pool_test_text, + pool_test_initial_vector, + pool_test_encrypted, + + pool_creation_timestamp, + pool_created_from, + version + ) +VALUES( + 0, -- pool_id + + 'NONE', -- pool_algorithm: CipherAlgorithm:NONE + 'NONE', -- pool_pwd_src_type: CipherPasswordSourceType:NONE + null, -- pool_pwd_src_data + + 'test-text1', --pool_test_text: plain text + X'6E6F6E65', -- pool_test_initial_vector: as bytes of simple text: "none" + X'746573742d7465787431', --decode('dGVzdC10ZXh0MQ==', 'base64'), -- pool_test_encrypted: "test-text1" just as plain text bytes from base64 + + now(), -- created : SQL 92 spec, + null, -- createdFrom: not user created + 0 +); + +-- In postgres we do a migration +-- But here, for h2, we do not migrate old data, because +-- h2 is only for testing! \ No newline at end of file diff --git a/sechub-server/src/main/resources/db/migration/postgres/U30__encryption_first_cipher_postgres.sql b/sechub-server/src/main/resources/db/migration/postgres/U30__encryption_first_cipher_postgres.sql new file mode 100644 index 0000000000..a1cfbc6744 --- /dev/null +++ b/sechub-server/src/main/resources/db/migration/postgres/U30__encryption_first_cipher_postgres.sql @@ -0,0 +1,11 @@ +-- SPDX-License-Identifier: MIT + +-- Remark: the scripts U31-U29 are only to provide the downgrade per SQL +-- but it IS NOT recommended to do this with encrypted data inside! This will +-- only work if all jobs are encrypted with NoneCipher, otherwise this means configuration data +-- loss because it will not contain valid json + + +-- convert to var char - we expect unencrypted_configuration is only "encrypted" with 'NONE' +update schedule_sechub_job ssj set + configuration = convert_from(encrypted_configuration, 'UTF8') diff --git a/sechub-server/src/main/resources/db/migration/postgres/V30__encryption_first_cipher_postgres.sql b/sechub-server/src/main/resources/db/migration/postgres/V30__encryption_first_cipher_postgres.sql new file mode 100644 index 0000000000..f3f66c7fe0 --- /dev/null +++ b/sechub-server/src/main/resources/db/migration/postgres/V30__encryption_first_cipher_postgres.sql @@ -0,0 +1,41 @@ +-- SPDX-License-Identifier: MIT +INSERT INTO schedule_cipher_pool_data ( + pool_id, + + pool_algorithm, + pool_pwd_src_type, + pool_pwd_src_data, + + pool_test_text, + pool_test_initial_vector, + pool_test_encrypted, + + pool_creation_timestamp, + pool_created_from, + version + ) +VALUES( + 0, -- pool_id + + 'NONE', -- pool_algorithm: CipherAlgorithm:NONE + 'NONE', -- pool_pwd_src_type: CipherPasswordSourceType:NONE + null, -- pool_pwd_src_data + + 'test-text1', --pool_test_text: plain text + decode('bm9uZQ==', 'base64'), -- pool_test_initial_vector: as bytes of simple text: "none" + decode('dGVzdC10ZXh0MQ==', 'base64'), -- pool_test_encrypted: "test-text1" just as plain text bytes from base64 + + now(), -- created : SQL 92 spec, + null, -- createdFrom: not user created + 0 +); + +-- Migrate unencrypted data to encrypted +-- Auto convert former unencrypted data to created NoneCipher pool entry: +-- see https://www.postgresql.org/docs/current/functions-binarystring.html#FUNCTIONS-BINARYSTRING-CONVERSIONS +update schedule_sechub_job ssj set + encrypted_configuration = convert_to(unencrypted_configuration, 'UTF8'), + encrypt_initial_vector = decode('bm9uZQ==', 'base64'), -- initial_vector: as bytes of simple text: "none" + encrypt_pool_data_id = 0; + + diff --git a/sechub-server/src/test/java/com/mercedesbenz/sechub/domain/scan/resolve/TargetResolverServiceSpringBootTest.java b/sechub-server/src/test/java/com/mercedesbenz/sechub/domain/scan/resolve/TargetResolverServiceSpringBootTest.java deleted file mode 100644 index 807a6d6491..0000000000 --- a/sechub-server/src/test/java/com/mercedesbenz/sechub/domain/scan/resolve/TargetResolverServiceSpringBootTest.java +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.domain.scan.resolve; - -import static org.junit.Assert.*; - -import java.net.Inet4Address; -import java.net.InetAddress; -import java.net.URI; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.TestPropertySource; - -import com.mercedesbenz.sechub.domain.scan.NetworkTarget; -import com.mercedesbenz.sechub.domain.scan.NetworkTargetType; -import com.mercedesbenz.sechub.sharedkernel.Profiles; - -/** - * Inside application-test.properties we have defined strategies, which will - * treat "*.intranet.example.com/org" and "192.168.*.*" as INTRANET.
    - *
    - * This integration test checks if the configured values are really used - * - * @author Albert Tregnaghi - * - */ -@SpringBootTest -@TestPropertySource(locations = "classpath:application-test.yml") -@ActiveProfiles(Profiles.TEST) -public class TargetResolverServiceSpringBootTest { - - @Autowired - TargetResolverService serviceToTest; - - @Test - public void product_failure_demo_example_org__is_INTERNET() { - /* prepare */ - URI uri = URI.create("https://productfailure.demo.example.org"); - - /* execute */ - NetworkTarget found = serviceToTest.resolveTarget(uri); - - /* test */ - assertEquals(new NetworkTarget(uri, NetworkTargetType.INTERNET), found); - - } - - @Test - public void ip_172_217_22_99__IS_INTERNET() throws Exception { - /* prepare */ - InetAddress address = Inet4Address.getByName("172.217.22.99"); - - /* execute */ - NetworkTarget found = serviceToTest.resolveTarget(address); - - /* test */ - assertEquals(new NetworkTarget(address, NetworkTargetType.INTERNET), found); - - } - - @Test - public void something_intranet_example_org__is_INTRANET() { - /* prepare */ - URI uri = URI.create("https://something.intranet.example.org"); - - /* execute */ - NetworkTarget found = serviceToTest.resolveTarget(uri); - - /* test */ - assertEquals(new NetworkTarget(uri, NetworkTargetType.INTRANET), found); - - } - - @Test - public void ip_192_168_22_99__IS_INTRANET() throws Exception { - /* prepare */ - InetAddress address = Inet4Address.getByName("192.168.22.99"); - - /* execute */ - NetworkTarget found = serviceToTest.resolveTarget(address); - - /* test */ - assertEquals(new NetworkTarget(address, NetworkTargetType.INTRANET), found); - - } - - @Test - public void uri_hostname_startswith_192_IS_INTRANET() { - /* prepare */ - URI uri = URI.create("https://192.168.22.99:7777"); - - /* execute */ - NetworkTarget found = serviceToTest.resolveTarget(uri); - - /* test */ - assertEquals(new NetworkTarget(uri, NetworkTargetType.INTRANET), found); - - } - -} diff --git a/sechub-server/src/test/java/com/mercedesbenz/sechub/domain/schedule/PasswordHasherTestApplication.java b/sechub-server/src/test/java/com/mercedesbenz/sechub/domain/schedule/PasswordHasherTestApplication.java deleted file mode 100644 index 71ce2278f2..0000000000 --- a/sechub-server/src/test/java/com/mercedesbenz/sechub/domain/schedule/PasswordHasherTestApplication.java +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.domain.schedule; - -import org.springframework.security.crypto.factory.PasswordEncoderFactories; -import org.springframework.security.crypto.password.PasswordEncoder; - -public class PasswordHasherTestApplication { - - public static void main(String[] args) { - PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); - String input = args[0]; - String encoded = encoder.encode(input); - System.out.println("given:" + input); - System.out.println("encoded:" + encoded); - } -} diff --git a/sechub-server/src/test/java/com/mercedesbenz/sechub/domain/schedule/SchedulerCreateJobServiceSpringBootTest.java b/sechub-server/src/test/java/com/mercedesbenz/sechub/domain/schedule/SchedulerCreateJobServiceSpringBootTest.java deleted file mode 100644 index 3e4a046ffa..0000000000 --- a/sechub-server/src/test/java/com/mercedesbenz/sechub/domain/schedule/SchedulerCreateJobServiceSpringBootTest.java +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.domain.schedule; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.Optional; -import java.util.UUID; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.TestPropertySource; - -import com.mercedesbenz.sechub.domain.schedule.job.ScheduleSecHubJob; -import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobFactory; -import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobRepository; -import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.configuration.SecHubConfiguration; -import com.mercedesbenz.sechub.sharedkernel.error.NotFoundException; -import com.mercedesbenz.sechub.sharedkernel.validation.UserInputAssertion; - -@SpringBootTest -@TestPropertySource(locations = "classpath:application-test.yml") -@ActiveProfiles(Profiles.TEST) -public class SchedulerCreateJobServiceSpringBootTest { - - private static final String PROJECT_ID = "project1"; - - @Autowired - private SchedulerCreateJobService serviceToTest; - - @MockBean - private SecHubJobFactory jobFactory; - - @MockBean - private SecHubJobRepository jobRepository; - - @MockBean - private UserInputAssertion assertion; - - private SecHubConfiguration configuration; - private ScheduleSecHubJob nextJob; - - private UUID jobUUID; - - private String project; - - private String projectUUID = "projectId1"; - - @BeforeEach - public void beforeEach() { - jobUUID = UUID.randomUUID(); - nextJob = mock(ScheduleSecHubJob.class); - configuration = mock(SecHubConfiguration.class); - project = "projectId"; - - when(nextJob.getProjectId()).thenReturn(project); - - when(nextJob.getUUID()).thenReturn(jobUUID); - when(nextJob.getProjectId()).thenReturn(projectUUID); - when(jobFactory.createJob(eq(configuration))).thenReturn(nextJob); - - /* prepare */ - when(jobRepository.save(nextJob)).thenReturn(nextJob); - when(jobRepository.nextJobIdToExecuteFirstInFirstOut()).thenReturn(Optional.of(jobUUID)); - } - - @Test - public void scheduling_a_new_job_to_an_unexisting_project_throws_NOT_FOUND_exception() { - /* execute + test */ - Assertions.assertThrows(NotFoundException.class, () -> { - serviceToTest.createJob("a-project-not-existing", configuration); - }); - } - - @Test - public void no_access_entry__scheduling_a_configuration__will_throw_not_found_exception() { - /* execute + test */ - Assertions.assertThrows(NotFoundException.class, () -> { - serviceToTest.createJob(PROJECT_ID, configuration); - }); - } - - @Test - public void configuration_having_no_project_gets_project_from_URL() { - /* prepare */ - when(jobRepository.save(nextJob)).thenReturn(nextJob); - - /* execute + test */ - Assertions.assertThrows(NotFoundException.class, () -> { - serviceToTest.createJob(PROJECT_ID, configuration); - }); - } - -} diff --git a/sechub-server/src/test/java/com/mercedesbenz/sechub/domain/schedule/SchedulerGetJobStatusServiceTest.java b/sechub-server/src/test/java/com/mercedesbenz/sechub/domain/schedule/SchedulerGetJobStatusServiceTest.java deleted file mode 100644 index d3d9bf2433..0000000000 --- a/sechub-server/src/test/java/com/mercedesbenz/sechub/domain/schedule/SchedulerGetJobStatusServiceTest.java +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.domain.schedule; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.Optional; -import java.util.UUID; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.TestPropertySource; - -import com.mercedesbenz.sechub.domain.schedule.access.ScheduleAccessRepository; -import com.mercedesbenz.sechub.domain.schedule.job.ScheduleSecHubJob; -import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobFactory; -import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobRepository; -import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.configuration.SecHubConfiguration; -import com.mercedesbenz.sechub.sharedkernel.error.NotFoundException; -import com.mercedesbenz.sechub.sharedkernel.validation.UserInputAssertion; - -@SpringBootTest -@TestPropertySource(locations = "classpath:application-test.yml") -@ActiveProfiles(Profiles.TEST) -public class SchedulerGetJobStatusServiceTest { - - private static final String PROJECT_ID = "project1"; - - @Autowired - private SchedulerGetJobStatusService serviceToTest; - - @MockBean - private SecHubJobFactory jobFactory; - - @MockBean - private SecHubJobRepository jobRepository; - - @MockBean - private ScheduleAccessRepository projectUserAccessRepository; - - @MockBean - private UserInputAssertion assertion; - - private SecHubConfiguration configuration; - private ScheduleSecHubJob job; - - private UUID jobUUID; - - private String project; - - private String projectUUID = "projectId1"; - - @BeforeEach - public void beforeEach() { - jobUUID = UUID.randomUUID(); - job = mock(ScheduleSecHubJob.class); - configuration = mock(SecHubConfiguration.class); - project = "projectId"; - - when(job.getProjectId()).thenReturn(project); - - when(job.getUUID()).thenReturn(jobUUID); - when(job.getProjectId()).thenReturn(projectUUID); - when(jobFactory.createJob(eq(configuration))).thenReturn(job); - } - - @Test - public void get_a_job_status_from_an_unexisting_project_throws_NOT_FOUND_exception() { - /* prepare */ - UUID jobUUID = UUID.randomUUID(); - when(jobRepository.findById(jobUUID)).thenReturn(Optional.of(mock(ScheduleSecHubJob.class)));// should not be necessary, but to - - /* execute + test */ - // prevent dependency to call - // hierachy... we simulate job can be - // found - Assertions.assertThrows(NotFoundException.class, () -> { - serviceToTest.getJobStatus("a-project-not-existing", jobUUID); - }); - } - - @Test - public void get_a_job_status_from_an_exsting_project_but_no_job_throws_NOT_FOUND_exception() { - /* prepare */ - UUID jobUUID = UUID.randomUUID(); - when(jobRepository.findById(jobUUID)).thenReturn(Optional.empty()); // not found... - - /* execute + test */ - Assertions.assertThrows(NotFoundException.class, () -> { - serviceToTest.getJobStatus(PROJECT_ID, jobUUID); - }); - } - -} diff --git a/sechub-server/src/test/java/com/mercedesbenz/sechub/domain/schedule/SchedulerSmokeSpringBootTest.java b/sechub-server/src/test/java/com/mercedesbenz/sechub/domain/schedule/SchedulerSmokeSpringBootTest.java deleted file mode 100644 index 3ca3bbf8b2..0000000000 --- a/sechub-server/src/test/java/com/mercedesbenz/sechub/domain/schedule/SchedulerSmokeSpringBootTest.java +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.domain.schedule; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.TestPropertySource; - -import com.mercedesbenz.sechub.sharedkernel.Profiles; -import com.mercedesbenz.sechub.sharedkernel.RoleConstants; - -/* - * Smoke tests which checks that sechub-server spring boot container can be started and - * some defaults are as expected - * - * @author Albert Tregnaghi - * - */ -@SpringBootTest -@TestPropertySource(locations = "classpath:application-test.yml") -@WithMockUser(roles = { RoleConstants.ROLE_USER }) -@ActiveProfiles(Profiles.TEST) -public class SchedulerSmokeSpringBootTest { - - @Autowired - private SchedulerRestController schedulerRestController; - - @Autowired - SchedulerSourcecodeUploadConfiguration sourcecodeUploadConfiguration; - - @Autowired - SchedulerBinariesUploadConfiguration binariesUploadConfiguration; - - @Test - public void context_loads_and_some_defaults_are_as_expected() throws Exception { - // see https://spring.io/guides/gs/testing-web/ for details about testing with - // spring MVC test - assertThat(schedulerRestController).isNotNull(); // we test that we got the schedulerRestController. Means - the spring container - // context - // has been loaded successfully! - - /* check configuration defaults injected by container are as expected */ - assertTrue(sourcecodeUploadConfiguration.isZipValidationEnabled()); - assertTrue(sourcecodeUploadConfiguration.isChecksumValidationEnabled()); - - assertEquals(50 * 1024 * 1024, binariesUploadConfiguration.getMaxUploadSizeInBytes()); - } - -} diff --git a/sechub-server/src/test/java/com/mercedesbenz/sechub/server/SecHubMultiSpringBootTest.java b/sechub-server/src/test/java/com/mercedesbenz/sechub/server/SecHubMultiSpringBootTest.java new file mode 100644 index 0000000000..70d17f15c6 --- /dev/null +++ b/sechub-server/src/test/java/com/mercedesbenz/sechub/server/SecHubMultiSpringBootTest.java @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.server; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.URI; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +import com.mercedesbenz.sechub.domain.scan.NetworkTarget; +import com.mercedesbenz.sechub.domain.scan.NetworkTargetType; +import com.mercedesbenz.sechub.domain.scan.resolve.TargetResolverService; +import com.mercedesbenz.sechub.domain.schedule.SchedulerBinariesUploadConfiguration; +import com.mercedesbenz.sechub.domain.schedule.SchedulerCreateJobService; +import com.mercedesbenz.sechub.domain.schedule.SchedulerGetJobStatusService; +import com.mercedesbenz.sechub.domain.schedule.SchedulerRestController; +import com.mercedesbenz.sechub.domain.schedule.SchedulerSourcecodeUploadConfiguration; +import com.mercedesbenz.sechub.domain.schedule.access.ScheduleAccessRepository; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionResult; +import com.mercedesbenz.sechub.domain.schedule.encryption.ScheduleEncryptionService; +import com.mercedesbenz.sechub.domain.schedule.job.ScheduleSecHubJob; +import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobFactory; +import com.mercedesbenz.sechub.domain.schedule.job.SecHubJobRepository; +import com.mercedesbenz.sechub.sharedkernel.Profiles; +import com.mercedesbenz.sechub.sharedkernel.RoleConstants; +import com.mercedesbenz.sechub.sharedkernel.configuration.SecHubConfiguration; +import com.mercedesbenz.sechub.sharedkernel.error.NotFoundException; +import com.mercedesbenz.sechub.sharedkernel.validation.UserInputAssertion; + +/** + * Full spring boot tests which checks multiple parts. Why so much different + * tests inside one test class? Because here we launch a complete spring boot + * container with full setup (means all classes are inspected) which takes a + * long time to startup. + * + * The long startup time is for only for one spring boot test class setup, means + * adding additional tests via additional methods has nearly no time impact. + * Example: If we have 3 test classes with full spring boot setup and the full + * spring boot test setup for one class takes 7 seconds and we create 3 separate + * classes, we need 21 seconds for startup. If we combine it, we have need only + * 7 seconds. + * + * Because we want to have faster unit tests/builds we do this here this way. + * + * Combined parts inside this multi spring test are: + *
      + *
    • sechub-server spring boot container can be started
    • + *
    • some defaults are as expected
    • + *
    • target resolver works as defaults are as expected
      + *
      + * Inside application-test.properties we have defined strategies, which will + * treat "*.intranet.example.com/org" and "192.168.*.*" as INTRANET.
      + * This integration test checks if the configured values are really used
    • + *
    + * + * @author Albert Tregnaghi + * + */ +@SpringBootTest +@TestPropertySource(locations = "classpath:application-test.yml") +@WithMockUser(roles = { RoleConstants.ROLE_USER }) +@ActiveProfiles(Profiles.TEST) +public class SecHubMultiSpringBootTest { + + private static final String PROJECT_ID = "project1"; + private SecHubConfiguration configuration; + private ScheduleSecHubJob job; + + private UUID jobUUID; + + private String project; + + private String projectUUID = "projectId1"; + + @Autowired + private SchedulerRestController schedulerRestController; + + @Autowired + SchedulerSourcecodeUploadConfiguration sourcecodeUploadConfiguration; + + @Autowired + SchedulerBinariesUploadConfiguration binariesUploadConfiguration; + + @Autowired + ScheduleEncryptionService encryptionService; + + @Autowired + TargetResolverService targetResolverServiceToTest; + + @Autowired + private SchedulerGetJobStatusService getJobStatusServiceToTest; + + @Autowired + private SchedulerCreateJobService createJobServiceToTest; + + @MockBean + private SecHubJobFactory jobFactory; + + @MockBean + private SecHubJobRepository jobRepository; + + @MockBean + private ScheduleAccessRepository projectUserAccessRepository; + + @MockBean + private UserInputAssertion assertion; + + @BeforeEach + void beforeEach() { + jobUUID = UUID.randomUUID(); + job = mock(ScheduleSecHubJob.class); + configuration = mock(SecHubConfiguration.class); + project = "projectId"; + + when(job.getProjectId()).thenReturn(project); + when(jobRepository.save(job)).thenReturn(job); + + when(job.getUUID()).thenReturn(jobUUID); + when(job.getProjectId()).thenReturn(projectUUID); + when(jobFactory.createJob(eq(configuration))).thenReturn(job); + } + + @Test + void context_loads_and_some_defaults_are_as_expected() throws Exception { + // see https://spring.io/guides/gs/testing-web/ for details about testing with + // spring MVC test + assertThat(schedulerRestController).isNotNull(); // we test that we got the schedulerRestController. Means - the spring container + // context + // has been loaded successfully! + + /* check configuration defaults injected by container are as expected */ + assertTrue(sourcecodeUploadConfiguration.isZipValidationEnabled()); + assertTrue(sourcecodeUploadConfiguration.isChecksumValidationEnabled()); + + assertEquals(50 * 1024 * 1024, binariesUploadConfiguration.getMaxUploadSizeInBytes()); + + // test encryption service is initialized and works + String textToEncrypt = "i need encryption"; + ScheduleEncryptionResult encryptResult = encryptionService.encryptWithLatestCipher(textToEncrypt); + String decrypted = encryptionService.decryptToString(encryptResult.getEncryptedData(), encryptResult.getCipherPoolId(), + encryptResult.getInitialVector()); + assertEquals(textToEncrypt, decrypted); + + } + + @Test + void target_resolver_test_product_failure_demo_example_org__is_INTERNET() { + /* prepare */ + URI uri = URI.create("https://productfailure.demo.example.org"); + + /* execute */ + NetworkTarget found = targetResolverServiceToTest.resolveTarget(uri); + + /* test */ + assertEquals(new NetworkTarget(uri, NetworkTargetType.INTERNET), found); + + } + + @Test + void target_resolver_test_ip_172_217_22_99__IS_INTERNET() throws Exception { + /* prepare */ + InetAddress address = Inet4Address.getByName("172.217.22.99"); + + /* execute */ + NetworkTarget found = targetResolverServiceToTest.resolveTarget(address); + + /* test */ + assertEquals(new NetworkTarget(address, NetworkTargetType.INTERNET), found); + + } + + @Test + void target_resolver_test_something_intranet_example_org__is_INTRANET() { + /* prepare */ + URI uri = URI.create("https://something.intranet.example.org"); + + /* execute */ + NetworkTarget found = targetResolverServiceToTest.resolveTarget(uri); + + /* test */ + assertEquals(new NetworkTarget(uri, NetworkTargetType.INTRANET), found); + + } + + @Test + void target_resolver_test_ip_192_168_22_99__IS_INTRANET() throws Exception { + /* prepare */ + InetAddress address = Inet4Address.getByName("192.168.22.99"); + + /* execute */ + NetworkTarget found = targetResolverServiceToTest.resolveTarget(address); + + /* test */ + assertEquals(new NetworkTarget(address, NetworkTargetType.INTRANET), found); + + } + + @Test + void target_resolver_test_uri_hostname_startswith_192_IS_INTRANET() { + /* prepare */ + URI uri = URI.create("https://192.168.22.99:7777"); + + /* execute */ + NetworkTarget found = targetResolverServiceToTest.resolveTarget(uri); + + /* test */ + assertEquals(new NetworkTarget(uri, NetworkTargetType.INTRANET), found); + + } + + @Test + void get_a_job_status_from_an_unexisting_project_throws_NOT_FOUND_exception() { + /* prepare */ + UUID jobUUID = UUID.randomUUID(); + when(jobRepository.findById(jobUUID)).thenReturn(Optional.of(mock(ScheduleSecHubJob.class)));// should not be necessary, but to + + /* execute + test */ + // prevent dependency to call + // hierachy... we simulate job can be + // found + Assertions.assertThrows(NotFoundException.class, () -> { + getJobStatusServiceToTest.getJobStatus("a-project-not-existing", jobUUID); + }); + } + + @Test + void get_a_job_status_from_an_exsting_project_but_no_job_throws_NOT_FOUND_exception() { + /* prepare */ + UUID jobUUID = UUID.randomUUID(); + when(jobRepository.findById(jobUUID)).thenReturn(Optional.empty()); // not found... + + /* execute + test */ + Assertions.assertThrows(NotFoundException.class, () -> { + getJobStatusServiceToTest.getJobStatus(PROJECT_ID, jobUUID); + }); + } + + @Test + void create_job_scheduling_a_new_job_to_an_unexisting_project_throws_NOT_FOUND_exception() { + /* execute + test */ + Assertions.assertThrows(NotFoundException.class, () -> { + createJobServiceToTest.createJob("a-project-not-existing", configuration); + }); + } + + @Test + void create_job_no_access_entry__scheduling_a_configuration__will_throw_not_found_exception() { + /* execute + test */ + Assertions.assertThrows(NotFoundException.class, () -> { + createJobServiceToTest.createJob(PROJECT_ID, configuration); + }); + } +} diff --git a/sechub-shared-kernel/build.gradle b/sechub-shared-kernel/build.gradle index 32ac0b50f1..8d21e55f77 100644 --- a/sechub-shared-kernel/build.gradle +++ b/sechub-shared-kernel/build.gradle @@ -18,6 +18,7 @@ dependencies { implementation project(':sechub-storage-sharedvolume-spring') api project(':sechub-storage-s3-aws') implementation project(':sechub-adapter') + implementation project(':sechub-commons-encryption') implementation library.apache_commons_validator implementation library.logstashLogbackEncoder diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/DefaultEncryptionEnvironmentEntryProvider.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/DefaultEncryptionEnvironmentEntryProvider.java new file mode 100644 index 0000000000..238474c069 --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/DefaultEncryptionEnvironmentEntryProvider.java @@ -0,0 +1,17 @@ +package com.mercedesbenz.sechub.sharedkernel.encryption; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import com.mercedesbenz.sechub.sharedkernel.Profiles; + +@Component +@Profile("!" + Profiles.INTEGRATIONTEST) +public class DefaultEncryptionEnvironmentEntryProvider implements EncryptionEnvironmentEntryProvider { + + @Override + public String getBase64EncodedEnvironmentEntry(String environmentVariableName) { + return System.getenv(environmentVariableName); + } + +} diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/EncryptionEnvironmentEntryProvider.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/EncryptionEnvironmentEntryProvider.java new file mode 100644 index 0000000000..3235704671 --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/EncryptionEnvironmentEntryProvider.java @@ -0,0 +1,14 @@ +package com.mercedesbenz.sechub.sharedkernel.encryption; + +public interface EncryptionEnvironmentEntryProvider { + + /** + * Resolves value of an environment variable, it is assumed that the value is + * base 64 encoded + * + * @param environmentVariableName + * @return environment value + */ + String getBase64EncodedEnvironmentEntry(String environmentVariableName); + +} diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/IntegrationTestEncryptionEnvironmentEntryProvider.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/IntegrationTestEncryptionEnvironmentEntryProvider.java new file mode 100644 index 0000000000..d9d64d805d --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/IntegrationTestEncryptionEnvironmentEntryProvider.java @@ -0,0 +1,37 @@ +package com.mercedesbenz.sechub.sharedkernel.encryption; + +import java.nio.charset.Charset; +import java.util.Base64; +import java.util.Map; +import java.util.TreeMap; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import com.mercedesbenz.sechub.sharedkernel.Profiles; + +@Component +@Profile(Profiles.INTEGRATIONTEST) +public class IntegrationTestEncryptionEnvironmentEntryProvider implements EncryptionEnvironmentEntryProvider { + + private Map encryptionEntryMap = new TreeMap<>(); + + public IntegrationTestEncryptionEnvironmentEntryProvider() { + addPasswordAsBase64ForEnvironmentVariable("INTEGRATION_TEST_SECRET_1_AES_256", "123456789012345678901234567890AX"); + } + + private void addPasswordAsBase64ForEnvironmentVariable(String environmentVariableName, String plainTextPassword) { + String base64Value = Base64.getEncoder().encodeToString(plainTextPassword.getBytes(Charset.forName("UTF-8"))); + encryptionEntryMap.put(environmentVariableName, base64Value); + } + + @Override + public String getBase64EncodedEnvironmentEntry(String environmentVariableName) { + String result = encryptionEntryMap.get(environmentVariableName); + if (result == null) { + throw new IllegalStateException("Integration test setup has no entry for variable:" + environmentVariableName); + } + return result; + } + +} diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubCipherAlgorithm.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubCipherAlgorithm.java new file mode 100644 index 0000000000..2c5dd479b8 --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubCipherAlgorithm.java @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.sharedkernel.encryption; + +import com.mercedesbenz.sechub.commons.core.MustBeKeptStable; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherType; + +@MustBeKeptStable("used in database as values and also inside event communication/domain messages") +public enum SecHubCipherAlgorithm { + + NONE(PersistentCipherType.NONE), + + AES_GCM_SIV_128(PersistentCipherType.AES_GCM_SIV_128), + + AES_GCM_SIV_256(PersistentCipherType.AES_GCM_SIV_256),; + + private PersistentCipherType type; + + private SecHubCipherAlgorithm(PersistentCipherType type) { + this.type = type; + } + + public PersistentCipherType getType() { + return type; + } + +} diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubCipherPasswordSourceType.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubCipherPasswordSourceType.java new file mode 100644 index 0000000000..b94f3afbcb --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubCipherPasswordSourceType.java @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.sharedkernel.encryption; + +import com.mercedesbenz.sechub.commons.core.MustBeKeptStable; + +@MustBeKeptStable("used in database as values and also inside event communication/domain messages") +public enum SecHubCipherPasswordSourceType { + + /** + * No password + */ + NONE, + + /** + * Password comes from an environment variable, the name of the variable has to + * be defined inside password source date. + * + * Attention: The content of the variable may not be empty and MUST be + * base64 encoded! + */ + ENVIRONMENT_VARIABLE +} diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubDomainEncryptionData.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubDomainEncryptionData.java new file mode 100644 index 0000000000..dd2ad7e17b --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubDomainEncryptionData.java @@ -0,0 +1,77 @@ +package com.mercedesbenz.sechub.sharedkernel.encryption; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.TreeMap; + +//SPDX-License-Identifier: MIT +public class SecHubDomainEncryptionData { + + public static final String PROPERTY_ID = "id"; + public static final String PROPERTY_ALGORITHM = "algorithm"; + public static final String PROPERTY_PASSWORDSOURCE = "passwordSource"; + public static final String PROPERTY_CREATED = "created"; + public static final String PROPERTY_CREATED_FROM = "createdFrom"; + public static final String PROPERTY_USAGE = "usage"; + + private String id; + + private SecHubCipherAlgorithm algorithm; + + private SecHubPasswordSource passwordSource = new SecHubPasswordSource(); + + private Map usage = new TreeMap<>(); + + private String createdFrom; + + private LocalDateTime created; + + public String getId() { + return id; + } + + public void setId(String poolId) { + this.id = poolId; + } + + public SecHubCipherAlgorithm getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(SecHubCipherAlgorithm algorithm) { + this.algorithm = algorithm; + } + + public SecHubPasswordSource getPasswordSource() { + return passwordSource; + } + + public void setPasswordSource(SecHubPasswordSource passwordSource) { + this.passwordSource = passwordSource; + } + + public Map getUsage() { + return usage; + } + + public void setUsage(Map usage) { + this.usage = usage; + } + + public String getCreatedFrom() { + return createdFrom; + } + + public void setCreatedFrom(String createdFrom) { + this.createdFrom = createdFrom; + } + + public LocalDateTime getCreated() { + return created; + } + + public void setCreated(LocalDateTime created) { + this.created = created; + } + +} diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubDomainEncryptionStatus.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubDomainEncryptionStatus.java new file mode 100644 index 0000000000..bf72ea737d --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubDomainEncryptionStatus.java @@ -0,0 +1,45 @@ +package com.mercedesbenz.sechub.sharedkernel.encryption; + +import java.util.ArrayList; +import java.util.List; + +import com.mercedesbenz.sechub.commons.model.JSONable; + +public class SecHubDomainEncryptionStatus implements JSONable { + + public static final String PROPERTY_DATA = "data"; + public static final String PROPERTY_NAME = "name"; + + private static final SecHubDomainEncryptionStatus CONVERTER = new SecHubDomainEncryptionStatus(); + private String name; + + private List data = new ArrayList<>(); + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + /** + * Returns the list of encryption data entries. Each entry represents one + * encryption pool entry! + * + * @return list of encryption data entries + */ + public List getData() { + return data; + } + + public static SecHubDomainEncryptionStatus fromString(String json) { + return CONVERTER.fromJSON(json); + } + + @Override + public Class getJSONTargetClass() { + return SecHubDomainEncryptionStatus.class; + } + +} diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubEncryptionData.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubEncryptionData.java new file mode 100644 index 0000000000..c71ef49343 --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubEncryptionData.java @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.sharedkernel.encryption; + +import com.mercedesbenz.sechub.commons.model.JSONable; + +public class SecHubEncryptionData implements JSONable { + + private static final SecHubEncryptionData CONVERTER = new SecHubEncryptionData(); + + public static final String PROPERTY_ALGORITHM = "algorithm"; + public static final String PROPERTY_PASSWORD_SOURCETYPE = "passwordSourceType"; + public static final String PROPERTY_PASSWORD_SOURCEDATA = "passwordSourceData"; + + private SecHubCipherAlgorithm algorithm; + + private SecHubCipherPasswordSourceType passwordSourceType; + + private String passwordSourceData; + + public SecHubCipherAlgorithm getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(SecHubCipherAlgorithm algorithm) { + this.algorithm = algorithm; + } + + public SecHubCipherPasswordSourceType getPasswordSourceType() { + return passwordSourceType; + } + + public void setPasswordSourceType(SecHubCipherPasswordSourceType passwordSourceType) { + this.passwordSourceType = passwordSourceType; + } + + public String getPasswordSourceData() { + return passwordSourceData; + } + + public void setPasswordSourceData(String passwordSourceData) { + this.passwordSourceData = passwordSourceData; + } + + @Override + public Class getJSONTargetClass() { + return SecHubEncryptionData.class; + } + + public static SecHubEncryptionData fromString(String dataAsString) { + return CONVERTER.fromJSON(dataAsString); + } + +} diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubEncryptionDataValidator.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubEncryptionDataValidator.java new file mode 100644 index 0000000000..a47fc533c3 --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubEncryptionDataValidator.java @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.sharedkernel.encryption; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; + +/** + * A very simple validation - just checks that necessary fields are filled. For + * NONE cipher algorithm we need no password data. For all others the password + * source type and password source data must be defined to have no validation + * errors. + * + * @author Albert Tregnaghi + * + */ +@Component +public class SecHubEncryptionDataValidator implements Validator { + + private static final Logger LOG = LoggerFactory.getLogger(SecHubEncryptionDataValidator.class); + + @Override + public boolean supports(Class clazz) { + return SecHubEncryptionData.class.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + if (target instanceof SecHubEncryptionData data) { + internalValidate(data, errors); + } else { + LOG.error("Validation cannot handle object type: {}", target == null ? null : target.getClass()); + } + } + + private void internalValidate(SecHubEncryptionData data, Errors errors) { + + ValidationUtils.rejectIfEmptyOrWhitespace(errors, SecHubEncryptionData.PROPERTY_ALGORITHM, "field.required"); + + SecHubCipherAlgorithm algorithm = data.getAlgorithm(); + handleAlgorithmPasswordData(errors, algorithm); + } + + private void handleAlgorithmPasswordData(Errors errors, SecHubCipherAlgorithm algorithm) { + if (algorithm == null) { + return; + } + switch (algorithm) { + case NONE: + // no password data necessary here + break; + default: + ValidationUtils.rejectIfEmptyOrWhitespace(errors, SecHubEncryptionData.PROPERTY_PASSWORD_SOURCETYPE, "field.required"); + ValidationUtils.rejectIfEmptyOrWhitespace(errors, SecHubEncryptionData.PROPERTY_PASSWORD_SOURCEDATA, "field.required"); + break; + + } + } +} diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubEncryptionStatus.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubEncryptionStatus.java new file mode 100644 index 0000000000..494863ff59 --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubEncryptionStatus.java @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.sharedkernel.encryption; + +import java.util.ArrayList; +import java.util.List; + +import com.mercedesbenz.sechub.commons.model.JSONable; + +public class SecHubEncryptionStatus implements JSONable { + + public static final String PROPERTY_TYPE = "type"; + public static final String PROPERTY_DOMAINS = "domains"; + + private static SecHubEncryptionStatus CONVERTER = new SecHubEncryptionStatus(); + + private String type = "encryptionStatus"; + + public String getType() { + return type; + } + + private List domains = new ArrayList<>(); + + public List getDomains() { + return domains; + } + + @Override + public Class getJSONTargetClass() { + return SecHubEncryptionStatus.class; + } + + public static SecHubEncryptionStatus fromString(String statusAsString) { + return CONVERTER.fromJSON(statusAsString); + } +} diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubPasswordSource.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubPasswordSource.java new file mode 100644 index 0000000000..16445af499 --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubPasswordSource.java @@ -0,0 +1,25 @@ +package com.mercedesbenz.sechub.sharedkernel.encryption; + +public class SecHubPasswordSource { + public static final String PROPERTY_TYPE = "type"; + public static final String PROPERTY_DATA = "data"; + + private SecHubCipherPasswordSourceType type; + private String data; + + public void setData(String data) { + this.data = data; + } + + public void setType(SecHubCipherPasswordSourceType type) { + this.type = type; + } + + public String getData() { + return data; + } + + public SecHubCipherPasswordSourceType getType() { + return type; + } +} \ No newline at end of file diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubSecretKeyProviderFactory.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubSecretKeyProviderFactory.java new file mode 100644 index 0000000000..18016b2a93 --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubSecretKeyProviderFactory.java @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.sharedkernel.encryption; + +import java.util.Base64; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.mercedesbenz.sechub.commons.encryption.DefaultSecretKeyProvider; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherType; +import com.mercedesbenz.sechub.commons.encryption.SecretKeyProvider; + +@Component +public class SecHubSecretKeyProviderFactory { + + @Autowired + EncryptionEnvironmentEntryProvider encryptionEnvironmentEntryProvider; + + public SecretKeyProvider createSecretKeyProvider(PersistentCipherType cipherType, SecHubCipherPasswordSourceType passwordSourceType, + String cipherPasswordSourceData) { + if (PersistentCipherType.NONE.equals(cipherType)) { + /* none has never a secret key provider - there is just no secret */ + return null; + } + try { + return handle(cipherType, passwordSourceType, cipherPasswordSourceData); + + } catch (Exception e) { + throw new SecHubSecretKeyProviderFactoryException( + "Was not able to create key provider for cipherType: '%s', passwordSourceType: '%s', cipherPasswordSourceData: '%s'".formatted(cipherType, + passwordSourceType, cipherPasswordSourceData), + e); + } + + } + + private SecretKeyProvider handle(PersistentCipherType cipherType, SecHubCipherPasswordSourceType passwordSourceType, String cipherPasswordSourceData) { + switch (passwordSourceType) { + + case ENVIRONMENT_VARIABLE: + String environmentVariableName = cipherPasswordSourceData; + String environmentEntry = encryptionEnvironmentEntryProvider.getBase64EncodedEnvironmentEntry(environmentVariableName); + if (environmentEntry == null || environmentEntry.isBlank()) { + throw new IllegalArgumentException("The environment variable: " + environmentVariableName + " has no value!"); + } + byte[] base64decoded = Base64.getDecoder().decode(environmentEntry); + + return new DefaultSecretKeyProvider(base64decoded, cipherType); + + default: + throw new IllegalArgumentException("Password source type '%s' for cipher type: '%s' is not supported!".formatted(passwordSourceType, cipherType)); + } + } +} diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubSecretKeyProviderFactoryException.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubSecretKeyProviderFactoryException.java new file mode 100644 index 0000000000..1a9f8749c5 --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubSecretKeyProviderFactoryException.java @@ -0,0 +1,11 @@ +package com.mercedesbenz.sechub.sharedkernel.encryption; + +public class SecHubSecretKeyProviderFactoryException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public SecHubSecretKeyProviderFactoryException(String message, Throwable cause) { + super(message, cause); + } + +} \ No newline at end of file diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/jpa/TypedQuerySupport.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/jpa/TypedQuerySupport.java index 4a57f81c39..5f82c8b745 100644 --- a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/jpa/TypedQuerySupport.java +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/jpa/TypedQuerySupport.java @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.sharedkernel.jpa; +import java.util.List; import java.util.Optional; import jakarta.persistence.NoResultException; @@ -48,4 +49,9 @@ public T getSingleResultOrNull(Query query) { } throw new IllegalStateException("The given query returns not expected type:" + clazz + " but " + result.getClass()); } + + @SuppressWarnings("unchecked") + public List getList(Query query) { + return query.getResultList(); + } } diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/DomainMessageService.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/DomainMessageService.java index 5f156001fa..5d749b6a51 100644 --- a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/DomainMessageService.java +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/DomainMessageService.java @@ -45,7 +45,7 @@ public DomainMessageService(List injectedSynchronousHand Set messageIds = getSupportedMessageIdsFor(handler); for (MessageID messageId : messageIds) { synchronHandlers.put(messageId, handler); - LOG.debug("Registered synchron message handler:{} for message ID:{}", handler.getClass(), messageId); + LOG.trace("Registered synchron message handler:{} for message ID:{}", handler.getClass(), messageId); } } @@ -61,7 +61,7 @@ public DomainMessageService(List injectedSynchronousHand this.asynchronHandlers.put(messageId, foundAsynchronousHandlersForID); } foundAsynchronousHandlersForID.add(handler); - LOG.debug("Registered asynchronus message handler:{} for message ID:{}", handler.getClass(), messageId); + LOG.trace("Registered asynchronus message handler:{} for message ID:{}", handler.getClass(), messageId); } } } diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/JobMessage.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/JobMessage.java index eaafdf361b..092b7037a1 100644 --- a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/JobMessage.java +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/JobMessage.java @@ -32,8 +32,6 @@ public class JobMessage implements JSONable { private String info; - private String configuration; - @JsonFormat(pattern = ("yyyy/MM/dd HH:mm:ss")) @JsonSerialize(using = LocalDateTimeSerializer.class) @JsonDeserialize(using = LocalDateTimeDeserializer.class) @@ -59,14 +57,6 @@ public void setInfo(String info) { this.info = info; } - public String getConfiguration() { - return configuration; - } - - public void setConfiguration(String configuration) { - this.configuration = configuration; - } - public void setSince(LocalDateTime date) { this.since = date; } diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/MessageDataKeys.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/MessageDataKeys.java index 9854d242ae..a4203dca50 100644 --- a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/MessageDataKeys.java +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/MessageDataKeys.java @@ -6,6 +6,8 @@ import com.mercedesbenz.sechub.commons.model.SecHubMessagesList; import com.mercedesbenz.sechub.sharedkernel.configuration.SecHubConfiguration; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubDomainEncryptionStatus; +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionData; /** * @@ -32,6 +34,8 @@ public class MessageDataKeys { private static final AdministrationConfigMessageDataProvider ADMIN_CONFIG_MESSAGE_DATA_PROVIDER = new AdministrationConfigMessageDataProvider(); private static final UserMessageDataProvider USER_MESSAGE_DATA_PROVIDER = new UserMessageDataProvider(); private static final LocalDateTimeMessageDataProvider LOCAL_DATE_TIME_MESSAGE_DATA_PROVIDER = new LocalDateTimeMessageDataProvider(); + private static final SecHubEncryptionMessageDataProvider SECHUB_ENCRYPTION_MESSAGE_DATA_PROVIDER = new SecHubEncryptionMessageDataProvider(); + private static final SecHubEncryptionStatusMessageDataProvider SECHUB_DOMAIN_ENCRYPTION_STATUS_MESSAGE_DATA_PROVIDER = new SecHubEncryptionStatusMessageDataProvider(); /* * Only reason why this is not an emum is that we want to have generic type @@ -51,13 +55,21 @@ private MessageDataKeys() { public static final MessageDataKey SECHUB_JOB_UUID = createKey("sechub.job.uuid", UID_MESSAGE_DATA_PROVIDER); public static final MessageDataKey SECHUB_EXECUTION_UUID = createKey("sechub.execution.uuid", UID_MESSAGE_DATA_PROVIDER); + + public static final MessageDataKey SECHUB_ENCRYPT_ROTATION_DATA = createKey("sechub.encrypt.rotation.data", + SECHUB_ENCRYPTION_MESSAGE_DATA_PROVIDER); + + public static final MessageDataKey SECHUB_DOMAIN_ENCRYPTION_STATUS = createKey("sechub.domain.encryption.status", + SECHUB_DOMAIN_ENCRYPTION_STATUS_MESSAGE_DATA_PROVIDER); + /** * Use this generic key when you just want to define timestamp without using a * dedicated model where it is already contained. */ public static final MessageDataKey LOCAL_DATE_TIME_SINCE = createKey("localdatetime.since", LOCAL_DATE_TIME_MESSAGE_DATA_PROVIDER); - public static final MessageDataKey SECHUB_CONFIG = createKey("sechub.config", SECHUB_CONFIGURATION_MESSAGE_DATA_PROVIDER); + public static final MessageDataKey SECHUB_UNENCRYPTED_CONFIG = createKey("sechub.unencryptedconfig", + SECHUB_CONFIGURATION_MESSAGE_DATA_PROVIDER); public static final MessageDataKey ENVIRONMENT_CLUSTER_MEMBER_STATUS = createKey("environment.cluster.member.status", CLUSTER_MEMBER_MESSAGE_DATA_PROVIDER); diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/MessageID.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/MessageID.java index f0f961d451..63134169f6 100644 --- a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/MessageID.java +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/MessageID.java @@ -8,21 +8,21 @@ public enum MessageID { /** - * Is send/received synchron + * Is send/received synchronous */ START_SCAN( /* @formatter:off */ MessageDataKeys.SECHUB_JOB_UUID, MessageDataKeys.EXECUTED_BY, - MessageDataKeys.SECHUB_CONFIG), + MessageDataKeys.SECHUB_UNENCRYPTED_CONFIG), /* @formatter:on */ /** - * Is send/received synchron + * Is send/received synchronous */ SCAN_DONE, /** - * Is send/received synchron + * Is send/received synchronous */ SCAN_FAILED, @@ -192,6 +192,28 @@ public enum MessageID { BINARY_UPLOAD_DONE(MessageDataKeys.SECHUB_JOB_UUID, MessageDataKeys.UPLOAD_STORAGE_DATA), + /** + * This message will be send when an administrator defines new encryption data + */ + START_ENCRYPTION_ROTATION(MessageDataKeys.SECHUB_ENCRYPT_ROTATION_DATA, MessageDataKeys.EXECUTED_BY), + + /** + * Event is sent when a scheduler instance creates or recreates its encryption + * pool + */ + SCHEDULE_ENCRYPTION_POOL_INITIALIZED, + + /** + * Is send/received synchronous + */ + GET_ENCRYPTION_STATUS_SCHEDULE_DOMAIN, + + /** + * Contains result for encryption status request by + * {@link #GET_ENCRYPTION_STATUS_SCHEDULE_DOMAIN} + */ + RESULT_ENCRYPTION_STATUS_SCHEDULE_DOMAIN(MessageDataKeys.SECHUB_DOMAIN_ENCRYPTION_STATUS), + ; private Set> unmodifiableKeys; diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/SecHubEncryptionMessageDataProvider.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/SecHubEncryptionMessageDataProvider.java new file mode 100644 index 0000000000..5b5609e22a --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/SecHubEncryptionMessageDataProvider.java @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.sharedkernel.messaging; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubEncryptionData; + +public class SecHubEncryptionMessageDataProvider implements MessageDataProvider { + + private static final Logger LOG = LoggerFactory.getLogger(SecHubEncryptionMessageDataProvider.class); + + @Override + public SecHubEncryptionData get(String encryptionDataAsJson) { + if (encryptionDataAsJson == null) { + return null; + } + try { + return SecHubEncryptionData.fromString(encryptionDataAsJson); + } catch (IllegalArgumentException e) { + LOG.error("Cannot transform json to sechub encryption data", e); + return null; + } + } + + @Override + public String getString(SecHubEncryptionData data) { + if (data == null) { + return null; + } + return data.toJSON(); + } + +} diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/SecHubEncryptionStatusMessageDataProvider.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/SecHubEncryptionStatusMessageDataProvider.java new file mode 100644 index 0000000000..74011b74f8 --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/messaging/SecHubEncryptionStatusMessageDataProvider.java @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.sharedkernel.messaging; + +import com.mercedesbenz.sechub.sharedkernel.encryption.SecHubDomainEncryptionStatus; + +public class SecHubEncryptionStatusMessageDataProvider implements MessageDataProvider { + + @Override + public SecHubDomainEncryptionStatus get(String json) { + if (json == null) { + return null; + } + return SecHubDomainEncryptionStatus.fromString(json); + } + + @Override + public String getString(SecHubDomainEncryptionStatus data) { + if (data == null) { + return null; + } + return data.toJSON(); + } + +} diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/UseCaseGroup.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/UseCaseGroup.java index b3c6e66241..b7fed0dc4d 100644 --- a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/UseCaseGroup.java +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/UseCaseGroup.java @@ -23,6 +23,8 @@ public enum UseCaseGroup { CONFIGURATION("Configuration", "Usecases for configuration parts"), + ENCRYPTION("Encryption", "Usecases for encryption parts"), + OTHER("Other", "All other use cases"), ; diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/UseCaseIdentifier.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/UseCaseIdentifier.java index cb6218810f..aad7de68aa 100644 --- a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/UseCaseIdentifier.java +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/UseCaseIdentifier.java @@ -163,6 +163,17 @@ public enum UseCaseIdentifier { UC_ADMIN_SHOWS_USER_DETAILS_FOR_EMAIL_ADDRESS(72), + /* encryption */ + UC_ADMIN_STARTS_ENCRYPTION_ROTATION(73), + + UC_SCHEDULE_ENCRYPTION_POOL_REFRESH(74, false), + + UC_SCHEDULE_ROTATE_DATA_ENCRYPTION(75, false), + + UC_ADMIN_FETCHES_ENCRYPTION_STATUS(76), + + UC_ENCRYPTION_CLEANUP(77, false), // encryption cleanup is done by auto cleanup mechanism, no REST call necessary + ; /* +-----------------------------------------------------------------------+ */ diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/encryption/UseCaseAdminFetchesEncryptionStatus.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/encryption/UseCaseAdminFetchesEncryptionStatus.java new file mode 100644 index 0000000000..a5492edf2c --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/encryption/UseCaseAdminFetchesEncryptionStatus.java @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.sharedkernel.usecases.encryption; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseDefinition; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseGroup; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseIdentifier; + +/* @formatter:off */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@UseCaseDefinition( + id=UseCaseIdentifier.UC_ADMIN_FETCHES_ENCRYPTION_STATUS, + group=UseCaseGroup.ENCRYPTION, + apiName="adminFetchesEncryptionStatus", + title="Admin fetches encryption status", + description="An administrator fetches encryption status from all domains where encryption is used.") +public @interface UseCaseAdminFetchesEncryptionStatus{ + + Step value(); +} +/* @formatter:on */ diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/encryption/UseCaseAdminStartsEncryptionRotation.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/encryption/UseCaseAdminStartsEncryptionRotation.java new file mode 100644 index 0000000000..2edf05ebb1 --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/encryption/UseCaseAdminStartsEncryptionRotation.java @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.sharedkernel.usecases.encryption; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseDefinition; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseGroup; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseIdentifier; + +/* @formatter:off */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@UseCaseDefinition( + id=UseCaseIdentifier.UC_ADMIN_STARTS_ENCRYPTION_ROTATION, + group=UseCaseGroup.ENCRYPTION, + apiName="adminStartsEncryptionRotation", + title="Admin starts encryption rotation", + description="An administrator starts encryption rotation.") +public @interface UseCaseAdminStartsEncryptionRotation{ + + Step value(); +} +/* @formatter:on */ diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/encryption/UseCaseEncryptionCleanup.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/encryption/UseCaseEncryptionCleanup.java new file mode 100644 index 0000000000..654ad9f012 --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/encryption/UseCaseEncryptionCleanup.java @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.sharedkernel.usecases.encryption; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseDefinition; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseGroup; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseIdentifier; + +/* @formatter:off */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@UseCaseDefinition( + id=UseCaseIdentifier.UC_ENCRYPTION_CLEANUP, + group=UseCaseGroup.ENCRYPTION, + apiName="systemStartsEncryptionCleanup", + title="SecHub does cleanup encryption", + description=""" + Secub does an ecnryption cleanup. + + Inside relevant domains the encryption situation will be checked and + old encryption setup, which is no longer necessary, will be dropped. + + For example: When encryption was done with formerly via ENV variable + `SECRET_1_AES_256` and the new one setup is using `SECRET_2_AES_256` and + all jobs have been migrated to the new encryption, the cipher setup + using `SECRET_1_AES_256` will become obsolete and will be automatically + removed. After the remove is done, there is no longer a need to + start the server with `SECRET_1_AES_256`, but only with `SECRET_2_AES_256` ... + + """) +public @interface UseCaseEncryptionCleanup{ + + Step value(); +} +/* @formatter:on */ diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/encryption/UseCaseScheduleEncryptionPoolRefresh.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/encryption/UseCaseScheduleEncryptionPoolRefresh.java new file mode 100644 index 0000000000..e0b87e0724 --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/encryption/UseCaseScheduleEncryptionPoolRefresh.java @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.sharedkernel.usecases.encryption; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseDefinition; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseGroup; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseIdentifier; + +/* @formatter:off */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@UseCaseDefinition( + id=UseCaseIdentifier.UC_SCHEDULE_ENCRYPTION_POOL_REFRESH, + group=UseCaseGroup.ENCRYPTION, + apiName="serverStartsEncryptionPoolRefresh", + title="Scheduler encryption pool refresh", + description="The scheduler refreshes its encryption pool data to handle new setup") +public @interface UseCaseScheduleEncryptionPoolRefresh{ + + Step value(); +} +/* @formatter:on */ diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/encryption/UseCaseScheduleRotateDataEncryption.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/encryption/UseCaseScheduleRotateDataEncryption.java new file mode 100644 index 0000000000..59dc8c319d --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/encryption/UseCaseScheduleRotateDataEncryption.java @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.sharedkernel.usecases.encryption; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseDefinition; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseGroup; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseIdentifier; + +/* @formatter:off */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@UseCaseDefinition( + id=UseCaseIdentifier.UC_SCHEDULE_ROTATE_DATA_ENCRYPTION, + group=UseCaseGroup.ENCRYPTION, + apiName="serverRotatesScheduleDataEncryption", + title="Scheduler rotates data encryption", + description="The scheduler checks for old encrypted data and will encrypt with latest cipher") +public @interface UseCaseScheduleRotateDataEncryption{ + + Step value(); +} +/* @formatter:on */ diff --git a/sechub-shared-kernel/src/test/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubEncryptionStatusTest.java b/sechub-shared-kernel/src/test/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubEncryptionStatusTest.java new file mode 100644 index 0000000000..c98cf9d3d1 --- /dev/null +++ b/sechub-shared-kernel/src/test/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubEncryptionStatusTest.java @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.sharedkernel.encryption; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class SecHubEncryptionStatusTest { + + @Test + void json_transformation_works_as_expected() { + /* prepare */ + SecHubEncryptionStatus status = new SecHubEncryptionStatus(); + + SecHubDomainEncryptionData scheduleEntry0 = new SecHubDomainEncryptionData(); + scheduleEntry0.setAlgorithm(SecHubCipherAlgorithm.NONE); + scheduleEntry0.setId("0"); + scheduleEntry0.getPasswordSource().setData(null); + scheduleEntry0.getPasswordSource().setType(SecHubCipherPasswordSourceType.NONE); + + scheduleEntry0.getUsage().put("job.state.canceled", 0L); + scheduleEntry0.getUsage().put("job.state.ended", 0L); + scheduleEntry0.getUsage().put("job.state.initialized", 2L); + + SecHubDomainEncryptionData scheduleEntry1 = new SecHubDomainEncryptionData(); + scheduleEntry1.setAlgorithm(SecHubCipherAlgorithm.AES_GCM_SIV_128); + scheduleEntry1.setId("1"); + scheduleEntry1.getPasswordSource().setData("SECRET_1"); + scheduleEntry1.getPasswordSource().setType(SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE); + + scheduleEntry1.getUsage().put("job.state.canceled", 100L); + scheduleEntry1.getUsage().put("job.state.ended", 200L); + scheduleEntry1.getUsage().put("job.state.initialized", 1L); + + SecHubDomainEncryptionStatus schedulerStatus = new SecHubDomainEncryptionStatus(); + schedulerStatus.setName("schedule"); + schedulerStatus.getData().add(scheduleEntry0); + schedulerStatus.getData().add(scheduleEntry1); + + status.getDomains().add(schedulerStatus); + + /* execute */ + String json = status.toJSON(); + + /* test */ + assertNotNull(json); + + /* execute 2 */ + SecHubEncryptionStatus status2 = SecHubEncryptionStatus.fromString(json); + + /* test 2 */ + + List domains2 = status2.getDomains(); + assertThat(domains2).hasSize(1); + + SecHubDomainEncryptionStatus schedulerStatus2 = domains2.iterator().next(); + assertThat(schedulerStatus2.getName()).isEqualTo("schedule"); + + List data2 = schedulerStatus2.getData(); + assertThat(data2).hasSize(2); + + Iterator iterator = data2.iterator(); + SecHubDomainEncryptionData schedule2Entry0 = iterator.next(); + SecHubDomainEncryptionData schedule2Entry1 = iterator.next(); + /* @formatter:off */ + assertThat(schedule2Entry0.getAlgorithm()).isEqualTo(SecHubCipherAlgorithm.NONE); + assertThat(schedule2Entry0.getId()).isEqualTo("0"); + assertThat(schedule2Entry0.getPasswordSource().getType()).isEqualTo(SecHubCipherPasswordSourceType.NONE); + assertThat(schedule2Entry0.getPasswordSource().getData()).isNull(); + assertThat(schedule2Entry0.getUsage()).containsAllEntriesOf( + Map.of( + "job.state.canceled", 0L, + "job.state.ended", 0L, + "job.state.initialized", 2L + )); + + assertThat(schedule2Entry1.getAlgorithm()).isEqualTo(SecHubCipherAlgorithm.AES_GCM_SIV_128); + assertThat(schedule2Entry1.getId()).isEqualTo("1"); + assertThat(schedule2Entry1.getPasswordSource().getType()).isEqualTo(SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE); + assertThat(schedule2Entry1.getPasswordSource().getData()).isEqualTo("SECRET_1"); + assertThat(schedule2Entry1.getUsage()).containsAllEntriesOf( + Map.of( + "job.state.canceled", 100L, + "job.state.ended", 200L, + "job.state.initialized", 1L + )); + /* @formatter:on */ + + } + + @BeforeEach + public void beforeEach() throws Exception { + + } + +} diff --git a/sechub-shared-kernel/src/test/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubSecretKeyProviderFactoryTest.java b/sechub-shared-kernel/src/test/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubSecretKeyProviderFactoryTest.java new file mode 100644 index 0000000000..ceb51fca73 --- /dev/null +++ b/sechub-shared-kernel/src/test/java/com/mercedesbenz/sechub/sharedkernel/encryption/SecHubSecretKeyProviderFactoryTest.java @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.sharedkernel.encryption; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Base64; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EmptySource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherType; +import com.mercedesbenz.sechub.commons.encryption.SecretKeyProvider; + +class SecHubSecretKeyProviderFactoryTest { + + private static final Logger LOG = LoggerFactory.getLogger(SecHubSecretKeyProviderFactoryTest.class); + + private SecHubSecretKeyProviderFactory factoryToTest; + private EncryptionEnvironmentEntryProvider encryptionEnvironmentEntryProvider; + + @BeforeEach + void beforeEach() { + factoryToTest = new SecHubSecretKeyProviderFactory(); + encryptionEnvironmentEntryProvider = mock(EncryptionEnvironmentEntryProvider.class); + + factoryToTest.encryptionEnvironmentEntryProvider = encryptionEnvironmentEntryProvider; + + } + + @ParameterizedTest + @EnumSource(SecHubCipherPasswordSourceType.class) + void none_cipher_no_encryptionEnvironmentEntryProvider_used_no_matter_which_pwd_source_type_defined(SecHubCipherPasswordSourceType type) { + /* execute */ + SecretKeyProvider result = factoryToTest.createSecretKeyProvider(PersistentCipherType.NONE, type, "SECRET_1"); + + /* test */ + assertThat(result).isNull(); + + } + + @ParameterizedTest + @NullSource + @EmptySource + @ValueSource(strings = { "ZW52aXJvbm1lbnQtZW50cnktcGxhaW4tdGV4dA==" }) // valid base 64 + void cipher_aes_256_with_none_source_type_is_not_supported(String sourceData) { + /* @formatter:off */ + assertThatThrownBy(()->factoryToTest.createSecretKeyProvider(PersistentCipherType.AES_GCM_SIV_256, SecHubCipherPasswordSourceType.NONE, sourceData)). + isInstanceOf(SecHubSecretKeyProviderFactoryException.class). + hasMessage("Was not able to create key provider for cipherType: 'AES_GCM_SIV_256', passwordSourceType: 'NONE', cipherPasswordSourceData: '%s'", sourceData). + hasRootCauseExactlyInstanceOf(IllegalArgumentException.class). + hasRootCauseMessage("Password source type 'NONE' for cipher type: 'AES_GCM_SIV_256' is not supported!"); + /* @formatter:on */ + } + + @ParameterizedTest + @NullSource + @EmptySource + @ValueSource(strings = { "ZW52aXJvbm1lbnQtZW50cnktcGxhaW4tdGV4dA==" }) // valid base 64 + void cipher_aes_128_with_none_source_type_is_not_supported(String sourceData) { + /* @formatter:off */ + assertThatThrownBy(()->factoryToTest.createSecretKeyProvider(PersistentCipherType.AES_GCM_SIV_128, SecHubCipherPasswordSourceType.NONE, sourceData)). + isInstanceOf(SecHubSecretKeyProviderFactoryException.class). + hasMessage("Was not able to create key provider for cipherType: 'AES_GCM_SIV_128', passwordSourceType: 'NONE', cipherPasswordSourceData: '%s'", sourceData). + hasRootCauseExactlyInstanceOf(IllegalArgumentException.class). + hasRootCauseMessage("Password source type 'NONE' for cipher type: 'AES_GCM_SIV_128' is not supported!"); + /* @formatter:on */ + } + + @ParameterizedTest + @NullSource + @EmptySource + void when_environment_variable_not_defined_exception_is_thrown(String value) throws Exception { + + /* prepare */ + when(encryptionEnvironmentEntryProvider.getBase64EncodedEnvironmentEntry("SECRET_1")).thenReturn(value); + + /* execute + test */ + /* @formatter:off */ + assertThatThrownBy(() -> factoryToTest.createSecretKeyProvider(PersistentCipherType.AES_GCM_SIV_256, + SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE, "SECRET_1")). + isInstanceOf(SecHubSecretKeyProviderFactoryException.class). + hasMessage("Was not able to create key provider for cipherType: 'AES_GCM_SIV_256', passwordSourceType: 'ENVIRONMENT_VARIABLE', cipherPasswordSourceData: 'SECRET_1'"). + hasRootCauseExactlyInstanceOf(IllegalArgumentException.class). + hasRootCauseMessage("The environment variable: SECRET_1 has no value!"); + /* @formatter:on */ + + } + + @ParameterizedTest + @ValueSource(strings = { "a$lbert", "1:2", " ZW52aXJvbm1lbnQtZW50cnktcGxhaW4tdGV4dA==" }) + void when_environment_variable_has_not_base_64_encoded_value_exception_is_thrown(String value) throws Exception { + + /* prepare */ + when(encryptionEnvironmentEntryProvider.getBase64EncodedEnvironmentEntry("SECRET_2")).thenReturn(value); + + /* execute + test */ + /* @formatter:off */ + assertThatThrownBy(() -> factoryToTest.createSecretKeyProvider(PersistentCipherType.AES_GCM_SIV_128, SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE, "SECRET_2")). + isInstanceOf(SecHubSecretKeyProviderFactoryException.class). + hasMessage("Was not able to create key provider for cipherType: 'AES_GCM_SIV_128', passwordSourceType: 'ENVIRONMENT_VARIABLE', cipherPasswordSourceData: 'SECRET_2'"). + hasRootCauseExactlyInstanceOf(IllegalArgumentException.class); + /* @formatter:on */ + + } + + @ParameterizedTest + @ValueSource(strings = { "ZW52aXJvbm1lbnQtZW50cnktcGxhaW4tdGV4dA== ", "ZW52aXJvbm1lbnQtZW50cnktcGxhaW4tdGV4dA== ", + "ZW52aXJvbm1lbnQtZW50cnktcGxhaW4tdGV4dA==\t" }) + void when_environment_variable_base64_encoded_value_but_value_ends_with_whitspace_exception_is_thrown(String value) throws Exception { + + /* prepare */ + when(encryptionEnvironmentEntryProvider.getBase64EncodedEnvironmentEntry("SECRET_3")).thenReturn(value); + + /* execute + test */ + /* @formatter:off */ + assertThatThrownBy(() -> factoryToTest.createSecretKeyProvider(PersistentCipherType.AES_GCM_SIV_256, SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE, "SECRET_3")). + isInstanceOf(SecHubSecretKeyProviderFactoryException.class). + hasMessage("Was not able to create key provider for cipherType: 'AES_GCM_SIV_256', passwordSourceType: 'ENVIRONMENT_VARIABLE', cipherPasswordSourceData: 'SECRET_3'"). + hasRootCauseExactlyInstanceOf(IllegalArgumentException.class). + hasRootCauseMessage("Input byte array has incorrect ending byte at 40"); + /* @formatter:on */ + + } + + @Test + void when_environment_variable_is_base_64_provider_has_secret_key_with_base_64_decrypted_data_inside() throws Exception { + + /* prepare */ + String plainTextAsString = "environment-entry-plain-text2"; + byte[] plainTextAsBytes = plainTextAsString.getBytes(); + String base64String = Base64.getEncoder().encodeToString(plainTextAsBytes); + LOG.info("base64String:" + base64String); + when(encryptionEnvironmentEntryProvider.getBase64EncodedEnvironmentEntry("SECRET_1")).thenReturn(base64String); + + /* execute */ + SecretKeyProvider result = factoryToTest.createSecretKeyProvider(PersistentCipherType.AES_GCM_SIV_256, + SecHubCipherPasswordSourceType.ENVIRONMENT_VARIABLE, "SECRET_1"); + + /* test */ + assertThat(result).isNotNull(); + byte[] encoded = result.getSecretKey().getEncoded(); + assertThat(encoded).isEqualTo(plainTextAsBytes); + + } + +} diff --git a/sechub-test/build.gradle b/sechub-test/build.gradle index 752aaa488a..fc76570ab3 100644 --- a/sechub-test/build.gradle +++ b/sechub-test/build.gradle @@ -6,6 +6,7 @@ dependencies { testImplementation project(':sechub-server-core') testImplementation project(':sechub-pds-core') testImplementation project(':sechub-schedule') + testImplementation project(':sechub-commons-encryption') testImplementation project(':sechub-administration') testImplementation project(':sechub-adapter') testImplementation project(':sechub-testframework') diff --git a/sechub-test/src/test/java/com/mercedesbenz/sechub/test/encryption/EncryptionUsageTest.java b/sechub-test/src/test/java/com/mercedesbenz/sechub/test/encryption/EncryptionUsageTest.java new file mode 100644 index 0000000000..caf972757c --- /dev/null +++ b/sechub-test/src/test/java/com/mercedesbenz/sechub/test/encryption/EncryptionUsageTest.java @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.test.encryption; + +import static org.assertj.core.api.Assertions.*; + +import javax.crypto.AEADBadTagException; + +import org.junit.jupiter.api.Test; + +import com.mercedesbenz.sechub.commons.encryption.DefaultSecretKeyProvider; +import com.mercedesbenz.sechub.commons.encryption.EncryptionResult; +import com.mercedesbenz.sechub.commons.encryption.EncryptionRotationSetup; +import com.mercedesbenz.sechub.commons.encryption.EncryptionRotator; +import com.mercedesbenz.sechub.commons.encryption.EncryptionSupport; +import com.mercedesbenz.sechub.commons.encryption.InitializationVector; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipher; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherFactory; +import com.mercedesbenz.sechub.commons.encryption.PersistentCipherType; + +public class EncryptionUsageTest { + + @Test + void cipher_direct_usage() { + /* prepare */ + PersistentCipherType cipherType = PersistentCipherType.AES_GCM_SIV_256; + byte[] rawPlainTextInBytes = "hallo welt".getBytes(); + byte[] testSecret256bit = "x".repeat(32).getBytes(); + + /* execute - usage .. */ + PersistentCipherFactory factory = new PersistentCipherFactory(); + + // encrypt and simulate storage + PersistentCipher cipher = factory.createCipher(new DefaultSecretKeyProvider(testSecret256bit, cipherType), cipherType); + + InitializationVector initVector = cipher.createNewInitializationVector(); + byte[] encrypted = cipher.encrypt(rawPlainTextInBytes, initVector); + + // simulate storage + byte[] storedEncryptedDataBytes = encrypted; + byte[] storedInitVectorBytes = initVector.getInitializationBytes(); + + // decrypt + PersistentCipher anotherCipher = factory.createCipher(new DefaultSecretKeyProvider(testSecret256bit, cipherType), cipherType); + byte[] decrypted = anotherCipher.decrypt(storedEncryptedDataBytes, new InitializationVector(storedInitVectorBytes)); + + /* test */ + assertThat(decrypted).isEqualTo(rawPlainTextInBytes); + + } + + @Test + void encryption_support_usage_with_string() { /* prepare */ + PersistentCipherType cipherType = PersistentCipherType.AES_GCM_SIV_256; + String plainText = "hallo welt"; + byte[] testSecret256bit = "x".repeat(32).getBytes(); + + /* execute - usage .. */ + PersistentCipherFactory factory = new PersistentCipherFactory(); + + // encrypt and simulate storage + PersistentCipher cipher = factory.createCipher(new DefaultSecretKeyProvider(testSecret256bit, cipherType), cipherType); + + EncryptionSupport support = new EncryptionSupport(); + + EncryptionResult encryptionResult = support.encryptString(plainText, cipher); + + // simulate storage + byte[] storedEncryptedDataBytes = encryptionResult.getEncryptedData(); + byte[] storedInitVectorBytes = encryptionResult.getInitialVector().getInitializationBytes(); + + // decrypt + PersistentCipher anotherCipher = factory.createCipher(new DefaultSecretKeyProvider(testSecret256bit, cipherType), cipherType); + String decrypted = support.decryptString(storedEncryptedDataBytes, anotherCipher, new InitializationVector(storedInitVectorBytes)); + + /* test */ + assertThat(decrypted).isEqualTo(plainText); + + } + + @Test + void rotation_with_changed_password_same_init_vector() { + /* prepare */ + String testData = "hallo welt"; + byte[] oldPassword = "x".repeat(32).getBytes(); + byte[] newPassword = "y".repeat(32).getBytes(); + + PersistentCipherFactory factory = new PersistentCipherFactory(); + PersistentCipherType cipherType = PersistentCipherType.AES_GCM_SIV_256; + PersistentCipher cipherOldPassword = factory.createCipher(new DefaultSecretKeyProvider(oldPassword, cipherType), cipherType); + + EncryptionSupport encryptSupport = new EncryptionSupport(); + + // encrypt with old cipher + EncryptionResult encryptionResult = encryptSupport.encryptString(testData, cipherOldPassword); + + PersistentCipher cipherNewPassword = factory.createCipher(new DefaultSecretKeyProvider(newPassword, cipherType), cipherType); + + EncryptionRotator rotatorToTest = new EncryptionRotator(); + + /* test */ + + /* @formatter:off */ + EncryptionRotationSetup rotateSetup = EncryptionRotationSetup.builder(). + newCipher(cipherNewPassword). + oldCipher(cipherOldPassword). + oldInitialVector(encryptionResult.getInitialVector()). + build(); + + byte[] newEncrypted = rotatorToTest.rotate(encryptionResult.getEncryptedData(), rotateSetup); + + /* test */ + + /* @formatter:on */ + String unencryptedTestData = encryptSupport.decryptString(newEncrypted, cipherNewPassword, rotateSetup.getNewInitialVector()); + + assertThat(testData).isEqualTo(unencryptedTestData); + + } + + @Test + void rotation_with_changed_password_different_init_vector() { + /* prepare */ + String testData = "hallo welt"; + byte[] oldPassword = "x".repeat(32).getBytes(); + byte[] newPassword = "y".repeat(32).getBytes(); + + PersistentCipherFactory factory = new PersistentCipherFactory(); + PersistentCipherType cipherType = PersistentCipherType.AES_GCM_SIV_256; + PersistentCipher cipherOldPassword = factory.createCipher(new DefaultSecretKeyProvider(oldPassword, cipherType), cipherType); + + EncryptionSupport encryptSupport = new EncryptionSupport(); + + // encrypt with old cipher + EncryptionResult firstEncryptionResult = encryptSupport.encryptString(testData, cipherOldPassword); + + PersistentCipher cipherNewPassword = factory.createCipher(new DefaultSecretKeyProvider(newPassword, cipherType), cipherType); + InitializationVector newInitVector = cipherNewPassword.createNewInitializationVector(); + + EncryptionRotator rotatorToTest = new EncryptionRotator(); + + /* test */ + + /* @formatter:off */ + EncryptionRotationSetup rotateSetup = EncryptionRotationSetup.builder(). + newCipher(cipherNewPassword). + oldCipher(cipherOldPassword). + oldInitialVector(firstEncryptionResult.getInitialVector()). + newInitialVector(newInitVector). + build(); + + byte[] newEncrypted = rotatorToTest.rotate(firstEncryptionResult.getEncryptedData(), rotateSetup); + + /* test */ + + /* @formatter:on */ + String unencryptedTestData = encryptSupport.decryptString(newEncrypted, cipherNewPassword, newInitVector); + + assertThat(testData).isEqualTo(unencryptedTestData); + + // additional test - we check that the decryption with old init vector does not + // work + assertThatThrownBy(() -> encryptSupport.decryptString(newEncrypted, cipherNewPassword, firstEncryptionResult.getInitialVector())) + .isInstanceOf(IllegalStateException.class).hasRootCauseInstanceOf(AEADBadTagException.class); + } +} diff --git a/sechub-testframework/src/main/java/com/mercedesbenz/sechub/test/SecHubTestURLBuilder.java b/sechub-testframework/src/main/java/com/mercedesbenz/sechub/test/SecHubTestURLBuilder.java index 190fd95fbe..942266d5ff 100644 --- a/sechub-testframework/src/main/java/com/mercedesbenz/sechub/test/SecHubTestURLBuilder.java +++ b/sechub-testframework/src/main/java/com/mercedesbenz/sechub/test/SecHubTestURLBuilder.java @@ -445,6 +445,14 @@ public String buildAdminFetchesAutoCleanupConfigurationUrl() { return buildUrl(API_ADMIN_CONFIG, "autoclean"); } + public String buildAdminStartsEncryptionRotation() { + return buildUrl(API_ADMIN, "encryption/rotate"); + } + + public String buildAdminFetchesEncryptionStatus() { + return buildUrl(API_ADMIN, "encryption/status"); + } + /* +-----------------------------------------------------------------------+ */ /* +............................ integration test special (anonymous) .....+ */ /* +-----------------------------------------------------------------------+ */ @@ -642,7 +650,6 @@ public String buildIntegrationTestFetchFullScandata(UUID sechubJobUIUD) { } // statistic parts - public String buildintegrationTestFetchJobStatistic(UUID sechubJobUUID) { return buildUrl(API_ANONYMOUS, "integrationtest/statistic/job/" + sechubJobUUID); } @@ -659,4 +666,12 @@ public String buildintegrationTestFetchJobRunStatisticData(UUID sechubJobUUID) { return buildUrl(API_ANONYMOUS, "integrationtest/statistic/job-run-data/" + sechubJobUUID); } + public String buildIntegrationTestFetchScheduleEncryptionPoolIdForSecHubJob(UUID sechubJobUUID) { + return buildUrl(API_ANONYMOUS, "integrationtest/schedule/encryption-pool-id/job/" + sechubJobUUID.toString()); + } + + public String buildIntegrationTestStartScheduleCipherPoolDataCleanup() { + return buildUrl(API_ANONYMOUS, "integrationtest/schedule/cipher-pool-data/cleanup"); + } + } diff --git a/settings.gradle b/settings.gradle index 3eb2e1a6ad..aa295cf046 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,6 +15,9 @@ include 'sechub-cli', 'sechub-commons-pds', 'sechub-commons-archive', +/* encryption */ +'sechub-commons-encryption', + /* server POD area */ 'sechub-server', // executable spring boot jar 'sechub-server-core', // core project for test compile dependencies and more