From ba2b4efd8b2caa5c30565749f76cbff25de25916 Mon Sep 17 00:00:00 2001 From: Albert Tregnaghi Date: Thu, 17 Oct 2024 10:10:09 +0200 Subject: [PATCH 01/21] Enhanced concept of templates and assets #3521 --- .../concept_templates_and_assets.adoc | 221 +++++++++--------- .../pds-param-template-metadata-example1.json | 11 + .../pds-param-template-metadata-syntax.json | 16 ++ .../snippet/template-definition-syntax.json | 26 +++ 4 files changed, 163 insertions(+), 111 deletions(-) create mode 100644 sechub-doc/src/docs/asciidoc/documents/shared/snippet/pds-param-template-metadata-example1.json create mode 100644 sechub-doc/src/docs/asciidoc/documents/shared/snippet/pds-param-template-metadata-syntax.json create mode 100644 sechub-doc/src/docs/asciidoc/documents/shared/snippet/template-definition-syntax.json diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_templates_and_assets.adoc b/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_templates_and_assets.adoc index 2e9492cfa..489f2851b 100644 --- a/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_templates_and_assets.adoc +++ b/sechub-doc/src/docs/asciidoc/documents/shared/concepts/concept_templates_and_assets.adoc @@ -31,39 +31,53 @@ assets and also a variable definition. It can be administrated via REST end points: ``` -PUT /api/administration/template/$template-id -GET /api/administration/template/$template-id -DELETE /api/administration/template/$template-id +PUT /api/admin/template/$template-id +GET /api/admin/template/$template-id +DELETE /api/admin/template/$template-id ``` -PUT will contain following body: +PUT will contain a body with the template data: +====== Template definition +The template definitions are hold inside {sechub} database. +A template is defined by following json syntax: + +.Template definition syntax [source,json] ---- -{ - "type" : "webscan-login",//<1> - "assets" : ["asset-id-1"]//<2> - "variables" : {//<3> - "username" : "mandatory",//<4> - "password" : "mandatory", - "tip-of-the-day" : "optional"//<5> - } -} +include::../snippet/template-definition-syntax.json[] ---- <1> The type of the template. Currently possible: `webscan-login` <2> Array with asset identifiers assigned to the template <3> Variable definitions as list of key and value pairs which can be mandatory or optional. Via the type {sechub} server is able to check if the configuration is valid and give response to users when job starts. +<4> Name of the variable +<5> Describes if the variable is optional. + + When `false` configuration must contain the variable inside template data. When `true` + the configuration is valid without the variable. The default is `false`. +<6> Variable content validation definition ((optional) +<7> Minimum length (optional) +<8> Maximum length (optional) +<9> Regular expression (optional). If defined, the content must match the given regular expression + to be valid. + +[CAUTION] +==== +The validation section inside the definition is only for a "first simple check" on {sechub} side, to +stop before any {pds} job is created. +*But {pds} solutions which are using templates must ensure,that the given user content +(variable) is correct and not some malicious injected data!* +==== ===== Mapping templates to projects ``` -PUT /api/admin/project/$projectId/template/$templateid +PUT /api/admin/project/$projectId/template DELETE /api/admin/project/$projectId/template/$templateid -GET /api/admin/project/$projectId/template/list +GET /api/admin/project/$projectId/templates ``` -[IMPORTANT] +[NOTE] ==== The mapping is stored with template type as part of the composite key: If two template definitions with same type are uploaded, the last one will @@ -84,52 +98,27 @@ include::../configuration/sechub_config_example22_webscan_with_template.json[] is not able to define which template is used. <2> Setup template variables by a list of key and value pairs. -[IMPORTANT] -==== If the user has defined a `templateData` section in the configuration but no template of that type is assigned to the project, a dedicated error message must be returned to the user and the {sechub} job will always fail. -==== - -===== Asset storage -The assets are initially stored in {sechub} database, together with the asset file checksum. - -When a template is used inside a {sechub} configuration, the {sechub} start mechanism will -check if the file from database is already available at storage (S3/NFS) which represents -a cache for the assets. - -If available, the cached part will be used. Otherwise it will be uploaded -(see <> ) - - -[TIP] -==== -Main reason why we store always in DB and use storage (S3/NFS) as a cache only: - -1. Restore a DB backup on a fresh installation: + - Administrators can just apply the database backup and everything works again -2. Storage could be volatile/be deleted etc. (we rely to the database) -3. For having two clusters ( {sechub} and {pds} having not same database we need - a way to exchange. -==== - [IMPORTANT] ==== -For templates and assets we use the shared storage between {sechub} and {pds} . -solutions and not to provide own storage for {pds}. Diffeent storages are not supported here. +For templates and assets we must use shared storage between {sechub} and {pds} . +{pds} solutions may not to provide own storage for {pds} - different storages +are not supported here. ==== -`/assets/$assetId/` +====== Asset id format +An asset id -An asset id has - -- maximum length of 40 +- has a maximum length of 40 - can contain only `a-z` `A-Z`, `0-9` or `-` or `_` -At the storage for each product (as named in the `pds-config.json` of the {pds} solution), there will be a ZIP file having the -product identifier as filename +====== {sechub} server uses product identifiers for asset ids +The {sechub} server will use the product identifiers (like in the executor configurations) +as filenames when storing asset data inside database (and also inside storage). -For example: +Here an example for storage paths: ``` /assets/ asset-id-1/ @@ -145,7 +134,8 @@ The PDS solution defines which kind of template data is necessary and how the structure looks like! ==== -===== Administration REST end points for storage +===== Administration +====== General Over REST API Administrators will be able to - list (ZIP) file names + @@ -161,16 +151,35 @@ Over REST API Administrators will be able to The REST API will always create an audit log entry +====== Upload +The upload and delete operations will always handle DB and storage (S3/NFS)! +If an asset ZIP file already exists, the operation will overwrite the file and the +old sha256 checksum in both locations. + +[TIP] +==== +Main reason why we store always in DB and use storage (S3/NFS) as a cache only: + +1. Restore a DB backup on a fresh installation: + + Administrators can just apply the database backup and everything works again +2. Storage could be volatile/be deleted etc. (we rely to the database) +3. For having multiple and also different cluster types ( {sechub} cluster and multiple {pds} clusters) + having not same database we need a common way to exchange data. + Each {pds} cluster and also the {sechub} cluster have their own + database - means cannot be used for exchange. +==== + + [IMPORTANT] ==== This works only when the storage is shared between {sechub} and {pds}. If the {pds} uses its own storage (which should NOT be done for production, but only -for PDS solution development) the assets must be uploaded directly to the PDS storage -location! +for PDS solution development) the assets would needed to be uploaded directly to the PDS storage +location with correct checksum etc. ==== -===== SecHub template and asset handling -====== Validation +===== SecHub runtime +====== Validate config uses valid template location When a {sechub} job starts and there is a template data definition inside the configuration, {sechub} will validate if the project has a template assigned for the location inside the configuration. e.g. templates with type `webscan-login` may only be defined inside @@ -179,51 +188,43 @@ web scan login configuration). If this validation fails, the complete {sechub} job will fail and stop processing. [[sechub-concept-asset-upload-lazy]] -====== Upload in storage as cache lazily -When validation did not fail, {sechub} will check if the current version of the asset is already uploaded to -storage (S3/NFS) already. +====== Validate config uses valid template location + +When validation did not fail, {sechub} will check if the current version of the +necessary product specific asset ZIP file is exsting in storage. -When not uploaded to storage, the file will be uploaded before the job is further processed. +When not uploaded to storage, the file will be uploaded before the job is +further processed. ====== Template PDS parameter calculation -If validation and lazy upload did not fail, {sechub} will calculate a -{pds} parameter : `pds.template.metadata` with JSON (see parameter syntax for details). +If the job configuration has valid template data and the template +is available in storage, {sechub} will calculate the +{pds} parameter : `pds.template.metadata` with JSON: -.PDS parameter syntax for template meta data +[[sechub-concept-template-metadata-example]] +.PDS parameter syntax for template meta data [source,json] ---- -{ - "template-metadata" : [ //<1> - { - "template" : "single-singon", //<2> - "type": "webscan-login",//<3> - "assets" : [ //<4> - { - "id" : "asset-id-1", //<5> - "sha256" : "434c6c6ec1b0ed9844149069d7d45ac18e72505b" //<6> - } - ] - }] - } -} - +include::../snippet/pds-param-template-metadata-syntax.json[] ---- <1> Meta data array <2> Template identifier - just as an information for logging etc. <3> Template type <4> Asset information array -<5> ID of the asset, necessary for storage download -<6> checksum of the asset ZIP file, necessary to check after download +<5> Asset identifier +<6> Name of file (inside asset) +<7> Checksum of the file (SHA256) -===== PDS asset handling +===== PDS runtime +====== Asset handling {sechub} calls {pds}, with {pds} parameter `pds.template.metadata` (syntax is described above). -The {pds} intance will fetch all wanted asset ZIP file for the current product -from storage (S3 or NFS) and extract it to `$workspaceFolder/assets/`. +The {pds} instance will fetch all defined files from storage (S3 or NFS) +and extract/copy it to `$workspaceFolder/assets/`. -Before extraction is done a checksum for the downloaded ZIP file is created and compared -with the checksum from template meta data. If it is different the {pds} job will fail -with a dedicated error message. +Before extraction is done a checksum for the downloaded file is created and compared +with the checksum from template meta data. The checksum algorithm is SHA256. +If it is different the {pds} job will fail with a dedicated error message. If the checksum is valid, the assets ZIP file will be unzipped below a subfolder with the template type: @@ -238,7 +239,7 @@ template type: An example: -The `MY_PRODUCT.zip` file contains +The `WEBSCAN_PRODUCT_ID.zip` file contains [source,text] ---- @@ -249,25 +250,15 @@ The `MY_PRODUCT.zip` file contains and the template meta data looks like this: -[source,text] +[source,json] ---- -{ - "template-metadata" : { - { - "type: "webscan-login", - "assets" : [ - { - "id" : "asset-id-1", - "sha256" : "434c6c6ec1b0ed9844149069d7d45ac18e72505b" - } - ] - } - } -} +include::../snippet/pds-param-template-metadata-example1.json[] ---- +The extraction is done into folder: +`$workspaceFolder/assets/$type/` -It will be extracted the following way. +For the former example `WEBSCAN_PRODUCT_ID.zip` will be extracted the following way: [source,text] ---- @@ -275,29 +266,37 @@ It will be extracted the following way. assets/ webscan-login/ script.js - develoment/ + development/ debug-settings.json ---- ====== Pseudo code for usage inside PDS solutions/wrappers -Here an example (but pseudo code) how a product could use the assets inside: +Here an example (but pseudo code) how a product could use the assets inside +a wrapper application for a PDS solution: [source,java] ---- -WebScanTemplateData data = util.fetchWebScanTemplateData(sechubConfig); -if (data!=null){ + +import com.mercedesbenz.sechub.commons.model.template.*; + +WebScanTemplateData templateData = util.fetchWebScanTemplateData(sechubConfig); +if (templateData!=null){ + // folder=/$workspaceFolder/assets File folder = util.getAssetFolder(); - // in example: it is a web scan... - script = folder.getChild("webscan-login/script.js"); + // fetch file: in example it is a web scan login template + File scriptFile = folder.getChild(ID_WEBSCAN_LOGIN+"/script.js"); - script = replaceVariableInScript(data.getVariable("username"), script); - script = replaceVariableInScript(data.getVariable("password"), script); + // process + String script = readTextFile(scriptFile); + script = replaceVariableInScript(templateData.getVariable("username"), script); + script = replaceVariableInScript(templateData.getVariable("password"), script); - // use script.... + // use script + // ... } ---- diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/snippet/pds-param-template-metadata-example1.json b/sechub-doc/src/docs/asciidoc/documents/shared/snippet/pds-param-template-metadata-example1.json new file mode 100644 index 000000000..644ded1af --- /dev/null +++ b/sechub-doc/src/docs/asciidoc/documents/shared/snippet/pds-param-template-metadata-example1.json @@ -0,0 +1,11 @@ +{ + "templateMetaData" : [ { + "template" : "single-singon", + "type" : "webscan-login", + "assetData" : [ { + "asset" : "custom-webscan-setup", + "file" : "WEBSCAN_PRODUCT_ID.zip", + "checksum" : "434c6c6ec1b0ed9844149069d7d45ac18e72505b" + } ] + } ] +} \ No newline at end of file diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/snippet/pds-param-template-metadata-syntax.json b/sechub-doc/src/docs/asciidoc/documents/shared/snippet/pds-param-template-metadata-syntax.json new file mode 100644 index 000000000..285fcffa0 --- /dev/null +++ b/sechub-doc/src/docs/asciidoc/documents/shared/snippet/pds-param-template-metadata-syntax.json @@ -0,0 +1,16 @@ +{ + "templateMetaData" : [ //<1> + { + "template" : "$templateId", //<2> + "type": "$templateType", //<3> + + "assetData" : [ //<4> + { + "asset" : "$assetId", //<5> + "file" : "$fileName", //<6> + "checksum" : "$fileChecksum" //<7> + } + ] + }] + } +} \ No newline at end of file diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/snippet/template-definition-syntax.json b/sechub-doc/src/docs/asciidoc/documents/shared/snippet/template-definition-syntax.json new file mode 100644 index 000000000..5a3cc0f46 --- /dev/null +++ b/sechub-doc/src/docs/asciidoc/documents/shared/snippet/template-definition-syntax.json @@ -0,0 +1,26 @@ +{ + "templateDefinition" : { + + "id" : "$templateId", + + "type" : "$templateType", //<1> + + "assets" : ["$assetId1" , "$assetId2", /* .. ,*/ "$assetIdn"], //<2> + + "variables" : [ //<3> + { + "name" : "$variableName", // <4> + "optional": false, // <5> + + "validation" : { // <6> + + "minLength" : 1, // <7> + "maxLength" : 100, // <8> + + "regularExpression" : "$regularExpression" // <9> + } + } + ] + } + +} \ No newline at end of file From c5fc9299a111e52d71222b43911928264d0d6b35 Mon Sep 17 00:00:00 2001 From: Albert Tregnaghi Date: Tue, 22 Oct 2024 10:58:47 +0200 Subject: [PATCH 02/21] Introduce templates #3520 --- .../core/util/SecHubStorageUtilTest.java | 2 +- sechub-commons-model/build.gradle | 1 + .../commons/model/template/TemplateData.java | 11 + .../model/template/TemplateDefinition.java | 128 +++++++++ .../template/TemplateIdenifierConstants.java | 18 ++ .../commons/model/template/TemplateType.java | 29 ++ .../template/TemplateDefinitionTest.java | 86 ++++++ .../template-definition-example1.json | 14 + ...alsePositiveRestControllerRestDocTest.java | 4 +- .../sechub/restdoc/OpenApiSchema.java | 2 +- .../TemplateRestControllerRestDocTest.java | 268 ++++++++++++++++++ .../src/main/resources/openapi.yaml | 140 ++++++++- .../sechub/domain/scan/template/Template.java | 72 +++++ .../scan/template/TemplateRepository.java | 13 + .../scan/template/TemplateRestController.java | 84 ++++++ .../domain/scan/template/TemplateService.java | 92 ++++++ .../scan/template/TemplateServiceTest.java | 133 +++++++++ .../common/U32__template_and_assets.sql | 3 + .../common/V32__template_and_assets.sql | 12 + .../usecases/UseCaseIdentifier.java | 10 + .../UseCaseAdminCreatesOrUpdatesTemplate.java | 27 ++ .../config/UseCaseAdminDeletesTemplate.java | 27 ++ .../UseCaseAdminFetchesAllTemplateIds.java | 27 ++ .../config/UseCaseAdminFetchesTemplate.java | 27 ++ .../usecases/UseCaseIdentifierTest.java | 11 +- .../sechub/test/RestDocPathParameter.java | 2 + .../sechub/test/SecHubTestURLBuilder.java | 20 ++ 27 files changed, 1256 insertions(+), 7 deletions(-) create mode 100644 sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateData.java create mode 100644 sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateDefinition.java create mode 100644 sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateIdenifierConstants.java create mode 100644 sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateType.java create mode 100644 sechub-commons-model/src/test/java/com/mercedesbenz/sechub/commons/model/template/TemplateDefinitionTest.java create mode 100644 sechub-commons-model/src/test/resources/template/template-definition-example1.json create mode 100644 sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/TemplateRestControllerRestDocTest.java create mode 100644 sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/Template.java create mode 100644 sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateRepository.java create mode 100644 sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateRestController.java create mode 100644 sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateService.java create mode 100644 sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/template/TemplateServiceTest.java create mode 100644 sechub-server/src/main/resources/db/migration/common/U32__template_and_assets.sql create mode 100644 sechub-server/src/main/resources/db/migration/common/V32__template_and_assets.sql create mode 100644 sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/admin/config/UseCaseAdminCreatesOrUpdatesTemplate.java create mode 100644 sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/admin/config/UseCaseAdminDeletesTemplate.java create mode 100644 sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/admin/config/UseCaseAdminFetchesAllTemplateIds.java create mode 100644 sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/admin/config/UseCaseAdminFetchesTemplate.java diff --git a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/util/SecHubStorageUtilTest.java b/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/util/SecHubStorageUtilTest.java index a4fbc850b..97f9cd573 100644 --- a/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/util/SecHubStorageUtilTest.java +++ b/sechub-commons-core/src/test/java/com/mercedesbenz/sechub/commons/core/util/SecHubStorageUtilTest.java @@ -8,7 +8,7 @@ public class SecHubStorageUtilTest { @Test - void jobstorage_path_for_porject_with_id_project1234_is_correct_calculated() { + void jobstorage_path_for_project_with_id_project1234_is_correct_calculated() { assertEquals("jobstorage/project1234", SecHubStorageUtil.createStoragePath("project1234")); } diff --git a/sechub-commons-model/build.gradle b/sechub-commons-model/build.gradle index f0e7b9d98..e8b874151 100644 --- a/sechub-commons-model/build.gradle +++ b/sechub-commons-model/build.gradle @@ -17,4 +17,5 @@ dependencies{ testImplementation spring_boot_dependency.mockito_core testImplementation spring_boot_dependency.hamcrest + testImplementation spring_boot_dependency.assertj_core } \ No newline at end of file diff --git a/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateData.java b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateData.java new file mode 100644 index 000000000..9cbb9ee6c --- /dev/null +++ b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateData.java @@ -0,0 +1,11 @@ +package com.mercedesbenz.sechub.commons.model.template; + +/** + * Template data for SecHub configuration model + * + * @author Albert Tregnaghi + * + */ +public class TemplateData { + +} diff --git a/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateDefinition.java b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateDefinition.java new file mode 100644 index 000000000..04ea28237 --- /dev/null +++ b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateDefinition.java @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.commons.model.template; + +import java.util.ArrayList; +import java.util.List; + +import com.mercedesbenz.sechub.commons.model.JSONable; + +public class TemplateDefinition implements JSONable { + + private static TemplateDefinition IMPORTER = new TemplateDefinition(); + + public static final String PROPERTY_TYPE = "type"; + public static final String PROPERTY_ID = "id"; + public static final String PROPERTY_ASSETS = "assets"; + public static final String PROPERTY_VARIABLES = "variables"; + + private TemplateType type; + + private List assets = new ArrayList<>(); + private List variables = new ArrayList<>(); + + private String id; + + public static TemplateDefinition from(String json) { + return IMPORTER.fromJSON(json); + } + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public List getAssets() { + return assets; + } + + public List getVariables() { + return variables; + } + + public void setType(TemplateType type) { + this.type = type; + } + + public TemplateType getType() { + return type; + } + + public static class TemplateVariable { + public static final String PROPERTY_NAME = "name"; + public static final String PROPERTY_OPTIONAL = "optional"; + public static final String PROPERTY_VALIDATION = "validation"; + + private String name; + private boolean optional; + private TemplateVariableValidation validation; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isOptional() { + return optional; + } + + public void setOptional(boolean optional) { + this.optional = optional; + } + + public TemplateVariableValidation getValidation() { + return validation; + } + + public void setValidation(TemplateVariableValidation validation) { + this.validation = validation; + } + + } + + public static class TemplateVariableValidation { + + public static final String PROPERTY_MIN_LENGTH = "minLength"; + public static final String PROPERTY_MAX_LENGTH = "maxLength"; + public static final String PROPERTY_REGULAR_EXPRESSION = "regularExpression"; + + private Integer minLength; + private Integer maxLength; + private String regularExpression; + + public Integer getMinLength() { + return minLength; + } + + public void setMinLength(Integer minLength) { + this.minLength = minLength; + } + + public Integer getMaxLength() { + return maxLength; + } + + public void setMaxLength(Integer maxLength) { + this.maxLength = maxLength; + } + + public String getRegularExpression() { + return regularExpression; + } + + public void setRegularExpression(String regularExpression) { + this.regularExpression = regularExpression; + } + } + + @Override + public Class getJSONTargetClass() { + return TemplateDefinition.class; + } + +} diff --git a/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateIdenifierConstants.java b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateIdenifierConstants.java new file mode 100644 index 000000000..9a48c6120 --- /dev/null +++ b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateIdenifierConstants.java @@ -0,0 +1,18 @@ +package com.mercedesbenz.sechub.commons.model.template; + +import com.mercedesbenz.sechub.commons.core.MustBeKeptStable; + +/** + * Template constants - used as identifier inside database, json files but also + * for path handling inside PDS solutions. So NEVER CHANGE the content of this + * identifiers!!!! + * + * @author Albert Tregnaghi + * + */ +@MustBeKeptStable +public class TemplateIdenifierConstants { + + public static final String ID_WEBSCAN_LOGIN = "webscan-login"; + +} diff --git a/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateType.java b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateType.java new file mode 100644 index 000000000..23599e5dd --- /dev/null +++ b/sechub-commons-model/src/main/java/com/mercedesbenz/sechub/commons/model/template/TemplateType.java @@ -0,0 +1,29 @@ +package com.mercedesbenz.sechub.commons.model.template; + +import static com.mercedesbenz.sechub.commons.model.template.TemplateIdenifierConstants.*; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.mercedesbenz.sechub.commons.core.MustBeKeptStable; + +/** + * Defines the template type + * + * @author Albert Tregnaghi + * + */ +@MustBeKeptStable +public enum TemplateType { + + @JsonAlias({ ID_WEBSCAN_LOGIN }) + WEBSCAN_LOGIN(ID_WEBSCAN_LOGIN); + + private String id; + + TemplateType(String id) { + this.id = id; + } + + public String getId() { + return id; + } +} diff --git a/sechub-commons-model/src/test/java/com/mercedesbenz/sechub/commons/model/template/TemplateDefinitionTest.java b/sechub-commons-model/src/test/java/com/mercedesbenz/sechub/commons/model/template/TemplateDefinitionTest.java new file mode 100644 index 000000000..f4b38b158 --- /dev/null +++ b/sechub-commons-model/src/test/java/com/mercedesbenz/sechub/commons/model/template/TemplateDefinitionTest.java @@ -0,0 +1,86 @@ +package com.mercedesbenz.sechub.commons.model.template; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition.TemplateVariable; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition.TemplateVariableValidation; +import com.mercedesbenz.sechub.test.TestFileReader; + +class TemplateDefinitionTest { + + @Test + public void json_to_from_works() throws Exception { + + /* prepare */ + TemplateDefinition definition = new TemplateDefinition(); + definition.setId("identifier"); + definition.getAssets().add("asset1"); + definition.getAssets().add("asset2"); + + definition.setType(TemplateType.WEBSCAN_LOGIN); + + TemplateVariable variable1 = new TemplateVariable(); + variable1.setName("variable1"); + variable1.setOptional(true); + + TemplateVariableValidation validation = new TemplateVariableValidation(); + validation.setMinLength(1); + validation.setMaxLength(100); + validation.setRegularExpression("[a-z].*"); + variable1.setValidation(validation); + definition.getVariables().add(variable1); + + /* execute */ + String json = definition.toFormattedJSON(); + + /* test */ + TemplateDefinition deserialized = TemplateDefinition.from(json); + assertThat(deserialized.getId()).isEqualTo("identifier"); + assertThat(deserialized.getType()).isEqualTo(TemplateType.WEBSCAN_LOGIN); + assertThatList(deserialized.getAssets()).contains("asset1", "asset2"); + assertThatList(deserialized.getVariables()).hasSize(1); + + TemplateVariable var1 = deserialized.getVariables().iterator().next(); + assertThat(var1).describedAs("variable1").hasNoNullFieldsOrProperties(); + assertThat(var1.getName()).isEqualTo("variable1"); + assertThat(var1.isOptional()).isTrue(); + assertThat(var1.getValidation()).isNotNull(); + + TemplateVariableValidation var1Validation = var1.getValidation(); + assertThat(var1Validation).isNotNull(); + assertThat(var1Validation.getMinLength()).isEqualTo(1); + assertThat(var1Validation.getMaxLength()).isEqualTo(100); + assertThat(var1Validation.getRegularExpression()).isEqualTo("[a-z].*"); + } + + @Test + public void example1_with_type_defined_by_id_and_not_enum_name_can_be_loaded() throws Exception { + /* prepare */ + String json = TestFileReader.readTextFromFile("./src/test/resources/template/template-definition-example1.json"); + + /* execute */ + TemplateDefinition deserialized = TemplateDefinition.from(json); + + /* test */ + assertThat(deserialized.getId()).isEqualTo("identifier"); + assertThat(deserialized.getType()).isEqualTo(TemplateType.WEBSCAN_LOGIN); + assertThatList(deserialized.getAssets()).contains("asset1", "asset2"); + assertThatList(deserialized.getVariables()).hasSize(1); + + TemplateVariable var1 = deserialized.getVariables().iterator().next(); + assertThat(var1).describedAs("variable1").hasNoNullFieldsOrProperties(); + assertThat(var1.getName()).isEqualTo("variable1"); + assertThat(var1.isOptional()).isTrue(); + assertThat(var1.getValidation()).isNotNull(); + + TemplateVariableValidation var1Validation = var1.getValidation(); + assertThat(var1Validation).isNotNull(); + assertThat(var1Validation.getMinLength()).isEqualTo(1); + assertThat(var1Validation.getMaxLength()).isEqualTo(100); + assertThat(var1Validation.getRegularExpression()).isEqualTo("[a-z].*"); + + } + +} diff --git a/sechub-commons-model/src/test/resources/template/template-definition-example1.json b/sechub-commons-model/src/test/resources/template/template-definition-example1.json new file mode 100644 index 000000000..22c7ef12a --- /dev/null +++ b/sechub-commons-model/src/test/resources/template/template-definition-example1.json @@ -0,0 +1,14 @@ +{ + "type" : "webscan-login", + "assets" : [ "asset1", "asset2" ], + "variables" : [ { + "name" : "variable1", + "optional" : true, + "validation" : { + "minLength" : 1, + "maxLength" : 100, + "regularExpression" : "[a-z].*" + } + } ], + "id" : "identifier" +} diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/FalsePositiveRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/FalsePositiveRestControllerRestDocTest.java index 17a7b24f3..7b0d62418 100644 --- a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/FalsePositiveRestControllerRestDocTest.java +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/FalsePositiveRestControllerRestDocTest.java @@ -158,7 +158,7 @@ public void restdoc_mark_false_positives() throws Exception { fieldWithPath(PROPERTY_JOBDATA+"[]."+ PROPERTY_FINDINGID).description("SecHub finding identifier - identifies problem inside the job which shall be markeda as a false positive."), fieldWithPath(PROPERTY_JOBDATA+"[]."+ FalsePositiveJobData.PROPERTY_COMMENT).optional().description("A comment describing why this is a false positive"), - fieldWithPath(PROPERTY_PROJECTDATA).description("Porject data list containing false positive setup for the project"), + fieldWithPath(PROPERTY_PROJECTDATA).description("Project data list containing false positive setup for the project"), fieldWithPath(PROPERTY_PROJECTDATA+"[]."+ PROPERTY_ID).description("Identifier which is used to update or remove the respective false positive entry."), fieldWithPath(PROPERTY_PROJECTDATA+"[]."+ FalsePositiveProjectData.PROPERTY_COMMENT).optional().description("A comment describing why this is a false positive."), fieldWithPath(PROPERTY_PROJECTDATA+"[]."+ PROPERTY_WEBSCAN).optional().description("Defines a section for false positives which occur during webscans."), @@ -353,7 +353,7 @@ public void user_fetches_false_positive_configuration() throws Exception { fieldWithPath(PROPERTY_FALSE_POSITIVES+"[]."+FalsePositiveEntry.PROPERTY_JOBDATA+"."+PROPERTY_FINDINGID).description("SecHub finding identifier - identifies problem inside the job which shall be markeda as a false positive. *ATTENTION*: at the moment only code scan false positive handling is supported. Infra and web scan findings will lead to a non accepted error!"), fieldWithPath(PROPERTY_FALSE_POSITIVES+"[]."+FalsePositiveEntry.PROPERTY_JOBDATA+"."+FalsePositiveJobData.PROPERTY_COMMENT).optional().description("A comment from author describing why this was marked as a false positive"), - fieldWithPath(PROPERTY_FALSE_POSITIVES+"[]."+PROPERTY_PROJECTDATA).optional().description("Porject data list containing false positive setup for the project."), + fieldWithPath(PROPERTY_FALSE_POSITIVES+"[]."+PROPERTY_PROJECTDATA).optional().description("Project data list containing false positive setup for the project."), fieldWithPath(PROPERTY_FALSE_POSITIVES+"[]."+PROPERTY_PROJECTDATA+"."+ PROPERTY_ID).description("Identifier which is used to update or remove the respective false positive entry."), fieldWithPath(PROPERTY_FALSE_POSITIVES+"[]."+PROPERTY_PROJECTDATA+"."+ FalsePositiveProjectData.PROPERTY_COMMENT).optional().description("A comment describing why this is a false positive."), fieldWithPath(PROPERTY_FALSE_POSITIVES+"[]."+PROPERTY_PROJECTDATA+"."+ PROPERTY_WEBSCAN).optional().description("Defines a section for false positives which occur during webscans."), 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 70ac1df00..2eeeb293d 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,7 +64,7 @@ enum OpenApiSchema { ENCRYPTION_STATUS("EncryptionStatus"), - ; + TEMPLATES("Templates"),; private final Schema schema; diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/TemplateRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/TemplateRestControllerRestDocTest.java new file mode 100644 index 000000000..6b98144ab --- /dev/null +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/TemplateRestControllerRestDocTest.java @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.restdoc; + +import static com.mercedesbenz.sechub.commons.model.template.TemplateDefinition.*; +import static com.mercedesbenz.sechub.restdoc.RestDocumentation.*; +import static com.mercedesbenz.sechub.test.RestDocPathParameter.*; +import static com.mercedesbenz.sechub.test.SecHubTestURLBuilder.*; +import static org.mockito.Mockito.*; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.lang.annotation.Annotation; +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.context.annotation.Profile; +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.template.TemplateDefinition; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition.TemplateVariable; +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition.TemplateVariableValidation; +import com.mercedesbenz.sechub.commons.model.template.TemplateType; +import com.mercedesbenz.sechub.docgen.util.RestDocFactory; +import com.mercedesbenz.sechub.domain.scan.template.TemplateRepository; +import com.mercedesbenz.sechub.domain.scan.template.TemplateRestController; +import com.mercedesbenz.sechub.domain.scan.template.TemplateService; +import com.mercedesbenz.sechub.sharedkernel.Profiles; +import com.mercedesbenz.sechub.sharedkernel.RoleConstants; +import com.mercedesbenz.sechub.sharedkernel.configuration.AbstractSecHubAPISecurityConfiguration; +import com.mercedesbenz.sechub.sharedkernel.logging.AuditLogService; +import com.mercedesbenz.sechub.sharedkernel.logging.LogSanitizer; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminCreatesOrUpdatesTemplate; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminDeletesTemplate; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesAllTemplateIds; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesTemplate; +import com.mercedesbenz.sechub.test.ExampleConstants; +import com.mercedesbenz.sechub.test.TestIsNecessaryForDocumentation; +import com.mercedesbenz.sechub.test.TestPortProvider; + +@RunWith(SpringRunner.class) +@WebMvcTest(TemplateRestController.class) +@ContextConfiguration(classes = { TemplateRestController.class, TemplateRestControllerRestDocTest.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 TemplateRestControllerRestDocTest implements TestIsNecessaryForDocumentation { + + private static final int PORT_USED = TestPortProvider.DEFAULT_INSTANCE.getRestDocTestPort(); + + @Autowired + private MockMvc mockMvc; + + @MockBean + private TemplateRepository tempplateRepository; + + @MockBean + TemplateService templateService; + + @MockBean + AuditLogService auditLogService; + + @MockBean + LogSanitizer logSanitizer; + + private TemplateDefinition definition; + + private TemplateVariable usernameVariable; + + private TemplateVariable passwordVariable; + + private static final String TEST_TEMPLATE_ID1 = "template1"; + private static final String TEST_TEMPLATE_ID2 = "template2"; + + @Before + public void before() { + definition = new TemplateDefinition(); + definition.setType(TemplateType.WEBSCAN_LOGIN); + definition.getAssets().add("asset-id1"); + + usernameVariable = new TemplateVariable(); + usernameVariable.setName("username"); + TemplateVariableValidation usernameValidation = new TemplateVariableValidation(); + usernameValidation.setMinLength(3); + usernameValidation.setMaxLength(15); + usernameValidation.setRegularExpression("[a-zA-Z0-9_-].*"); + + usernameVariable.setValidation(usernameValidation); + + passwordVariable = new TemplateVariable(); + passwordVariable.setName("password"); + TemplateVariableValidation passwordValidation = new TemplateVariableValidation(); + passwordValidation.setMaxLength(20); + + passwordVariable.setValidation(passwordValidation); + + definition.getVariables().add(usernameVariable); + } + + @Test + @UseCaseRestDoc(useCase = UseCaseAdminCreatesOrUpdatesTemplate.class) + public void restdoc_admin_creates_or_updates_template() throws Exception { + /* prepare */ + String apiEndpoint = https(PORT_USED).buildUserCreatesOrUpdatesTemplate(TEMPLATE_ID.pathElement()); + Class useCase = UseCaseAdminCreatesOrUpdatesTemplate.class; + + String content = definition.toFormattedJSON(); + + /* execute + test @formatter:off */ + this.mockMvc.perform( + post(apiEndpoint, TEST_TEMPLATE_ID1). + contentType(MediaType.APPLICATION_JSON_VALUE). + content(content). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(RestDocFactory.extractTag(apiEndpoint)). + requestSchema(OpenApiSchema.TEMPLATES.getSchema()). + and(). + document( + requestFields( + fieldWithPath(PROPERTY_TYPE).description("The template type. Currently supported types are: "+ TemplateType.values()), + + fieldWithPath(PROPERTY_ASSETS).description("An array list containing ids of referenced assets"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_NAME).description("The variable name"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_OPTIONAL).optional().description("Defines if the variable is optional. The default is false"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_VALIDATION).optional().description("Defines a simple validation segment."), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_VALIDATION+"."+ TemplateVariableValidation.PROPERTY_MIN_LENGTH).optional().description("The minimum content length of this variable"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_VALIDATION+"."+ TemplateVariableValidation.PROPERTY_MAX_LENGTH).optional().description("The maximum content length of this variable"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_VALIDATION+"."+ TemplateVariableValidation.PROPERTY_REGULAR_EXPRESSION).optional().description("A regular expression which must match to accept the user input inside the variable") + ), + pathParameters( + parameterWithName(TEMPLATE_ID.paramName()).description("The (unique) template id") + ) + )); + + /* @formatter:on */ + } + + @Test + @UseCaseRestDoc(useCase = UseCaseAdminDeletesTemplate.class) + public void restdoc_admin_deletes_template() throws Exception { + /* prepare */ + String apiEndpoint = https(PORT_USED).buildUserDeletesTemplate(TEMPLATE_ID.pathElement()); + Class useCase = UseCaseAdminDeletesTemplate.class; + + /* execute + test @formatter:off */ + this.mockMvc.perform( + delete(apiEndpoint, TEST_TEMPLATE_ID1). + contentType(MediaType.APPLICATION_JSON_VALUE). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(RestDocFactory.extractTag(apiEndpoint)). + requestSchema(OpenApiSchema.TEMPLATES.getSchema()). + and(). + document( + pathParameters( + parameterWithName(TEMPLATE_ID.paramName()).description("The (unique) template id") + ) + )); + + /* @formatter:on */ + } + + @Test + @UseCaseRestDoc(useCase = UseCaseAdminFetchesTemplate.class) + public void restdoc_admin_fetches_template() throws Exception { + /* prepare */ + definition.setId(TEST_TEMPLATE_ID1); // to have this in result as well, for create/delete it was not necessary, but + // here we want it + when(templateService.fetchTemplate(TEST_TEMPLATE_ID1)).thenReturn(definition); + + String apiEndpoint = https(PORT_USED).buildUserFetchesTemplate(TEMPLATE_ID.pathElement()); + Class useCase = UseCaseAdminFetchesTemplate.class; + + /* execute + test @formatter:off */ + this.mockMvc.perform( + get(apiEndpoint, TEST_TEMPLATE_ID1). + contentType(MediaType.APPLICATION_JSON_VALUE). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(RestDocFactory.extractTag(apiEndpoint)). + requestSchema(OpenApiSchema.TEMPLATES.getSchema()). + and(). + document( + responseFields( + fieldWithPath(PROPERTY_TYPE).description("The template type. Currently supported types are: "+ TemplateType.values()), + + fieldWithPath(PROPERTY_ID).description("The (unique) template id"), + fieldWithPath(PROPERTY_ASSETS).description("An array list containing ids of referenced assets"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_NAME).description("The variable name"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_OPTIONAL).optional().description("Defines if the variable is optional. The default is false"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_VALIDATION).optional().description("Defines a simple validation segment."), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_VALIDATION+"."+ TemplateVariableValidation.PROPERTY_MIN_LENGTH).optional().description("The minimum content length of this variable"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_VALIDATION+"."+ TemplateVariableValidation.PROPERTY_MAX_LENGTH).optional().description("The maximum content length of this variable"), + fieldWithPath(PROPERTY_VARIABLES+"[]."+ TemplateVariable.PROPERTY_VALIDATION+"."+ TemplateVariableValidation.PROPERTY_REGULAR_EXPRESSION).optional().description("A regular expression which must match to accept the user input inside the variable") + ), + pathParameters( + parameterWithName(TEMPLATE_ID.paramName()).description("The (unique) template id") + ) + )); + + /* @formatter:on */ + } + + @Test + @UseCaseRestDoc(useCase = UseCaseAdminFetchesAllTemplateIds.class) + public void restdoc_admin_fetches_templatelist() throws Exception { + /* prepare */ + when(templateService.fetchAllTemplateIds()).thenReturn(List.of(TEST_TEMPLATE_ID1, TEST_TEMPLATE_ID2)); + + String apiEndpoint = https(PORT_USED).buildUserFetchesTemplateList(); + Class useCase = UseCaseAdminFetchesAllTemplateIds.class; + + /* execute + test @formatter:off */ + this.mockMvc.perform( + get(apiEndpoint, TEST_TEMPLATE_ID1). + contentType(MediaType.APPLICATION_JSON_VALUE). + header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue()) + ). + andExpect(status().isOk()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(RestDocFactory.extractTag(apiEndpoint)). + requestSchema(OpenApiSchema.TEMPLATES.getSchema()). + and(). + document( + responseFields( + fieldWithPath("[]").description("Array contains all existing template identifiers") + ) + )); + + /* @formatter:on */ + } + + @Profile(Profiles.TEST) + @EnableAutoConfiguration + public static class SimpleTestConfiguration extends AbstractSecHubAPISecurityConfiguration { + + } + +} diff --git a/sechub-openapi-java/src/main/resources/openapi.yaml b/sechub-openapi-java/src/main/resources/openapi.yaml index 316395f78..a69246ba5 100644 --- a/sechub-openapi-java/src/main/resources/openapi.yaml +++ b/sechub-openapi-java/src/main/resources/openapi.yaml @@ -1982,8 +1982,53 @@ components: $ref: '#/components/schemas/SecHubJobInfoForUser' projectId: type: string - - + + TemplateType: + title: TemplateType + type: string + enum: + - WEBSCAN_LOGIN + + TemplateVariableValidation: + title: TemplateVariableValidation + type: object + properties: + minLength: + type: integer + format: int32 + maxLength: + type: integer + format: int32 + regularExpression: + type: string + + TemplateVariable: + title: TemplateVariable + type: object + properties: + name: + type: string + optional: + type: boolean + validation: + $ref: '#/components/schemas/TemplateVariableValidation' + + TemplateDefinition: + title: TemplateDefinition + type: object + properties: + type: + $ref: '#/components/schemas/TemplateType' + assets: + type: array + items: + type: string + variables: + type: array + items: + $ref: '#/components/schemas/TemplateVariable' + + security: - basicAuth: [ ] @@ -3736,3 +3781,94 @@ paths: x-content-type: application/json tags: - Other + + /api/admin/template/{templateId}: + post: + summary: Admin creates or updates a template + description: An administrator wants to create a new template or to update a template definition + operationId: adminCreateOrUpdateTemplate + parameters: + - name: templateId + description: The template id + in: path + required: true + schema: + type: string + responses: + "200": + description: "Ok" + "404": + description: "Not found" + "406": + description: "Not acceptable" + tags: + - Configuration + + delete: + summary: Admin deletes a template + description: An administrator wants to delete an existing template + operationId: adminDeleteTemplate + parameters: + - name: templateId + description: The template id + in: path + required: true + schema: + type: string + responses: + "200": + description: "Ok" + "404": + description: "Not found" + tags: + - Configuration + + get: + summary: Admin fetches template + description: An administrator wants to fetch the template definition by template id + operationId: adminFetchTemplate + parameters: + - name: templateId + description: The template id + in: path + required: true + schema: + type: string + responses: + "200": + description: "Ok" + content: + application/json: + schema: + $ref: '#/components/schemas/TemplateDefinition' + "404": + description: "Not found" + tags: + - Configuration + + /api/admin/templates: + get: + summary: Admin fetches template ids + description: An administrator wants to fetch a list containing all available template identifiers + operationId: adminFetchTemplateIds + parameters: + - name: templateId + description: The template id + in: path + required: true + schema: + type: string + responses: + "200": + description: "Ok" + content: + application/json;charset=UTF-8: + schema: + type: array + items: + type: string + "404": + description: "Not found" + tags: + - Configuration + \ No newline at end of file diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/Template.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/Template.java new file mode 100644 index 000000000..c55a0afbd --- /dev/null +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/Template.java @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.template; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +/** + * Represents a template + * + * @author Albert Tregnaghi + * + */ +@Entity +@Table(name = Template.TABLE_NAME) +public class Template { + + /* +-----------------------------------------------------------------------+ */ + /* +............................ SQL ......................................+ */ + /* +-----------------------------------------------------------------------+ */ + public static final String TABLE_NAME = "SCAN_TEMPLATE"; + + public static final String COLUMN_TEMPLATE_ID = "TEMPLATE_ID"; + public static final String COLUMN_DEFINITION = "TEMPLATE_DEFINITION"; + + /* +-----------------------------------------------------------------------+ */ + /* +............................ JPQL .....................................+ */ + /* +-----------------------------------------------------------------------+ */ + public static final String CLASS_NAME = "Template"; + + public static final String PROPERTY_ID = "id"; + public static final String PROPERTY_DEFINITION = "definition"; + + public static final String QUERY_All_TEMPLATE_IDS = "select t.id from #{#entityName} t"; + + @Id + @Column(name = COLUMN_TEMPLATE_ID, updatable = false, nullable = false) + String id; + + @Column(name = COLUMN_DEFINITION) + String definition; + + @Version + @Column(name = "VERSION") + @JsonIgnore + Integer version; + + Template() { + // jpa only + } + + public Template(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public void setDefinition(String definition) { + this.definition = definition; + } + + public String getDefinition() { + return definition; + } + +} diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateRepository.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateRepository.java new file mode 100644 index 000000000..3f5ceeaf8 --- /dev/null +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateRepository.java @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.template; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface TemplateRepository extends JpaRepository { + + @Query(Template.QUERY_All_TEMPLATE_IDS) + List findAllTemplateIds(); +} diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateRestController.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateRestController.java new file mode 100644 index 000000000..e48a82cf7 --- /dev/null +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateRestController.java @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.template; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; +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.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import com.mercedesbenz.sechub.commons.model.template.TemplateDefinition; +import com.mercedesbenz.sechub.sharedkernel.APIConstants; +import com.mercedesbenz.sechub.sharedkernel.RoleConstants; +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.logging.AuditLogService; +import com.mercedesbenz.sechub.sharedkernel.logging.LogSanitizer; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminCreatesOrUpdatesTemplate; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminDeletesTemplate; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesAllTemplateIds; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesTemplate; + +import jakarta.annotation.security.RolesAllowed; + +@RestController +@EnableAutoConfiguration +@RequestMapping(APIConstants.API_ADMINISTRATION) +@RolesAllowed({ RoleConstants.ROLE_SUPERADMIN }) +public class TemplateRestController { + + @Autowired + TemplateService templateService; + + @Autowired + AuditLogService auditLogService; + + @Autowired + LogSanitizer logSanitizer; + + @UseCaseAdminCreatesOrUpdatesTemplate(@Step(number = 1, next = 2, name = "REST API call to create or update template", needsRestDoc = true)) + @RequestMapping(path = "/template/{templateId}", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.OK) + public void createOrUpdate(@RequestBody TemplateDefinition templateDefinition, @PathVariable("templateId") String templateId) { + + auditLogService.log("starts create/update of template: {}", logSanitizer.sanitize(templateId, -1)); + + templateService.createOrUpdateTemplate(templateId, templateDefinition); + + } + + @UseCaseAdminDeletesTemplate(@Step(number = 1, next = 2, name = "REST API call to delete a template", needsRestDoc = true)) + @RequestMapping(path = "/template/{templateId}", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.OK) + public void delete(@PathVariable("templateId") String templateId) { + + auditLogService.log("starts delete of template: {}", logSanitizer.sanitize(templateId, -1)); + + templateService.deleteTemplate(templateId); + + } + + @UseCaseAdminFetchesTemplate(@Step(number = 1, next = 2, name = "REST API call to fetch template", needsRestDoc = true)) + @RequestMapping(path = "/template/{templateId}", method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public TemplateDefinition fetchTemplate(@PathVariable("templateId") String templateId) { + + auditLogService.log("fetches template definition for template: {}", logSanitizer.sanitize(templateId, -1)); + + return templateService.fetchTemplate(templateId); + + } + + @UseCaseAdminFetchesAllTemplateIds(@Step(number = 1, next = 2, name = "REST API call to fetch template list", needsRestDoc = true)) + @RequestMapping(path = "/templates", method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public List fetchAllTemplateIds() { + return templateService.fetchAllTemplateIds(); + + } +} diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateService.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateService.java new file mode 100644 index 000000000..2e81ffab9 --- /dev/null +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/template/TemplateService.java @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.domain.scan.template; + +import java.util.List; +import java.util.Optional; + +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.template.TemplateDefinition; +import com.mercedesbenz.sechub.sharedkernel.RoleConstants; +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.error.NotFoundException; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminCreatesOrUpdatesTemplate; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminDeletesTemplate; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesAllTemplateIds; +import com.mercedesbenz.sechub.sharedkernel.usecases.admin.config.UseCaseAdminFetchesTemplate; + +import jakarta.annotation.security.RolesAllowed; + +@Service +@RolesAllowed(RoleConstants.ROLE_SUPERADMIN) +public class TemplateService { + + private static final Logger LOG = LoggerFactory.getLogger(TemplateService.class); + + private TemplateRepository repository; + + TemplateService(@Autowired TemplateRepository repository) { + this.repository = repository; + } + + @UseCaseAdminCreatesOrUpdatesTemplate(@Step(number = 2, name = "Service creates or updates template")) + public void createOrUpdateTemplate(String templateId, TemplateDefinition templateDefinition) { + if (templateId == null) { + throw new IllegalArgumentException("Template id may not be null!"); + } + if (templateDefinition == null) { + throw new IllegalArgumentException("Template definition may not be null!"); + } + + // first of all we always set template id, so we have never a clash here + // even when somebody copied an existing template definition. + templateDefinition.setId(templateId); + + Optional