From e91513ecbd7ec9439be6fc8a0947c437e84e6e6e Mon Sep 17 00:00:00 2001 From: "Wendland, Florian" <florian.wendland@aisec.fraunhofer.de> Date: Mon, 20 Nov 2023 12:00:52 +0100 Subject: [PATCH] Prepare release 1.6.0 --- .dockerignore | 6 +- .gitignore | 3 +- Dockerfile | 53 +- README.md | 101 +- build.gradle.kts | 126 +- docs/mapping.md | 67 + docs/medina-rules.md | 32 + gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 35 +- gradlew.bat | 1 + local.Dockerfile | 26 + mark/bc-jca/Cipher.mark | 143 ++ .../bc-jca}/KeyAgreement.mark | 26 + mark/bc-jca/LICENSE | 202 +++ .../bouncycastle => mark/bc-jca}/Mac.mark | 44 +- mark/bc-jca/MessageDigest.mark | 64 + mark/bc-jca/README.md | 8 + .../bc-jca}/SecureRandom.mark | 41 +- mark/bc-jca/Signature.mark | 115 ++ mark/bc-jca/findingDescription.json | 92 ++ mark/bc-jca/mapping.yaml | 41 + mark/bc-jca/rules.mark | 402 ++++++ mark/bc-jsse/LICENSE | 202 +++ mark/bc-jsse/README.md | 4 +- mark/bc-jsse/SSLContext.mark | 28 + mark/bc-jsse/SSLServerSocket.mark | 28 + mark/bc-jsse/SSLServerSocketFactory.mark | 28 + mark/bc-jsse/findingDescription.json | 32 + mark/bc-jsse/mapping.yaml | 146 ++ mark/bc-jsse/rules.mark | 89 +- settings.gradle.kts | 2 +- .../aisec/codyze/medina/CodyzeMedina.kt | 144 -- .../aisec/codyze/medina/Connection.kt | 329 ----- .../codyze/medina/assembling/Assembler.kt | 199 +++ .../codyze/medina/assembling/Environment.kt | 154 +++ .../aisec/codyze/medina/auth/Auth.kt | 104 -- .../codyze/medina/connection/ApiManager.kt | 162 +++ .../codyze/medina/connection/Connection.kt | 85 ++ .../codyze/medina/connection/OAuthManager.kt | 114 ++ .../codyze/medina/connection/TokenManager.kt | 114 ++ .../medina/evaluation/EvaluationResult.kt | 54 + .../codyze/medina/evaluation/Evaluator.kt | 142 ++ .../aisec/codyze/medina/evaluation/Rule.kt | 70 + .../base/ApprovedCommitAuthorEvaluator.kt | 67 + .../evaluation/base/GPGSignatureEvaluator.kt | 103 ++ .../medina/evaluation/base/MetricEvaluator.kt | 114 ++ .../evaluation/base/SignOffEvaluator.kt | 84 ++ .../evaluation/base/SignedSignOffEvaluator.kt | 141 ++ .../aisec/codyze/medina/main/CodyzeMedina.kt | 428 ++++++ .../codyze/medina/{ => main}/Configuration.kt | 113 +- .../aisec/codyze/medina/main/MarkResolver.kt | 82 ++ .../aisec/codyze/medina/mapping/Mapping.kt | 57 + .../codyze/medina/mapping/MappingTree.kt | 115 ++ .../codyze/medina/mapping/MedinaMapping.kt | 41 + .../aisec/codyze/medina/util/Assemblers.kt | 87 -- .../aisec/codyze/medina/util/Environment.kt | 101 -- .../aisec/codyze/medina/util/MedinaSarif.kt | 157 +++ .../aisec/codyze/medina/util/Util.kt | 122 +- src/main/resources/evidence.yaml | 47 +- src/main/resources/log4j.properties | 9 + src/main/resources/log4j2.xml | 2 +- src/main/resources/orchestrator.yaml | 1214 +++++++++++++++-- .../aisec/codyze/medina/AuthTest.kt | 11 +- .../fraunhofer/aisec/codyze/medina/CliTest.kt | 37 + .../codyze/medina/CodyzeCliPassThroughTest.kt | 10 +- .../aisec/codyze/medina/ConfigurationTest.kt | 2 + .../aisec/codyze/medina/ConverterTest.kt | 111 ++ .../aisec/codyze/medina/DemoTest.kt | 1 + .../aisec/codyze/medina/IntegrationTest.kt | 161 +++ .../aisec/codyze/medina/LoggingTest.kt | 87 ++ .../aisec/codyze/medina/MappingTreeTest.kt | 54 + .../aisec/codyze/medina/MedinaMetrikTest.kt | 46 + .../aisec/codyze/medina/ParserTest.kt | 95 -- .../aisec/codyze/medina/SarifTest.kt | 122 ++ .../codyze/medina/assembling/AssemblerTest.kt | 180 +++ src/test/resources/Mark/botan/mapping.yaml | 29 + .../AlgorithmParameterGenerator.mark | 43 - .../bouncycastle/AlgorithmParameters.mark | 34 - .../Mark/bouncycastle/CertPathBuilder.mark | 24 - .../Mark/bouncycastle/CertPathValidator.mark | 28 - .../Mark/bouncycastle/CertStore.mark | 34 - .../Mark/bouncycastle/CertificateFactory.mark | 56 - .../bouncycastle/ChaCha20ParameterSpec.mark | 14 - .../Mark/bouncycastle/DHGenParameterSpec.mark | 15 - .../Mark/bouncycastle/DHParameterSpec.mark | 22 - .../Mark/bouncycastle/DHPrivateKeySpec.mark | 16 - .../Mark/bouncycastle/DHPublicKeySpec.mark | 16 - .../bouncycastle/DSAGenParameterSpec.mark | 22 - .../Mark/bouncycastle/DSAParameterSpec.mark | 17 - .../Mark/bouncycastle/DSAPrivateKeySpec.mark | 18 - .../Mark/bouncycastle/DSAPublicKeySpec.mark | 18 - .../Mark/bouncycastle/ECFieldF2m.mark | 22 - .../Mark/bouncycastle/ECFieldFp.mark | 13 - .../Mark/bouncycastle/ECGenParameterSpec.mark | 14 - .../resources/Mark/bouncycastle/ECKey.mark | 5 - .../Mark/bouncycastle/ECParameterSpec.mark | 19 - .../resources/Mark/bouncycastle/ECPoint.mark | 14 - .../Mark/bouncycastle/ECPrivateKeySpec.mark | 16 - .../Mark/bouncycastle/ECPublicKeySpec.mark | 14 - .../Mark/bouncycastle/EncodedKeySpec.mark | 18 - .../Mark/bouncycastle/GCMParameterSpec.mark | 22 - .../Mark/bouncycastle/HMACParameterSpec.mark | 12 - .../Mark/bouncycastle/IvParameterSpec.mark | 19 - .../Mark/bouncycastle/KeyFactory.mark | 31 - .../Mark/bouncycastle/KeyGenerator.mark | 40 - .../resources/Mark/bouncycastle/KeyPair.mark | 14 - .../Mark/bouncycastle/KeyPairGenerator.mark | 46 - .../KeyStore.PasswordProtection.mark | 19 - .../resources/Mark/bouncycastle/KeyStore.mark | 60 - .../Mark/bouncycastle/MGF1ParameterSpec.mark | 12 - .../Mark/bouncycastle/MessageDigest.mark | 39 - .../Mark/bouncycastle/OAEPParameterSpec.mark | 19 - .../resources/Mark/bouncycastle/PBEKey.mark | 5 - .../Mark/bouncycastle/PBEKeySpec.mark | 26 - .../Mark/bouncycastle/PBEParameterSpec.mark | 20 - .../bouncycastle/PKCS8EncodedKeySpec.mark | 18 - .../Mark/bouncycastle/PSSParameterSpec.mark | 23 - .../Mark/bouncycastle/PrivateKey.mark | 5 - .../Mark/bouncycastle/PublicKey.mark | 5 - .../bouncycastle/RSAKeyGenParameterSpec.mark | 20 - .../RSAMultiPrimePrivateCrtKeySpec.mark | 42 - .../bouncycastle/RSAPrivateCrtKeySpec.mark | 38 - .../Mark/bouncycastle/RSAPrivateKeySpec.mark | 21 - .../Mark/bouncycastle/RSAPublicKeySpec.mark | 21 - .../Mark/bouncycastle/RulesBase_Cipher.mark | 50 - .../Mark/bouncycastle/RulesTR_Cipher.mark | 371 ----- .../RulesTR_InstanceAuthentication.txt | 8 - .../bouncycastle/RulesTR_KeyAgreement.txt | 10 - .../Mark/bouncycastle/RulesTR_MAC.mark | 187 --- .../bouncycastle/RulesTR_MessageDigest.mark | 17 - .../Mark/bouncycastle/RulesTR_PRNG.txt | 10 - .../Mark/bouncycastle/RulesTR_Signature.mark | 81 -- .../bouncycastle/Rules_BouncyCastleJCA.mark | 223 --- .../Mark/bouncycastle/SecretKey.mark | 5 - .../Mark/bouncycastle/SecretKeyFactory.mark | 35 - .../Mark/bouncycastle/SecretKeySpec.mark | 23 - .../Mark/bouncycastle/Signature.mark | 89 -- .../Mark/bouncycastle/X509EncodedKeySpec.mark | 18 - .../Mark/bouncycastle/XECPrivateKeySpec.mark | 14 - .../Mark/bouncycastle/XECPublicKeySpec.mark | 14 - src/test/resources/Mark/exampleMapping.yaml | 20 + .../resources/Mark/jackson/ObjectMapper.mark | 12 - src/test/resources/codyze.yaml | 12 +- .../resources/demos/bcjsse/TlsServer.java | 146 -- src/test/resources/log4j2.xml | 19 - .../botan/bt1/block_ciphers.mark | 20 + .../botan/bt1/mapping.yaml | 11 + .../botan/bt2/hash.mark | 69 + .../botan/bt2/mapping.yaml | 11 + .../botan/cipher_mode.mark | 107 ++ .../botan/mapping.yaml | 13 + .../bc1}/BouncyCastleProvider.mark | 0 .../bouncycastle/bc1/mapping.yaml | 11 + .../bouncycastle/bc2}/Cipher.mark | 0 .../bouncycastle/bc2/mapping.yaml | 11 + templates/codyze-medina-metrics.yaml | 16 + templates/codyze-medina.yaml | 32 + 158 files changed, 6995 insertions(+), 3651 deletions(-) create mode 100644 docs/mapping.md create mode 100644 docs/medina-rules.md create mode 100644 local.Dockerfile create mode 100644 mark/bc-jca/Cipher.mark rename {src/test/resources/Mark/bouncycastle => mark/bc-jca}/KeyAgreement.mark (61%) create mode 100644 mark/bc-jca/LICENSE rename {src/test/resources/Mark/bouncycastle => mark/bc-jca}/Mac.mark (52%) create mode 100644 mark/bc-jca/MessageDigest.mark create mode 100644 mark/bc-jca/README.md rename {src/test/resources/Mark/bouncycastle => mark/bc-jca}/SecureRandom.mark (64%) create mode 100644 mark/bc-jca/Signature.mark create mode 100644 mark/bc-jca/findingDescription.json create mode 100644 mark/bc-jca/mapping.yaml create mode 100644 mark/bc-jca/rules.mark create mode 100644 mark/bc-jsse/LICENSE create mode 100644 mark/bc-jsse/findingDescription.json create mode 100644 mark/bc-jsse/mapping.yaml delete mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/CodyzeMedina.kt delete mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/Connection.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/assembling/Assembler.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/assembling/Environment.kt delete mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/auth/Auth.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/connection/ApiManager.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/connection/Connection.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/connection/OAuthManager.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/connection/TokenManager.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/EvaluationResult.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/Evaluator.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/Rule.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/ApprovedCommitAuthorEvaluator.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/GPGSignatureEvaluator.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/MetricEvaluator.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/SignOffEvaluator.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/SignedSignOffEvaluator.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/main/CodyzeMedina.kt rename src/main/kotlin/de/fraunhofer/aisec/codyze/medina/{ => main}/Configuration.kt (69%) create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/main/MarkResolver.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/mapping/Mapping.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/mapping/MappingTree.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/mapping/MedinaMapping.kt delete mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/util/Assemblers.kt delete mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/util/Environment.kt create mode 100644 src/main/kotlin/de/fraunhofer/aisec/codyze/medina/util/MedinaSarif.kt create mode 100644 src/main/resources/log4j.properties create mode 100644 src/test/kotlin/de/fraunhofer/aisec/codyze/medina/ConverterTest.kt create mode 100644 src/test/kotlin/de/fraunhofer/aisec/codyze/medina/IntegrationTest.kt create mode 100644 src/test/kotlin/de/fraunhofer/aisec/codyze/medina/LoggingTest.kt create mode 100644 src/test/kotlin/de/fraunhofer/aisec/codyze/medina/MappingTreeTest.kt create mode 100644 src/test/kotlin/de/fraunhofer/aisec/codyze/medina/MedinaMetrikTest.kt delete mode 100644 src/test/kotlin/de/fraunhofer/aisec/codyze/medina/ParserTest.kt create mode 100644 src/test/kotlin/de/fraunhofer/aisec/codyze/medina/SarifTest.kt create mode 100644 src/test/kotlin/de/fraunhofer/aisec/codyze/medina/assembling/AssemblerTest.kt create mode 100644 src/test/resources/Mark/botan/mapping.yaml delete mode 100644 src/test/resources/Mark/bouncycastle/AlgorithmParameterGenerator.mark delete mode 100644 src/test/resources/Mark/bouncycastle/AlgorithmParameters.mark delete mode 100644 src/test/resources/Mark/bouncycastle/CertPathBuilder.mark delete mode 100644 src/test/resources/Mark/bouncycastle/CertPathValidator.mark delete mode 100644 src/test/resources/Mark/bouncycastle/CertStore.mark delete mode 100644 src/test/resources/Mark/bouncycastle/CertificateFactory.mark delete mode 100644 src/test/resources/Mark/bouncycastle/ChaCha20ParameterSpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/DHGenParameterSpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/DHParameterSpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/DHPrivateKeySpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/DHPublicKeySpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/DSAGenParameterSpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/DSAParameterSpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/DSAPrivateKeySpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/DSAPublicKeySpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/ECFieldF2m.mark delete mode 100644 src/test/resources/Mark/bouncycastle/ECFieldFp.mark delete mode 100644 src/test/resources/Mark/bouncycastle/ECGenParameterSpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/ECKey.mark delete mode 100644 src/test/resources/Mark/bouncycastle/ECParameterSpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/ECPoint.mark delete mode 100644 src/test/resources/Mark/bouncycastle/ECPrivateKeySpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/ECPublicKeySpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/EncodedKeySpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/GCMParameterSpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/HMACParameterSpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/IvParameterSpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/KeyFactory.mark delete mode 100644 src/test/resources/Mark/bouncycastle/KeyGenerator.mark delete mode 100644 src/test/resources/Mark/bouncycastle/KeyPair.mark delete mode 100644 src/test/resources/Mark/bouncycastle/KeyPairGenerator.mark delete mode 100644 src/test/resources/Mark/bouncycastle/KeyStore.PasswordProtection.mark delete mode 100644 src/test/resources/Mark/bouncycastle/KeyStore.mark delete mode 100644 src/test/resources/Mark/bouncycastle/MGF1ParameterSpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/MessageDigest.mark delete mode 100644 src/test/resources/Mark/bouncycastle/OAEPParameterSpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/PBEKey.mark delete mode 100644 src/test/resources/Mark/bouncycastle/PBEKeySpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/PBEParameterSpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/PKCS8EncodedKeySpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/PSSParameterSpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/PrivateKey.mark delete mode 100644 src/test/resources/Mark/bouncycastle/PublicKey.mark delete mode 100644 src/test/resources/Mark/bouncycastle/RSAKeyGenParameterSpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/RSAMultiPrimePrivateCrtKeySpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/RSAPrivateCrtKeySpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/RSAPrivateKeySpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/RSAPublicKeySpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/RulesBase_Cipher.mark delete mode 100644 src/test/resources/Mark/bouncycastle/RulesTR_Cipher.mark delete mode 100644 src/test/resources/Mark/bouncycastle/RulesTR_InstanceAuthentication.txt delete mode 100644 src/test/resources/Mark/bouncycastle/RulesTR_KeyAgreement.txt delete mode 100644 src/test/resources/Mark/bouncycastle/RulesTR_MAC.mark delete mode 100644 src/test/resources/Mark/bouncycastle/RulesTR_MessageDigest.mark delete mode 100644 src/test/resources/Mark/bouncycastle/RulesTR_PRNG.txt delete mode 100644 src/test/resources/Mark/bouncycastle/RulesTR_Signature.mark delete mode 100644 src/test/resources/Mark/bouncycastle/Rules_BouncyCastleJCA.mark delete mode 100644 src/test/resources/Mark/bouncycastle/SecretKey.mark delete mode 100644 src/test/resources/Mark/bouncycastle/SecretKeyFactory.mark delete mode 100644 src/test/resources/Mark/bouncycastle/SecretKeySpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/Signature.mark delete mode 100644 src/test/resources/Mark/bouncycastle/X509EncodedKeySpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/XECPrivateKeySpec.mark delete mode 100644 src/test/resources/Mark/bouncycastle/XECPublicKeySpec.mark create mode 100644 src/test/resources/Mark/exampleMapping.yaml delete mode 100644 src/test/resources/Mark/jackson/ObjectMapper.mark delete mode 100644 src/test/resources/demos/bcjsse/TlsServer.java delete mode 100644 src/test/resources/log4j2.xml create mode 100644 src/test/resources/mappingTreeTestStructure/botan/bt1/block_ciphers.mark create mode 100644 src/test/resources/mappingTreeTestStructure/botan/bt1/mapping.yaml create mode 100644 src/test/resources/mappingTreeTestStructure/botan/bt2/hash.mark create mode 100644 src/test/resources/mappingTreeTestStructure/botan/bt2/mapping.yaml create mode 100644 src/test/resources/mappingTreeTestStructure/botan/cipher_mode.mark create mode 100644 src/test/resources/mappingTreeTestStructure/botan/mapping.yaml rename src/test/resources/{Mark/bouncycastle => mappingTreeTestStructure/bouncycastle/bc1}/BouncyCastleProvider.mark (100%) create mode 100644 src/test/resources/mappingTreeTestStructure/bouncycastle/bc1/mapping.yaml rename src/test/resources/{Mark/bouncycastle => mappingTreeTestStructure/bouncycastle/bc2}/Cipher.mark (100%) create mode 100644 src/test/resources/mappingTreeTestStructure/bouncycastle/bc2/mapping.yaml create mode 100644 templates/codyze-medina-metrics.yaml create mode 100644 templates/codyze-medina.yaml diff --git a/.dockerignore b/.dockerignore index efffb91..13fb473 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1 @@ -# exclude everything -* - -# include binary distribution archives -!build/distributions/ +# exclude nothing diff --git a/.gitignore b/.gitignore index 62d8458..7c909f4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,7 @@ .secret #logging -crymlin.log -medina-codyze.log +*.log # Mobile Tools for Java (J2ME) .mtj.tmp/ diff --git a/Dockerfile b/Dockerfile index d9ead25..5cfe4fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,44 @@ -FROM openjdk:11-jre-slim +# Builder +FROM eclipse-temurin:11.0.20.1_1-jdk AS builder +# required for Codyze MEDINA functionalities -> git, gpg RUN apt-get update && apt-get -y --no-install-recommends install \ - unzip \ - wget + git \ + gpg \ + && rm -rf /var/lib/apt/lists/* -# add distribution -ADD build/distributions/codyze-*.tar /usr/local/lib/ -# add links for ease of use -RUN ln -s /usr/local/lib/codyze-*/bin/codyze /usr/local/bin/ \ - && ln -s /usr/local/lib/codyze-* /codyze +WORKDIR /codyze-medina -RUN wget "https://github.com/Fraunhofer-AISEC/codyze/archive/refs/heads/main.zip" \ - && unzip main.zip \ - && mv codyze-main/src/dist/mark/ /codyze/mark/ \ - && rm -rf main.zip codyze-main +# files for build filtered by .dockerignore +ADD . . +RUN ./gradlew --parallel \ + generateAll \ + && ./gradlew --parallel \ + build \ + installDist -# working location with sources to be analyzed -WORKDIR /source + +# Executable image +FROM eclipse-temurin:11.0.20.1_1-jre + +LABEL org.opencontainers.image.authors="Fraunhofer AISEC <codyze@aisec.fraunhofer.de>" +LABEL org.opencontainers.image.vendor="Fraunhofer AISEC" +LABEL org.opencontainers.image.licenses="Apache-2.0" +LABEL org.opencontainers.image.title="Codyze for MEDINA" +LABEL org.opencontainers.image.description="Compliance checks for source code using Codyze. Adjusted for MEDINA framework." + +COPY --from=builder /codyze-medina/build/install/ / +RUN ln -st /usr/local/bin/ /codyze-medina/bin/codyze-medina + +# required for Codyze MEDINA functionalities -> git, gpg +RUN apt-get update && apt-get -y --no-install-recommends install \ + git \ + gpg \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /project +# Add workdir to safe directories +RUN git config --global --add safe.directory /project # default entrypoint and parameters -ENTRYPOINT ["codyze"] -CMD ["-c"] +ENTRYPOINT ["codyze-medina"] \ No newline at end of file diff --git a/README.md b/README.md index be5a7b4..dfddf93 100644 --- a/README.md +++ b/README.md @@ -48,14 +48,22 @@ This makes Codyze for MEDINA available for immediate use in `./build/install/cod Codyze for MEDINA provides a CLI. The main options are: -| Option | Description | -|-----------------------|-------------------------------------------------------------| -| `--endpoint <URL>` | URL of Orchestrator endpoint | -| `--oauth-endpoint <URL>` | URL of OAuth endpoint | -| `--username <STRING> \| -u <STRING>` | username for OAuth | -| `--password <STRING> \| -p <STRING>` | password for OAuth | -| `--ci <STRING>` | CI environment being used (i.e., [GITLAB, GITHUB, JENKINS]) | -| `--config <FILE>` | configuration file for Codyze | +| Option | Default | Description | +|-------------------------------------|:--------------------------:|-----------------------------------------------------------------------------------------------------------------------------------------------| +| `--id <STRING>` | - | UUID of the analyzed cloud service | +| `--rules <FILE>` | codyze-medina-metrics.yaml | File specifying environmental MEDINA rules | +| `--medina-output <PATH>` | codyze-medina.sarif | File where MEDINA rule evaluations will be stored. In case `combined-output` is set to true, this is the location of the combined result file | +| `--combined-output <BOOLEAN>` | true | Whether the respective SARIF files created by Codyze and the MEDINA evaluation should be merged | +| `--key-location <PATH>` | public-keys/ | Location of public key files necessary to evaluate signatures | +| `--endpoint <URL>` | - | URL of Orchestrator endpoint | +| `--oauth-endpoint <URL>` | - | URL of OAuth endpoint | +| `--username <STRING> / -u <STRING>` | - | username for OAuth | +| `--password <STRING> / -p <STRING>` | - | password for OAuth | +| `--required <BOOLEAN>` | true | Whether analysis fails on Orchestrator connection issues. | +| `--mark-builtin <LIST<FILE>>` | [] | Builtin MARK files used in the analysis | +| `--mark-project <LIST<FILE>>` | [] | External MARK files used in the analysis | +| `--ci <STRING>` | NONE | CI environment being used (i.e., [GITLAB, GITHUB, JENKINS]). If set to `NONE`, The program will try to determine this at runtime. | +| `--config <FILE>` | codyze-medina.yaml | Configuration file for Codyze | In addition, Codyze for MEDINA passes CLI options along to Codyze v2. Options for Codyze v2 can be found in the documentation at [codyze.io](shttps://www.codyze.io/Getting%20Started/configuration/). @@ -71,24 +79,87 @@ The structure of a configuration file is: ```yaml orchestrator: + required: <BOOLEAN> endpoint: <URL> auth: oauth-endpoint: <URL> username: <STRING> password: <STRING> - + +mark: + builtin: + - <PATH> + - ... + project: + - <PATH> + - ... + +id: <STRING> +rules: <FILE> + +# ... additional optional parameters ... # ... additional parameters from original Codyze ... ``` +The MARK file paths are interpreted based on the location of the codyze distribution: +- The `builtin` segment represents included MARK rule groups found in `codyze-medina/mark`. Expected inputs are the names of the respective subdirectories. +- The `project` segment represents additional MARK rules provided by any other source. Note that they also need to include a valid mapping file to be considered during the analysis. + See the test resources for an example configuration file. It uses a locally running Orchestrator. +The configuration file should be located within the target project to comply with the following assumptions: + - All **relative paths** used in the configuration of Codyze for MEDINA are evaluated relative to the location of the configuration file. + - The specified MEDINA Metrics from the `rules` parameter are evaluated at the location of the configuration file. + ## Mapping Mapping from `Finding`s to `AssessmentResult`s is currently handled by the mapping file -`src/main/java/resoures/mappings.txt`. Its entries follow the -scheme `[findingId0:findingId1:...]->(metricId;isDefault;operator;targetValue;valueType)` -where `;` separates different parameters and `:` separates elements of a list. See the mapping file in this project for -an example. +`mappings.txt`. +Its entries follow this scheme: +``` +metrics: + - name: <Metric Name> + rules: + - <Required MARK Rule> + - <Required MARK Rule> + - ... + configuration: + default: <Whether Config is Default> + operator: <Comparator as String> + type: [STRING, NUMBER, BOOLEAN] + target: + - <Target Value> + - ... + - ... +``` + +All findings NOT mapped in such a file are ignored during the analysis. +> More information about the mapping file can be found in `docs/mapping.md` + +## Exit Codes + +Codyze for MEDINA currently returns one of the following exit codes depending on the situation: + +| Exit Code | Meaning | +|-----------|---------------------------------------------------------| +| **126** | Orchestrator Connection Failed | +| **3** | Unrecognized Cloud Service Id | +| **127** | General Execution Error (consult logs for more details) | +| **1** | Violations found during analysis | +| **0** | No violations or errors | + +Exit codes higher up in this list take priority over lower ones, e.g. a return code of 126 does not exclude violations within the analysis results. + +## Environment Variables + +The following environment variables can be used to additionally configure the execution: + +| Name | Effect | +|------------------|---------------------------------------------------------------------------------------------------------------------| +| CODYZE_PWD | Sets the orchestrator password. Overwrites configuration file entries but is overwritten by command line arguments. | +| CODYZE_LOG_LEVEL | Manually overrides the log level | + +Additionally, Codyze for MEDINA parses variables automatically set by the CI system to gain necessary information about the environment. -All findings NOT mapped in this file are ignored when sending results to the orchestrator (they will still be present in -the result file). \ No newline at end of file +> Codyze for MEDINA requires the target project to be within a Git repository. +> Additionally, GnuPG must be available in order to evaluate any signatures. diff --git a/build.gradle.kts b/build.gradle.kts index 5784922..84f7c97 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,22 +1,22 @@ import org.openapitools.generator.gradle.plugin.tasks.GenerateTask plugins { - java - kotlin("jvm") version "1.7.20" + kotlin("jvm") version "1.9.20" application - id("org.openapi.generator") version "6.2.0" + // generator for OpenAPI specs + id("org.openapi.generator") version "7.1.0" // code quality jacoco - id("com.diffplug.spotless") version "6.11.0" + id("com.diffplug.spotless") version "6.22.0" // documentation - id("org.jetbrains.dokka") version "1.7.20" + id("org.jetbrains.dokka") version "1.9.10" } group = "de.fraunhofer.aisec.codyze.medina" -version = "1.0.0" +version = "1.6.0" repositories { // JitPack for packages build directly from GitHub repos @@ -50,53 +50,55 @@ repositories { } dependencies { - // Java only - testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.1") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.1") + // testing + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.1") + testImplementation("org.mockito:mockito-junit-jupiter:5.7.0") // depend on executing the build.gradle of the generated openapi-projects api(project("generator_orchestrator")) api(project("generator_evidence")) - // codyze - api("com.github.Fraunhofer-AISEC:codyze:2.2.0") + // Codyze + implementation("com.github.Fraunhofer-AISEC:codyze:2.3.0") implementation( "com.github.Fraunhofer-AISEC.codyze-mark-eclipse-plugin:de.fraunhofer.aisec.mark:2.0.0:repackaged" ) // CLI using picocli - implementation("info.picocli:picocli:4.6.3") - annotationProcessor("info.picocli:picocli-codegen:4.6.3") + implementation("info.picocli:picocli:4.7.5") + annotationProcessor("info.picocli:picocli-codegen:4.7.5") // OAuth2 - api("org.dmfs:oauth2-essentials:0.18") - implementation("org.dmfs:httpurlconnection-executor:0.20") + implementation("org.dmfs:oauth2-essentials:0.22.1") + implementation("org.dmfs:httpurlconnection-executor:1.22.1") - // Logging - implementation("io.github.microutils:kotlin-logging-jvm:3.0.2") - implementation("org.slf4j:slf4j-simple:2.0.3") + // logging + implementation("io.github.oshai:kotlin-logging-jvm:5.1.0") + implementation("org.slf4j:slf4j-api:2.0.9") // required by `io.github.oshai:kotlin-logging-jvm` + implementation("org.apache.logging.log4j:log4j-core:2.21.1") + runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:2.21.1") - // additional dependencies - implementation("com.fasterxml.jackson.core:jackson-databind:2.13.4.2") - implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.4") + // YAML configuration files + implementation("com.fasterxml.jackson.core:jackson-databind:2.16.0") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.16.0") - // kotlin - implementation(kotlin("stdlib-jdk8")) + // SARIF Kotlin bindings + implementation("io.github.detekt.sarif4k:sarif4k:0.5.0") } -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(11)) - } +kotlin { + jvmToolchain(11) } application { - mainClass.set("de.fraunhofer.aisec.codyze.medina.CodyzeMedinaKt") + mainClass.set("de.fraunhofer.aisec.codyze.medina.main.CodyzeMedinaKt") } distributions { main { contents { + // copy over important files from("mark/") { into("mark/") } @@ -109,50 +111,62 @@ distributions { // license header used with spotless val license = """ - /* + // SPDX-License-Identifier: Apache-2.0 + + /* * Copyright (c) ${"$"}YEAR, Fraunhofer AISEC. All rights reserved. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * https://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * _____ _ - * / ____| | | - * | | ___ __| |_ _ _______ + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ * | | / _ \ / _` | | | |_ / _ \ * | |___| (_) | (_| | |_| |/ / __/ * \_____\___/ \__,_|\__, /___\___| - * __/ | - * |___/ + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. */ """.trimIndent() spotless { - ratchetFrom("origin/main") - java { - importOrder() - removeUnusedImports() - googleJavaFormat() - licenseHeader(license).yearSeparator("-") - } kotlin { ktfmt().kotlinlangStyle() licenseHeader(license).yearSeparator("-") } } +// Manually exclude all spotless-Tasks of the "generator_"-subprojects from the gradle task graph. +// This is a workaround to prevent the exceptions thrown from the outdated google-java-format specified there +gradle.taskGraph.whenReady { + if(hasTask(":spotlessJava") || hasTask(":spotlessJavaDiagnose")) { + allTasks.forEach { + for (project in subprojects) { + if(it.path.matches(":generator_.+:spotless.*".toRegex())) { + it.enabled = false + } + } + } + } +} + // generates an openapi-project from the `orchestrator.yaml` specification in the resources // directory tasks.register<GenerateTask>("generateOrchestrator") { inputSpec.set("$rootDir/src/main/resources/orchestrator.yaml") - outputDir.set("$buildDir/generator_orchestrator") + outputDir.set("${layout.buildDirectory.get().asFile}/generator_orchestrator") generatorName.set("java") apiPackage.set("org.openapitools.client.orchestrator.api") modelPackage.set("org.openapitools.client.orchestrator.model") @@ -162,7 +176,7 @@ tasks.register<GenerateTask>("generateOrchestrator") { // generates an openapi-project from the `evidence.yaml` specification in the resources directory tasks.register<GenerateTask>("generateEvidence") { inputSpec.set("$rootDir/src/main/resources/evidence.yaml") - outputDir.set("$buildDir/generator_evidence") + outputDir.set("${layout.buildDirectory.get().asFile}/generator_evidence") generatorName.set("java") apiPackage.set("org.openapitools.client.evidence.api") modelPackage.set("org.openapitools.client.evidence.model") @@ -175,7 +189,9 @@ tasks.register("generateAll") { dependsOn(tasks.named("generateEvidence")) } -tasks.named<Test>("test") { useJUnitPlatform() } +tasks.named<Test>("test") { + useJUnitPlatform() +} tasks.test { finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run @@ -191,3 +207,19 @@ tasks.jacocoTestReport { tasks.dokkaHtml.configure { outputDirectory.set(rootDir.resolve("docs")) } + +tasks { + jar { + from("LICENSE") { + into("META-INF/") + } + // add name and version from gradle configuration to jar manifest + manifest { + attributes( + "Implementation-Title" to rootProject.name, + "Implementation-Version" to rootProject.version, + "Bundle-License" to "https://opensource.org/licenses/Apache-2.0" + ) + } + } +} diff --git a/docs/mapping.md b/docs/mapping.md new file mode 100644 index 0000000..9d550b5 --- /dev/null +++ b/docs/mapping.md @@ -0,0 +1,67 @@ +# Mapping Documentation + +## Content of a single mapping + +To adapt the provided mapping, the following syntax must be considered: + +The mapping consists of a set of **metrics**, which define the results as they will be sent to the Orchestrator. + +Each metric is specified by its **name**, **rules** and **configuration**. + +### Rules + +It is possible to define one or multiple rules for each metric. These rules represent the MARK rules evaluated by Codyze. +A metric will only be evaluated if _all_ of its rules have been evaluated prior. Only if _all_ of its rules passed the metric will be evaluated as compliant. + +### Configuration + +The configuration of a metric defines its evaluation in the Orchestrator. + +The **default** value indicates whether this configuration represents a default configuration or has been modified. + +The **operator** specifies the operator which is used to compare different results of this metric with the target value. + +The **type** specifies the data type the results of this metric have. Possible values are *BOOLEAN*, *NUMBER* and *STRING*. + +The **target** value specifies one or multiple values that define - together with the operator - which results are considered good. + +## Having multiple mappings + +There are many reasons why one would want to have multiple mapping files. +One of them being that there may be rules with the same name in different modules which should not be verified together. + +Therefore, the specified Mark directory is being scanned top-to-bottom until a mapping is found. +This mapping is subsequently applied to all hierarchical equivalent or lower rules within the same directory. +Other mapping files found below an existing mapping will be ignored. + +When creating multiple mappings consider that every evaluated mapping will cause a separate analysis run. + +### Example + +Consider the following structure of the Mark rule source directory: + +``` +mark/ +├─ botan/ +│ ├─ botan-rules-1/ +│ │ ├─ mapping.yaml [0] +│ │ ├─ botan-rule-1.mark +│ ├─ botan-rules-2/ +│ │ ├─ botan-rule-2.mark +│ ├─ botan-rule-0.mark +├─ bouncycastle/ +│ ├─ mapping.yaml [1] +│ ├─ bc-rules-1/ +│ │ ├─ bc-rule-1.mark +│ ├─ bc-rules-2/ +│ │ ├─ bc-rule-2.mark +│ │ ├─ mapping.yaml [2] +│ ├─ bc-rule-0.mark + +``` + +In this scenario, `botan-rule-1.mark` will be included in the run evaluating `[0]`. +All other rules in the `botan/` directory will be ignored as there is no mapping at or above their level. + +Within the `bouncycastle/` directory all rules will be analyzed in the context of `[1]`. +The mapping found at `[2]` will not be applied as it is within a directory that already belongs to `[1]`. \ No newline at end of file diff --git a/docs/medina-rules.md b/docs/medina-rules.md new file mode 100644 index 0000000..68358b0 --- /dev/null +++ b/docs/medina-rules.md @@ -0,0 +1,32 @@ +# MEDINA Rule Documentation + +## Content of a rule specification + +The file containing the MEDINA rules is specified by the program argument `--rules` and defaults to `medina.yaml`. +The YAML content is expected to be structured in the following way: + +``` +metrics: + - name: <METRIC1> + target: + - <TARGET1> + - ... + - name: <METRIC2> + target: + - <TARGET1> + - ... + - ... +``` + +### Available rules + +The following table lists all currently available rules and the expected format for their respective target values: + +| Name | Target Value | Explanation | +|:--------------|:--------------------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------| +| CodeSignoff | "<SIGNER_NAME>" | Checks for a Signoff by the specified signer | +| SignedCommits | "<SIGNING_KEY_ID>" | Checks for a valid GPG signature from the specified key | +| SignedSignoff | <div>Map: {</br>name: "<SIGNER_NAME>", </br>email: "<SIGNER_EMAIL>", </br>pub-key-id: "<SIGNING_KEY_ID>"</br>}</div> | Checks whether the commit contains a valid signature and signoff from the specified subject | +| ApprovedCommitAuthor | "<AUTHOR_NAME>" | Checks whether the commit was authored from a specified subject | + +The `SIGNING_KEY_ID` is expected to be the 16-character-long ID of the key used for signing. diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f090a2944b7473328c07c9755baa3196..7f93135c49b765f8051ef9d0a6055ff8e46073d8 100644 GIT binary patch delta 44091 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�*~O;X<TNc~H2 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 J%%6f(003c>gtPzv delta 41205 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 ZZfl7Jc(byB#LXGZ8I*+?7^H83cmO;W-rE2G diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ae04661..3fa8f86 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index a69d9cb..1aa94a4 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 53a6b23..6689b85 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% diff --git a/local.Dockerfile b/local.Dockerfile new file mode 100644 index 0000000..5e5fced --- /dev/null +++ b/local.Dockerfile @@ -0,0 +1,26 @@ +FROM eclipse-temurin:11.0.20.1_1-jre + +LABEL org.opencontainers.image.authors="Fraunhofer AISEC <codyze@aisec.fraunhofer.de>" +LABEL org.opencontainers.image.vendor="Fraunhofer AISEC" +LABEL org.opencontainers.image.licenses="Apache-2.0" +LABEL org.opencontainers.image.title="Codyze for MEDINA" +LABEL org.opencontainers.image.description="Compliance checks for source code using Codyze. Adjusted for MEDINA framework." + +# required for Codyze MEDINA functionalities -> git, gpg +RUN apt-get update && apt-get -y --no-install-recommends install \ + git \ + gpg \ + && rm -rf /var/lib/apt/lists/* + +# add distribution +ADD build/distributions/codyze-*.tar /usr/local/lib/ +# add links for ease of use +RUN ln -s /usr/local/lib/codyze-*/bin/codyze-medina /usr/local/bin/ \ + && ln -s /usr/local/lib/codyze-* /codyze-medina + +WORKDIR /project +# Add workdir to safe directories +RUN git config --global --add safe.directory /project + +# default entrypoint +ENTRYPOINT ["codyze-medina"] \ No newline at end of file diff --git a/mark/bc-jca/Cipher.mark b/mark/bc-jca/Cipher.mark new file mode 100644 index 0000000..fc6d829 --- /dev/null +++ b/mark/bc-jca/Cipher.mark @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2020-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + */ +package java.jca + +entity Cipher { + + var transform; + var provider; + + var opmode; + var certificate; + var random; + var key; + var params; + var paramspec; + + var input; + var output; + + var wrappedkey; + var wrappedkeyalgorithm; + var wrappedkeytype; + + op instantiate { + javax.crypto.Cipher.getInstance( + transform : java.lang.String + ); + javax.crypto.Cipher.getInstance( + transform : java.lang.String, + provider : java.lang.String | java.security.Provider + ); + } + + op init { + javax.crypto.Cipher.init( + opmode : int, + certificate : java.security.cert.Certificate + ); + javax.crypto.Cipher.init( + opmode : int, + certificate : java.security.cert.Certificate, + random : java.security.SecureRandom + ); + javax.crypto.Cipher.init( + opmode : int, + key : java.security.Key + ); + javax.crypto.Cipher.init( + opmode : int, + key : java.security.Key, + params : java.security.AlgorithmParameters + ); + javax.crypto.Cipher.init( + opmode : int, + key : java.security.Key, + params : java.security.AlgorithmParameters, + random : java.security.SecureRandom + ); + javax.crypto.Cipher.init( + opmode : int, + key : java.security.Key, + random : java.security.SecureRandom + ); + javax.crypto.Cipher.init( + opmode : int, + key : java.security.Key, + paramspec : java.security.spec.AlgorithmParameterSpec + ); + javax.crypto.Cipher.init( + opmode : int, + key : java.security.Key, + paramspec : javax.crypto.spec.IvParameterSpec + ); + javax.crypto.Cipher.init( + opmode : int, + key : java.security.Key, + paramspec : java.security.spec.AlgorithmParameterSpec, + random : java.security.SecureRandom + ); + javax.crypto.Cipher.init( + opmode : int, + key : java.security.Key, + paramspec : javax.crypto.spec.IvParameterSpec, + random : java.security.SecureRandom + ); + } + + op aad { + javax.crypto.Cipher.updateAAD(src : byte[] | java.nio.ByteBuffer); + javax.crypto.Cipher.updateAAD(src: byte[], ...); + } + + op update { + output = javax.crypto.Cipher.update(input : byte[]); + output = javax.crypto.Cipher.update(input : byte[], _, _); + javax.crypto.Cipher.update(input : byte[], _, _, output : byte[]); + javax.crypto.Cipher.update(input : byte[], _, _, output : byte[], _); + javax.crypto.Cipher.update(input : java.nio.ByteBuffer, output : java.nio.ByteBuffer); + } + + op finalize { + output = javax.crypto.Cipher.doFinal(); + output = javax.crypto.Cipher.doFinal(input : byte[]); + javax.crypto.Cipher.doFinal(output : byte[], _); + output = javax.crypto.Cipher.doFinal(input : byte[], _, _); + javax.crypto.Cipher.doFinal(input : byte[], _, _, output : byte[]); + javax.crypto.Cipher.doFinal(input : byte[], _, _, output : byte[], _); + javax.crypto.Cipher.doFinal(input : java.nio.ByteBuffer, output: java.nio.ByteBuffer); + } + + op keywrap { + wrappedkey = javax.crypto.Cipher.wrap(key : java.security.Key); + key = javax.crypto.Cipher.unwrap( + wrappedkey : byte[], + wrappedkeyalgorithm : java.lang.String, + wrappedkeytype : int + ); + } +} diff --git a/src/test/resources/Mark/bouncycastle/KeyAgreement.mark b/mark/bc-jca/KeyAgreement.mark similarity index 61% rename from src/test/resources/Mark/bouncycastle/KeyAgreement.mark rename to mark/bc-jca/KeyAgreement.mark index f2ed50a..c99e99c 100644 --- a/src/test/resources/Mark/bouncycastle/KeyAgreement.mark +++ b/mark/bc-jca/KeyAgreement.mark @@ -1,3 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2020-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + */ package java.jca entity KeyAgreement { diff --git a/mark/bc-jca/LICENSE b/mark/bc-jca/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/mark/bc-jca/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/test/resources/Mark/bouncycastle/Mac.mark b/mark/bc-jca/Mac.mark similarity index 52% rename from src/test/resources/Mark/bouncycastle/Mac.mark rename to mark/bc-jca/Mac.mark index f0f9046..15c7933 100644 --- a/src/test/resources/Mark/bouncycastle/Mac.mark +++ b/mark/bc-jca/Mac.mark @@ -1,20 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2020-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + */ package java.jca /* * Represents javax.crypto.Mac */ entity Mac { - + var algorithm; var provider; - + var key; var params; - + var input; var output; - op instantiate { javax.crypto.Mac.getInstance( algorithm : java.lang.String @@ -24,7 +49,7 @@ entity Mac { provider : java.lang.String | java.security.Provider ); } - + op init { javax.crypto.Mac.init(key : java.security.Key); javax.crypto.Mac.init( @@ -32,20 +57,19 @@ entity Mac { params : java.security.spec.AlgorithmParameterSpec ); } - + op update { javax.crypto.Mac.update(input : byte | byte[] | java.nio.ByteBuffer); javax.crypto.Mac.update(input : byte[], ...); } - + op finalize { output = javax.crypto.Mac.doFinal(); output = javax.crypto.Mac.doFinal(input : byte[]); javax.crypto.Mac.doFinal(output : byte[], _); } - + op reset { javax.crypto.Mac.reset(); } - -} \ No newline at end of file +} diff --git a/mark/bc-jca/MessageDigest.mark b/mark/bc-jca/MessageDigest.mark new file mode 100644 index 0000000..0e3b5d1 --- /dev/null +++ b/mark/bc-jca/MessageDigest.mark @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2020-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + */ +package java.jca + +/* + * Represents java.security.MessageDigest + */ +entity MessageDigest { + + var algorithm; + var provider; + var input; + var digest; + + op instantiate { + java.security.MessageDigest.getInstance(algorithm : java.lang.String); + java.security.MessageDigest.getInstance( + algorithm : java.lang.String, + provider : java.lang.String | java.security.Provider + ); + } + + op update { + java.security.MessageDigest.update(input : byte | byte[] | java.nio.ByteBuffer); + java.security.MessageDigest.update( + input : byte[], + ... + ); + } + + op digest { + digest = java.security.MessageDigest.digest(); + digest = java.security.MessageDigest.digest(input : byte[]); + java.security.MessageDigest.digest(digest : byte[], ...); + } + + op reset { + java.security.MessageDigest.reset(); + } +} diff --git a/mark/bc-jca/README.md b/mark/bc-jca/README.md new file mode 100644 index 0000000..1854e6c --- /dev/null +++ b/mark/bc-jca/README.md @@ -0,0 +1,8 @@ +# MARK for Bouncy Castle's JCA/JCE Provider + +## License +These files are part of [Codyze](https://www.codyze.io/). +They are distributed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt). + +They have been included from [Codyze on GitHub](https://github.com/Fraunhofer-AISEC/codyze/) into Codyze for MEDINA. +Adjustments within the [MEDINA project](https://medina-project.eu/) are marked appropriately. \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/SecureRandom.mark b/mark/bc-jca/SecureRandom.mark similarity index 64% rename from src/test/resources/Mark/bouncycastle/SecureRandom.mark rename to mark/bc-jca/SecureRandom.mark index fe531bf..c85ea98 100644 --- a/src/test/resources/Mark/bouncycastle/SecureRandom.mark +++ b/mark/bc-jca/SecureRandom.mark @@ -1,14 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2020-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + */ package java.jca entity SecureRandom { - + var algorithm; var provider; var params; var seed; var numBytes; var randomBytes; - + op instantiate { java.security.SecureRandom.getInstance(algorithm : java.lang.String); java.security.SecureRandom.getInstance( @@ -32,20 +58,20 @@ entity SecureRandom { seed : byte[] ); } - + op seed { java.security.SecureRandom.setSeed(seed : byte[] | long); } - + op reseed { java.security.SecureRandom.reseed(); java.security.SecureRandom.reseed(params : java.security.SecureRandomParameters); } - + op generateSeed { seed = java.security.SecureRandom.generateSeed(numBytes : int); } - + op generate { java.security.SecureRandom.next(numBytes : int); java.security.SecureRandom.nextBytes(randomBytes : bytes[]); @@ -54,5 +80,4 @@ entity SecureRandom { params : java.security.SecureRandomParameters ); } - -} \ No newline at end of file +} diff --git a/mark/bc-jca/Signature.mark b/mark/bc-jca/Signature.mark new file mode 100644 index 0000000..61a6b8d --- /dev/null +++ b/mark/bc-jca/Signature.mark @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2020-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + */ +package java.jca + +entity Signature { + + var algorithm; + var provider; + + var privateKey; + var random; + + var certificate; + var publicKey; + + var b; + var data; + var off; + var len; + + var outbuf; + var offset; + var len; + + var signature; + var offset; + var length; + + op instantiate { + java.security.Signature.getInstance( + algorithm : java.lang.String + ); + java.security.Signature.getInstance( + algorithm : java.lang.String, + provider : java.lang.String | java.security.Provider + ); + } + + op initsign { + java.security.Signature.initSign( + privateKey : java.security.PrivateKey + ); + java.security.Signature.initSign( + privateKey : java.security.PrivateKey, + random : java.security.SecureRandom + ); + } + + op initverify { + java.security.Signature.initVerify( + certificate : java.security.cert.Certificate + ); + java.security.Signature.initVerify( + publicKey : java.security.PublicKey + ); + } + + op update { + java.security.Signature.update( + b : byte + ); + java.security.Signature.update( + data : byte[] | java.nio.ByteBuffer + ); + java.security.Signature.update( + data : byte[], + off : int, + len : int + ); + } + + op sign { + signature = java.security.Signature.sign(); + java.security.Signature.sign( + outbuf : byte[], + offseet : int, + len : int + ); + } + + op verify { + java.security.Signature.verify( + signature : byte[] + ); + java.security.Signature.verify( + signature : byte[], + offset : int, + length : int + ); + } +} diff --git a/mark/bc-jca/findingDescription.json b/mark/bc-jca/findingDescription.json new file mode 100644 index 0000000..52dc0b7 --- /dev/null +++ b/mark/bc-jca/findingDescription.json @@ -0,0 +1,92 @@ +{ + "Ciphers": { + "fullDescription": { + "text": "Using insecure ciphers. Ensure the use of recommended ciphers by BSI TR-02102-1, which includes AES and RSA/ECIES/DLIES." + }, + "shortDescription": { + "text": "Use of insecure ciphers." + }, + "fixes": [ + { + "description": { + "text": "Use AES with cipher modes CCM, GCM, CTR, CBC or RSA and encryption schemes ECIES and DLIES." + } + } + ] + }, + "HashFunctions": { + "fullDescription": { + "text": "Using insecure hash functions. Ensure the use of recommended hash functions by BSI TR-02102-1, which includes SHA-2 and SHA-2 of at least 256 bit digest size." + }, + "shortDescription": { + "text": "Use of insecure hash functions." + }, + "fixes": [ + { + "description": { + "text": "Use hash functions of the SHA-2 and SHA-3 family with at least 256 bit digest sizes." + } + } + ] + }, + "MessageAuthenticationCodes": { + "fullDescription": { + "text": "Using insecure message authentication codes. Ensure the use of recommended message authentication codes by BSI TR-02102-1, which includes HMAC, CMAC and GMAC using recommended ciphers and hash functions." + }, + "shortDescription": { + "text": "Use insecure message authentication codes." + }, + "fixes": [ + { + "description": { + "text": "Use message authentication codes based on HMAC, CMAC and GMAC and using recommended ciphers and hash functions." + } + } + ] + }, + "Signatures": { + "fullDescription": { + "text": "Using insecure signatures. Ensure the use of recommended signatures by BSI TR-02102-1, which includes DSA, ECDSA and RSA with recommended hash functions." + }, + "shortDescription": { + "text": "Use of insecure signatures." + }, + "fixes": [ + { + "description": { + "text": "Use signatures based on DSA, ECDSA and RSA with recommended hash functions." + } + } + ] + }, + "SecureRandom": { + "fullDescription": { + "text": "Using insecure PRNG. Ensure the use of recommended PRNG by BSI TR-02102-1, which includes DRBG and blocking native PRNG." + }, + "shortDescription": { + "text": "Use of insecure PRNG." + }, + "fixes": [ + { + "description": { + "text": "Use PRNG based on DRBG or native blocking PRNG." + } + } + ] + }, + "KeyAgreement": { + "fullDescription": { + "text": "Using insecure key agreement. Ensure the use of recommended key agreement by BSI TR-02102-1, which includes DH, ECDH/ECCDH and ECKA-EG with recommended hash functions." + }, + "shortDescription": { + "text": "Use of insecure key agreement." + }, + "fixes": [ + { + "description": { + "text": "Use key agreement based on DH, ECDH/ECCDH and ECKA-EG with recommended hash functions." + } + } + ] + } +} \ No newline at end of file diff --git a/mark/bc-jca/mapping.yaml b/mark/bc-jca/mapping.yaml new file mode 100644 index 0000000..f72cabc --- /dev/null +++ b/mark/bc-jca/mapping.yaml @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# _____ _ +# / ____| | | +# | | ___ __| |_ _ _______ +# | | / _ \ / _` | | | |_ / _ \ +# | |___| (_) | (_| | |_| |/ / __/ +# \_____\___/ \__,_|\__, /___\___| +# __/ | +# |___/ +# +# This file is part of the MEDINA Framework. +metrics: + - name: "SecureCryptographicPrimitives" + rules: + - "Ciphers" + - "HashFunctions" + - "MessageAuthenticationCodes" + - "Signatures" + - "SecureRandom" + - "KeyAgreement" + configuration: + default: false + operator: "==" + type: BOOLEAN + target: + - true diff --git a/mark/bc-jca/rules.mark b/mark/bc-jca/rules.mark new file mode 100644 index 0000000..50cac13 --- /dev/null +++ b/mark/bc-jca/rules.mark @@ -0,0 +1,402 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2020-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA framework. + */ +package de.fraunhofer.aisec.codyze.mark.bcjca + +/* + * Included providers + * - SUN version 17 + * - SunRsaSign version 17 + * - SunEC version 17 + * - SunJCE version 17 + * - BC version 1.76 + */ + +/** + * Check used ciphers transforms. + * + * Note: Recommendations are based on BSI TR-02102-1, version 2023-01 + * - Symmetric crypto + * - ciphers: AES-128, AES-192, AES-256 + * - cipher modes: CCM, GCM, CTR, CBC + * - padding (only for cipher mode CBC): ISO7816-4Padding, PKCS5Padding, PKCS7Padding + * - Asymmetric crypto + * - system/scheme: ECIES, DLIES, RSA + */ +rule Ciphers { + using + Cipher as c + ensure + c.transform in [ + /* SunJCE version 17 */ + /* AES with cipher modes CCM, GCM, CTR, CBC */ + "AES/CTR/NOPADDING", + "AES/CBC/PKCS5PADDING", + "AES/GCM/NoPadding", + "Cipher.AES_128/GCM/NoPadding", + "OID.2.16.840.1.101.3.4.1.6", "2.16.840.1.101.3.4.1.6", + "AES_192/GCM/NoPadding", + "OID.2.16.840.1.101.3.4.1.26", "2.16.840.1.101.3.4.1.26", + "AES_256/GCM/NoPadding", + "OID.2.16.840.1.101.3.4.1.46", "2.16.840.1.101.3.4.1.46", + /* AES Password based Encryption */ + "PBEWithHmacSHA256AndAES_128", + "PBEWithHmacSHA384AndAES_128", + "PBEWithHmacSHA512AndAES_128", + "PBEWithHmacSHA256AndAES_256", + "PBEWithHmacSHA384AndAES_256", + "PBEWithHmacSHA512AndAES_256", + /* RSA */ + "RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING", + "RSA/ECB/OAEPWITHSHA-512/256ANDMGF1PADDING", + "RSA/ECB/OAEPWITHSHA-384ANDMGF1PADDING", + "RSA/ECB/OAEPWITHSHA-512ANDMGF1PADDING", + /* BC version 1.76 */ + /* AES with cipher modes CCM, GCM, CTR, CBC */ + "CCM", + "2.16.840.1.101.3.4.1.7", "OID.2.16.840.1.101.3.4.1.7", "2.16.840.1.101.3.4.1.27", "OID.2.16.840.1.101.3.4.1.27", "2.16.840.1.101.3.4.1.47", "OID.2.16.840.1.101.3.4.1.47", "1.2.410.200046.1.1.37", "OID.1.2.410.200046.1.1.37", "1.2.410.200046.1.1.38", "OID.1.2.410.200046.1.1.38", "1.2.410.200046.1.1.39", "OID.1.2.410.200046.1.1.39", + "GCM", + "2.16.840.1.101.3.4.1.6", "OID.2.16.840.1.101.3.4.1.6", "2.16.840.1.101.3.4.1.26", "OID.2.16.840.1.101.3.4.1.26", "2.16.840.1.101.3.4.1.46", "OID.2.16.840.1.101.3.4.1.46", + /* AES Password based Encryption */ + "PBEWITHSHA256AND128BITAES-CBC-BC", + "1.3.6.1.4.1.22554.1.2.1.2.1.2", "OID.1.3.6.1.4.1.22554.1.2.1.2.1.2", "PBEWITHSHA-256AND128BITAES-CBC-BC", "PBEWITHSHA256AND128BITAES-BC", "PBEWITHSHA-256AND128BITAES-BC", + "PBEWITHSHA256AND192BITAES-CBC-BC", + "1.3.6.1.4.1.22554.1.2.1.2.1.22", "OID.1.3.6.1.4.1.22554.1.2.1.2.1.22", "PBEWITHSHA-256AND192BITAES-CBC-BC", "PBEWITHSHA256AND192BITAES-BC", "PBEWITHSHA-256AND192BITAES-BC", + "PBEWITHSHA256AND256BITAES-CBC-BC", + "1.3.6.1.4.1.22554.1.2.1.2.1.42", "OID.1.3.6.1.4.1.22554.1.2.1.2.1.42", "PBEWITHSHA-256AND256BITAES-CBC-BC", "PBEWITHSHA256AND256BITAES-BC", "PBEWITHSHA-256AND256BITAES-BC", + /* ECIES */ + "ECIESwithSHA256andAES-CBC", + "ECIESwithSHA384andAES-CBC", + "ECIESwithSHA512andAES-CBC" + /* DLIES */ + /* none */ + ] + fail +} + +/** + * Check used hash functions (aka message digest). + * + * Note: Recommendations are based on BSI TR-02102-1, version 2023-01 + * - Hash functions: SHA-256, SHA-512/256, SHA-384, SHA-512, SHA3-256, SHA3-384, SHA3-512 + */ +rule HashFunctions { + using + MessageDigest as md + ensure + md.algorithm in [ + /* SUN version 17 */ + "SHA-256", + "OID.2.16.840.1.101.3.4.2.1", "2.16.840.1.101.3.4.2.1", "SHA256", + "SHA-512/256", + "OID.2.16.840.1.101.3.4.2.6", "2.16.840.1.101.3.4.2.6", "SHA512/256", + "SHA-384", + "OID.2.16.840.1.101.3.4.2.2", "2.16.840.1.101.3.4.2.2", "SHA384", + "SHA-512", + "OID.2.16.840.1.101.3.4.2.3", "2.16.840.1.101.3.4.2.3", "SHA512", + "SHA3-256", + "OID.2.16.840.1.101.3.4.2.8", "2.16.840.1.101.3.4.2.8", + "SHA3-384", + "OID.2.16.840.1.101.3.4.2.9", "2.16.840.1.101.3.4.2.9", + "SHA3-512", + "OID.2.16.840.1.101.3.4.2.10", "2.16.840.1.101.3.4.2.10", + /* BC version 1.76 */ + "SHA-256", + "SHA256", "2.16.840.1.101.3.4.2.1", + "SHA-512/256", + "SHA512/256", "SHA512256", "SHA-512(256)", "SHA512(256)", "2.16.840.1.101.3.4.2.6", + "SHA-384", + "SHA384", "2.16.840.1.101.3.4.2.2", + "SHA-512", + "SHA512", "2.16.840.1.101.3.4.2.3", + "SHA3-256", + "2.16.840.1.101.3.4.2.8", "OID.2.16.840.1.101.3.4.2.8", + "SHA3-384", + "2.16.840.1.101.3.4.2.9", "OID.2.16.840.1.101.3.4.2.9", + "SHA3-512", + "2.16.840.1.101.3.4.2.10", "OID.2.16.840.1.101.3.4.2.10" + ] + fail +} + +/** + * Check used message authentication codes. + * + * Note: Recommendations are based on BSI TR-02102-1, version 2023-01 + * - CMAC -> using recommended block ciphers + * - HMAC -> using recommended hash functions + * - GMAC -> using recommended block ciphers + */ +rule MessageAuthenticationCodes { + using + Mac as m + ensure + m.algorithm in [ + /* SunJCE version 17*/ + /* HMAC */ + "HmacPBESHA256", "HmacPBESHA512/256", "HmacPBESHA384", "HmacPBESHA512", + "PBEWithHmacSHA256", "PBEWithHmacSHA384", "PBEWithHmacSHA512", + "HmacSHA256", + "OID.1.2.840.113549.2.9", "1.2.840.113549.2.9", + "HmacSHA512/256", + "OID.1.2.840.113549.2.13", "1.2.840.113549.2.13", + "HmacSHA384", + "OID.1.2.840.113549.2.10", "1.2.840.113549.2.10", + "HmacSHA512", + "OID.1.2.840.113549.2.11", "1.2.840.113549.2.11", + "HmacSHA3-256", + "OID.2.16.840.1.101.3.4.2.14", "2.16.840.1.101.3.4.2.14", + "HmacSHA3-384", + "OID.2.16.840.1.101.3.4.2.15", "2.16.840.1.101.3.4.2.15", + "HmacSHA3-512", + "OID.2.16.840.1.101.3.4.2.16", "2.16.840.1.101.3.4.2.16", + /* BC version 1.76 */ + /* CMAC */ + "AESCMAC", + /* HMAC */ + "PBEWITHHMACSHA256", "PBEWITHHMACSHA384", "PBEWITHHMACSHA512", + "HMACSHA256", + "HMAC-SHA256", "HMAC/SHA256", "1.2.840.113549.2.9", "2.16.840.1.101.3.4.2.1", + "HMACSHA512/256", " + HMAC-SHA512/256", "HMAC/SHA512/256", + "HMACSHA384", + "HMAC-SHA384", "HMAC/SHA384", "1.2.840.113549.2.10", + "HMACSHA512", + "HMAC-SHA512", "HMAC/SHA512", "1.2.840.113549.2.11", + "HMACSHA3-256", + "HMAC-SHA3-256", "HMAC/SHA3-256", "2.16.840.1.101.3.4.2.14", + "HMACSHA3-384", + "HMAC-SHA3-384", "HMAC/SHA3-384", "2.16.840.1.101.3.4.2.15", + "HMACSHA3-512", + "HMAC-SHA3-512", "HMAC/SHA3-512", "2.16.840.1.101.3.4.2.16", + /* GMAC */ + "AES-GMAC", + "AESGMAC" + ] + fail +} + +rule Signatures { + using + Signature as s + ensure + s.algorithm in [ + /* SUN version 17 */ + /* DSA */ + "SHA256withDSA", + "OID.2.16.840.1.101.3.4.3.2", "2.16.840.1.101.3.4.3.2", + "SHA384withDSA", + "OID.2.16.840.1.101.3.4.3.3", "2.16.840.1.101.3.4.3.3", + "SHA512withDSA", + "OID.2.16.840.1.101.3.4.3.4", "2.16.840.1.101.3.4.3.4", + "SHA3-256withDSA", + "OID.2.16.840.1.101.3.4.3.6", "2.16.840.1.101.3.4.3.6", + "SHA3-384withDSA", + "OID.2.16.840.1.101.3.4.3.7", "2.16.840.1.101.3.4.3.7", + "SHA3-512withDSA", + "OID.2.16.840.1.101.3.4.3.8", "2.16.840.1.101.3.4.3.8", + "SHA256withDSAinP1363Format", + "SHA384withDSAinP1363Format", + "SHA512withDSAinP1363Format", + "SHA3-256withDSAinP1363Format", + "SHA3-384withDSAinP1363Format", + "SHA3-512withDSAinP1363Format", + /* SunRsaSign version 17 */ + /* RSA */ + /* none */ + /* SunEC version 17 */ + /* ECDSA */ + "SHA256withECDSA", + "OID.1.2.840.10045.4.3.2", "1.2.840.10045.4.3.2", + "SHA384withECDSA", + "OID.1.2.840.10045.4.3.3", "1.2.840.10045.4.3.3", + "SHA512withECDSA", + "OID.1.2.840.10045.4.3.4", "1.2.840.10045.4.3.4", + "SHA3-256withECDSA", + "OID.2.16.840.1.101.3.4.3.10", "2.16.840.1.101.3.4.3.10", + "SHA3-384withECDSA", + "OID.2.16.840.1.101.3.4.3.11", "2.16.840.1.101.3.4.3.11", + "SHA3-512withECDSA", + "OID.2.16.840.1.101.3.4.3.12", "2.16.840.1.101.3.4.3.12", + "SHA256withECDSAinP1363Format", + "SHA384withECDSAinP1363Format", + "SHA512withECDSAinP1363Format", + "SHA3-256withECDSAinP1363Format", + "SHA3-384withECDSAinP1363Format", + "SHA3-512withECDSAinP1363Format", + /* BC version 1.76*/ + /* RSA - EMSA-PSS */ + "SHA256WITHRSAANDMGF1", + "SHA256withRSA/PSS", "SHA256WithRSA/PSS", "SHA256WITHRSA/PSS", "SHA256withRSASSA-PSS", "SHA256WithRSASSA-PSS", "SHA256WITHRSASSA-PSS", "SHA256withRSAandMGF1", "SHA256WithRSAAndMGF1", + "SHA512(256)WITHRSAANDMGF1", + "SHA512(256)withRSA/PSS", "SHA512(256)WithRSA/PSS", "SHA512(256)WITHRSA/PSS", "SHA512(256)withRSASSA-PSS", "SHA512(256)WithRSASSA-PSS", "SHA512(256)WITHRSASSA-PSS", "SHA512(256)withRSAandMGF1", "SHA512(256)WithRSAAndMGF1", + "SHA384WITHRSAANDMGF1", + "SHA384withRSA/PSS", "SHA384WithRSA/PSS", "SHA384WITHRSA/PSS", "SHA384withRSASSA-PSS", "SHA384WithRSASSA-PSS", "SHA384WITHRSASSA-PSS", "SHA384withRSAandMGF1", "SHA384WithRSAAndMGF1", + "SHA512WITHRSAANDMGF1", + "SHA512withRSA/PSS", "SHA512WithRSA/PSS", "SHA512WITHRSA/PSS", "SHA512withRSASSA-PSS", "SHA512WithRSASSA-PSS", "SHA512WITHRSASSA-PSS", "SHA512withRSAandMGF1", "SHA512WithRSAAndMGF1", + "SHA3-256WITHRSAANDMGF1", + "SHA3-256withRSA/PSS", "SHA3-256WithRSA/PSS", "SHA3-256WITHRSA/PSS", "SHA3-256withRSASSA-PSS", "SHA3-256WithRSASSA-PSS", "SHA3-256WITHRSASSA-PSS", "SHA3-256withRSAandMGF1", "SHA3-256WithRSAAndMGF1", + "SHA3-384WITHRSAANDMGF1", + "SHA3-384withRSA/PSS", "SHA3-384WithRSA/PSS", "SHA3-384WITHRSA/PSS", "SHA3-384withRSASSA-PSS", "SHA3-384WithRSASSA-PSS", "SHA3-384WITHRSASSA-PSS", "SHA3-384withRSAandMGF1", "SHA3-384WithRSAAndMGF1", + "SHA3-512WITHRSAANDMGF1", + "SHA3-512withRSA/PSS", "SHA3-512WithRSA/PSS", "SHA3-512WITHRSA/PSS", "SHA3-512withRSASSA-PSS", "SHA3-512WithRSASSA-PSS", "SHA3-512WITHRSASSA-PSS", "SHA3-512withRSAandMGF1", "SHA3-512WithRSAAndMGF1", + /* RSA - Digital Signature Scheme (DS) 2 und 3 */ + "SHA256WITHRSA/ISO9796-2", + "SHA256withRSA/ISO9796-2", "SHA256WithRSA/ISO9796-2", + "SHA512(256)WITHRSA/ISO9796-2", + "SHA512(256)withRSA/ISO9796-2", "SHA512(256)WithRSA/ISO9796-2", + "SHA384WITHRSA/ISO9796-2", + "SHA384withRSA/ISO9796-2", "SHA384WithRSA/ISO9796-2", + "SHA512WITHRSA/ISO9796-2", + "SHA512withRSA/ISO9796-2", "SHA512WithRSA/ISO9796-2", + /* DSA */ + "SHA256WITHDSA", + "SHA256withDSA", "SHA256WithDSA", "SHA256/DSA", "2.16.840.1.101.3.4.3.2", "OID.2.16.840.1.101.3.4.3.2", + "SHA384WITHDSA", + "SHA384withDSA", "SHA384WithDSA", "SHA384/DSA", "2.16.840.1.101.3.4.3.3", "OID.2.16.840.1.101.3.4.3.3", + "SHA512WITHDSA", + "SHA512withDSA", "SHA512WithDSA", "SHA512/DSA", "2.16.840.1.101.3.4.3.4", "OID.2.16.840.1.101.3.4.3.4", + "SHA3-256WITHDSA", + "SHA3-256withDSA", "SHA3-256WithDSA", "SHA3-256/DSA", "2.16.840.1.101.3.4.3.6", "OID.2.16.840.1.101.3.4.3.6", + "SHA3-384WITHDSA", + "SHA3-384withDSA", "SHA3-384WithDSA", "SHA3-384/DSA", "2.16.840.1.101.3.4.3.7", "OID.2.16.840.1.101.3.4.3.7", + "SHA3-512WITHDSA", + "SHA3-512withDSA", "SHA3-512WithDSA", "SHA3-512/DSA", "2.16.840.1.101.3.4.3.8", "OID.2.16.840.1.101.3.4.3.8", + /* DSA - deterministic */ + "SHA256WITHDDSA", + "SHA384WITHDDSA", + "SHA512WITHDDSA", + "SHA3-256WITHDDSA", + "SHA3-384WITHDDSA", + "SHA3-512WITHDDSA", + "SHA256WITHDETDSA", + "SHA384WITHDETDSA", + "SHA512WITHDETDSA", + /* ECDSA */ + "SHA256WITHECDSA", + "SHA256withECDSA", "SHA256WithECDSA", "SHA256/ECDSA", "1.2.840.10045.4.3.2", "OID.1.2.840.10045.4.3.2", + "SHA384WITHECDSA", + "SHA384withECDSA", "SHA384WithECDSA", "SHA384/ECDSA", "1.2.840.10045.4.3.3", "OID.1.2.840.10045.4.3.3", + "SHA512WITHECDSA", + "SHA512withECDSA", "SHA512WithECDSA", "SHA512/ECDSA", "1.2.840.10045.4.3.4", "OID.1.2.840.10045.4.3.4", + "SHA3-256WITHECDSA", + "SHA3-256withECDSA", "SHA3-256WithECDSA", "SHA3-256/ECDSA", "2.16.840.1.101.3.4.3.10", "OID.2.16.840.1.101.3.4.3.10", + "SHA3-384WITHECDSA", + "SHA3-384withECDSA", "SHA3-384WithECDSA", "SHA3-384/ECDSA", "2.16.840.1.101.3.4.3.11", "OID.2.16.840.1.101.3.4.3.11", + "SHA3-512WITHECDSA", + "SHA3-512withECDSA", "SHA3-512WithECDSA", "SHA3-512/ECDSA", "2.16.840.1.101.3.4.3.12", "OID.2.16.840.1.101.3.4.3.12", + "SHA256WITHCVC-ECDSA", + "SHA256withCVC-ECDSA", "SHA256WithCVC-ECDSA", "SHA256/CVC-ECDSA", "0.4.0.127.0.7.2.2.2.2.3", "OID.0.4.0.127.0.7.2.2.2.2.3", + "SHA384WITHCVC-ECDSA", + "SHA384withCVC-ECDSA", "SHA384WithCVC-ECDSA", "SHA384/CVC-ECDSA", "0.4.0.127.0.7.2.2.2.2.4", "OID.0.4.0.127.0.7.2.2.2.2.4", + "SHA512WITHCVC-ECDSA", + "SHA512withCVC-ECDSA", "SHA512WithCVC-ECDSA", "SHA512/CVC-ECDSA", "0.4.0.127.0.7.2.2.2.2.5", "OID.0.4.0.127.0.7.2.2.2.2.5", + "SHA256WITHPLAIN-ECDSA", + "SHA256withPLAIN-ECDSA", "SHA256WithPLAIN-ECDSA", "SHA256/PLAIN-ECDSA", "0.4.0.127.0.7.1.1.4.1.3", "OID.0.4.0.127.0.7.1.1.4.1.3", + "SHA384WITHPLAIN-ECDSA", + "SHA384withPLAIN-ECDSA", "SHA384WithPLAIN-ECDSA", "SHA384/PLAIN-ECDSA", "0.4.0.127.0.7.1.1.4.1.4", "OID.0.4.0.127.0.7.1.1.4.1.4", + "SHA512WITHPLAIN-ECDSA", + "SHA512withPLAIN-ECDSA", "SHA512WithPLAIN-ECDSA", "SHA512/PLAIN-ECDSA", "0.4.0.127.0.7.1.1.4.1.5", "OID.0.4.0.127.0.7.1.1.4.1.5", + "SHA3-256WITHPLAIN-ECDSA", + "SHA3-256withPLAIN-ECDSA", "SHA3-256WithPLAIN-ECDSA", "SHA3-256/PLAIN-ECDSA", "0.4.0.127.0.7.1.1.4.1.9", "OID.0.4.0.127.0.7.1.1.4.1.9", + "SHA3-384WITHPLAIN-ECDSA", + "SHA3-384withPLAIN-ECDSA", "SHA3-384WithPLAIN-ECDSA", "SHA3-384/PLAIN-ECDSA", "0.4.0.127.0.7.1.1.4.1.10", "OID.0.4.0.127.0.7.1.1.4.1.10", + "SHA3-512WITHPLAIN-ECDSA", + "SHA3-512withPLAIN-ECDSA", "SHA3-512WithPLAIN-ECDSA", "SHA3-512/PLAIN-ECDSA", "0.4.0.127.0.7.1.1.4.1.11", "OID.0.4.0.127.0.7.1.1.4.1.11", + /* ECDSA - deterministic */ + "SHA256WITHECDDSA", + "SHA256WITHDETECDSA", + "SHA384WITHECDDSA", + "SHA384WITHDETECDSA", + "SHA512WITHECDDSA", + "SHA512WITHDETECDSA", + "SHA3-256WITHECDDSA", + "SHA3-384WITHECDDSA", + "SHA3-512WITHECDDSA" + ] + fail +} + +/** + * Check used message authentication codes. + * + * Note: Recommendations are based on BSI TR-02102-1, version 2023-01 + */ +rule SecureRandom { + using + SecureRandom as sr + ensure + sr.algorithm in [ + /* SUN version 17 */ + "NativePRNGBlocking", + "DRBG", + /* BC version 1.76 */ + "DEFAULT", + "NONCEANDIV" + ] + fail +} + +/** + * Check used key agreement protocols. + * + * Note: Recommendations are based on BSI TR-02102-1, version 2023-01 + */ +rule KeyAgreement { + using + KeyAgreement as ka + ensure + ka.algorithm in [ + /* BC version 1.76 */ + /* DH */ + "DHWITHSHA256KDF", + "DHWITHSHA384KDF", + "DHWITHSHA512KDF", + /* ECDH / ECCDH */ + "ECDHWITHSHA256KDF", + "1.3.132.1.11.1", "OID.1.3.132.1.11.1", + "ECDHWITHSHA384KDF", + "1.3.132.1.11.2", "OID.1.3.132.1.11.2", + "ECDHWITHSHA512KDF", + "1.3.132.1.11.3", "OID.1.3.132.1.11.3", + "ECCDHWITHSHA256KDF", + "1.3.132.1.14.1", "OID.1.3.132.1.14.1", + "ECCDHWITHSHA384KDF", + "1.3.132.1.14.2", "OID.1.3.132.1.14.2", + "ECCDHWITHSHA512KDF", + "1.3.132.1.14.3", "OID.1.3.132.1.14.3", + /* ECKA-EG */ + "ECKAEGWITHSHA256KDF", + "0.4.0.127.0.7.1.1.5.1.1.3", "OID.0.4.0.127.0.7.1.1.5.1.1.3", + "ECKAEGWITHSHA384KDF", + "0.4.0.127.0.7.1.1.5.1.1.4", "OID.0.4.0.127.0.7.1.1.5.1.1.4", + "ECKAEGWITHSHA512KDF", + "0.4.0.127.0.7.1.1.5.1.1.5", "OID.0.4.0.127.0.7.1.1.5.1.1.5" + ] + fail +} diff --git a/mark/bc-jsse/LICENSE b/mark/bc-jsse/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/mark/bc-jsse/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/mark/bc-jsse/README.md b/mark/bc-jsse/README.md index df62830..059f36f 100644 --- a/mark/bc-jsse/README.md +++ b/mark/bc-jsse/README.md @@ -1,3 +1,5 @@ # MARK for Bouncy Castle's JSSE Provider -Example MARK specification files for a TLS server implemented using JSSE with Bouncy Castle as provide. The rule checks for an appropriate version of TLS. +## License +These files are part of the [MEDINA project](https://medina-project.eu/). +They are distributed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt). \ No newline at end of file diff --git a/mark/bc-jsse/SSLContext.mark b/mark/bc-jsse/SSLContext.mark index d078c67..528825c 100644 --- a/mark/bc-jsse/SSLContext.mark +++ b/mark/bc-jsse/SSLContext.mark @@ -1,3 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ package de.fraunhofer.aisec.codyze.bcjsse entity SSLContext { diff --git a/mark/bc-jsse/SSLServerSocket.mark b/mark/bc-jsse/SSLServerSocket.mark index 6be07b7..a8a3883 100644 --- a/mark/bc-jsse/SSLServerSocket.mark +++ b/mark/bc-jsse/SSLServerSocket.mark @@ -1,3 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ package de.fraunhofer.aisec.codyze.bcjsse entity SSLServerSocket { diff --git a/mark/bc-jsse/SSLServerSocketFactory.mark b/mark/bc-jsse/SSLServerSocketFactory.mark index 7521deb..91edaa6 100644 --- a/mark/bc-jsse/SSLServerSocketFactory.mark +++ b/mark/bc-jsse/SSLServerSocketFactory.mark @@ -1,3 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ package de.fraunhofer.aisec.codyze.bcjsse entity SSLServerSocketFactory { diff --git a/mark/bc-jsse/findingDescription.json b/mark/bc-jsse/findingDescription.json new file mode 100644 index 0000000..7e9790b --- /dev/null +++ b/mark/bc-jsse/findingDescription.json @@ -0,0 +1,32 @@ +{ + "TlsVersion": { + "fullDescription": { + "text": "TLS version isn't sufficiently restricted and may allow the use of deprecated version. Ensure the use of TLS versions 1.2 or 1.3." + }, + "shortDescription": { + "text": "Insufficient restriction of TLS to version 1.2 or 1.3." + }, + "fixes": [ + { + "description": { + "text": "Use TLS version 1.2 or 1.3." + } + } + ] + }, + "TlsCipherSuites": { + "fullDescription": { + "text": "TLS cipher suites aren't sufficiently restricted and may allow the use of less secure or deprecated cipher suites. Ensure the use of: \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CCM\", \"TLS_ECDHE_ECDSA_WITH_AES_256_CCM\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_DSS_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_RSA_WITH_AES_128_CCM\", \"TLS_DHE_RSA_WITH_AES_256_CCM\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DH_DSS_WITH_AES_128_CBC_SHA256\", \"TLS_DH_DSS_WITH_AES_256_CBC_SHA256\", \"TLS_DH_DSS_WITH_AES_128_GCM_SHA256\", \"TLS_DH_DSS_WITH_AES_256_GCM_SHA384\", \"TLS_DH_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DH_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_DH_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DH_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384\", \"TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256\", \"TLS_DHE_PSK_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_PSK_WITH_AES_256_CBC_SHA384\", \"TLS_DHE_PSK_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_PSK_WITH_AES_256_GCM_SHA38\", \"TLS_DHE_PSK_WITH_AES_128_CCM\", \"TLS_DHE_PSK_WITH_AES_256_CCM\", \"TLS_RSA_PSK_WITH_AES_128_CBC_SHA256\", \"TLS_RSA_PSK_WITH_AES_256_CBC_SHA384\", \"TLS_RSA_PSK_WITH_AES_128_GCM_SHA256\", or \"TLS_RSA_PSK_WITH_AES_256_GCM_SHA384\"." + }, + "shortDescription": { + "text": "Insufficient restriction of TLS cipher suites." + }, + "fixes": [ + { + "description": { + "text": "Use any selection of the following TLS cipher suites: \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_ECDSA_WITH_AES_128_CCM\", \"TLS_ECDHE_ECDSA_WITH_AES_256_CCM\", \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_256_CBC_SHA256\", \"TLS_DHE_DSS_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_DSS_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DHE_RSA_WITH_AES_128_CCM\", \"TLS_DHE_RSA_WITH_AES_256_CCM\", \"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384\", \"TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_DH_DSS_WITH_AES_128_CBC_SHA256\", \"TLS_DH_DSS_WITH_AES_256_CBC_SHA256\", \"TLS_DH_DSS_WITH_AES_128_GCM_SHA256\", \"TLS_DH_DSS_WITH_AES_256_GCM_SHA384\", \"TLS_DH_RSA_WITH_AES_128_CBC_SHA256\", \"TLS_DH_RSA_WITH_AES_256_CBC_SHA256\", \"TLS_DH_RSA_WITH_AES_128_GCM_SHA256\", \"TLS_DH_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256\", \"TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384\", \"TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256\", \"TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256\", \"TLS_DHE_PSK_WITH_AES_128_CBC_SHA256\", \"TLS_DHE_PSK_WITH_AES_256_CBC_SHA384\", \"TLS_DHE_PSK_WITH_AES_128_GCM_SHA256\", \"TLS_DHE_PSK_WITH_AES_256_GCM_SHA38\", \"TLS_DHE_PSK_WITH_AES_128_CCM\", \"TLS_DHE_PSK_WITH_AES_256_CCM\", \"TLS_RSA_PSK_WITH_AES_128_CBC_SHA256\", \"TLS_RSA_PSK_WITH_AES_256_CBC_SHA384\", \"TLS_RSA_PSK_WITH_AES_128_GCM_SHA256\", or \"TLS_RSA_PSK_WITH_AES_256_GCM_SHA384\"." + } + } + ] + } +} \ No newline at end of file diff --git a/mark/bc-jsse/mapping.yaml b/mark/bc-jsse/mapping.yaml new file mode 100644 index 0000000..d160855 --- /dev/null +++ b/mark/bc-jsse/mapping.yaml @@ -0,0 +1,146 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# _____ _ +# / ____| | | +# | | ___ __| |_ _ _______ +# | | / _ \ / _` | | | |_ / _ \ +# | |___| (_) | (_| | |_| |/ / __/ +# \_____\___/ \__,_|\__, /___\___| +# __/ | +# |___/ +# +# This file is part of the MEDINA Framework. +metrics: + - name: "TLSVersion" + rules: + - "TlsVersion" + configuration: + default: true + operator: ">" + type: STRING + target: + - "1.2" + - name: "TlsCipherSuites" + rules: + - "TlsCipherSuites" + configuration: + default: true + operator: "<=" + type: STRING + target: + - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256" + - "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384" + - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" + - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" + - "TLS_ECDHE_ECDSA_WITH_AES_128_CCM" + - "TLS_ECDHE_ECDSA_WITH_AES_256_CCM" + - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384" + - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" + - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256" + - "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256" + - "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256" + - "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256" + - "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384" + - "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256" + - "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256" + - "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256" + - "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384" + - "TLS_DHE_RSA_WITH_AES_128_CCM" + - "TLS_DHE_RSA_WITH_AES_256_CCM" + - "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256" + - "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384" + - "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256" + - "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384" + - "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256" + - "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384" + - "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256" + - "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384" + - "TLS_DH_DSS_WITH_AES_128_CBC_SHA256" + - "TLS_DH_DSS_WITH_AES_256_CBC_SHA256" + - "TLS_DH_DSS_WITH_AES_128_GCM_SHA256" + - "TLS_DH_DSS_WITH_AES_256_GCM_SHA384" + - "TLS_DH_RSA_WITH_AES_128_CBC_SHA256" + - "TLS_DH_RSA_WITH_AES_256_CBC_SHA256" + - "TLS_DH_RSA_WITH_AES_128_GCM_SHA256" + - "TLS_DH_RSA_WITH_AES_256_GCM_SHA384" + - "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256" + - "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384" + - "TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256" + - "TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384" + - "TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256" + - "TLS_DHE_PSK_WITH_AES_128_CBC_SHA256" + - "TLS_DHE_PSK_WITH_AES_256_CBC_SHA384" + - "TLS_DHE_PSK_WITH_AES_128_GCM_SHA256" + - "TLS_DHE_PSK_WITH_AES_256_GCM_SHA384" + - "TLS_DHE_PSK_WITH_AES_128_CCM" + - "TLS_DHE_PSK_WITH_AES_256_CCM" + - "TLS_RSA_PSK_WITH_AES_128_CBC_SHA256" + - "TLS_RSA_PSK_WITH_AES_256_CBC_SHA384" + - "TLS_RSA_PSK_WITH_AES_128_GCM_SHA256" + - "TLS_RSA_PSK_WITH_AES_256_GCM_SHA384" + - name: "TlsDHGroups" + rules: + - "TlsDHGroups" + configuration: + default: true + operator: "<=" + type: STRING + target: + - "secp256r1" + - "secp384r1" + - "secp521r1" + - "brainpoolP256r1" + - "brainpoolP384r1" + - "brainpoolP512r1" + - "ffdhe2048" + - "ffdhe3072" + - "ffdhe4096" + - "brainpoolP256r1tls13" + - "brainpoolP384r1tls13" + - "brainpoolP512r1tls13" + - name: "TlsSignatureAlgorithms" + rules: + - "TlsSignatureAlgorithm" + configuration: + default: true + operator: "<=" + type: STRING + target: + - "RSA+SHA256" + - "RSA+SHA384" + - "RSA+SHA512" + - "DSA+SHA256" + - "DSA+SHA384" + - "DSA+SHA512" + - "ECDSA+SHA256" + - "ECDSA+SHA384" + - "ECDSA+SHA512" + - "rsa_pss_rsae_sha256" + - "rsa_pss_rsae_sha384" + - "rsa_pss_rsae_sha512" + - "rsa_pss_pss_sha256" + - "rsa_pss_pss_sha384" + - "rsa_pss_pss_sha512" + - "ecdsa_secp256r1_sha256" + - "ecdsa_secp384r1_sha384" + - "ecdsa_brainpoolP256r1tls13_sha256" + - "ecdsa_brainpoolP384r1tls13_sha384" + - "ecdsa_brainpoolP512r1tls13_sha512" + - "rsa_pkcs1_sha256" + - "rsa_pkcs1_sha384" + - "rsa_pkcs1_sha512" diff --git a/mark/bc-jsse/rules.mark b/mark/bc-jsse/rules.mark index 0bf5ca5..0a293e6 100644 --- a/mark/bc-jsse/rules.mark +++ b/mark/bc-jsse/rules.mark @@ -1,9 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ package de.fraunhofer.aisec.codyze.mark.bcjsse -rule ID_1 { +rule TlsVersion { using SSLServerSocket as socket ensure _subset(socket.protocols, ["TLSv1.2", "TLSv1.3"]) fail } + +rule TlsCipherSuites { + using + SSLServerSocket as socket + ensure + _subset(socket.ciphersuites, + ["TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_CCM", + "TLS_ECDHE_ECDSA_WITH_AES_256_CCM", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", + "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256", + "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256", + "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DHE_RSA_WITH_AES_128_CCM", + "TLS_DHE_RSA_WITH_AES_256_CCM", + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DH_DSS_WITH_AES_128_CBC_SHA256", + "TLS_DH_DSS_WITH_AES_256_CBC_SHA256", + "TLS_DH_DSS_WITH_AES_128_GCM_SHA256", + "TLS_DH_DSS_WITH_AES_256_GCM_SHA384", + "TLS_DH_RSA_WITH_AES_128_CBC_SHA256", + "TLS_DH_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DH_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DH_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256", + "TLS_DHE_PSK_WITH_AES_128_CBC_SHA256", + "TLS_DHE_PSK_WITH_AES_256_CBC_SHA384", + "TLS_DHE_PSK_WITH_AES_128_GCM_SHA256", + "TLS_DHE_PSK_WITH_AES_256_GCM_SHA384", + "TLS_DHE_PSK_WITH_AES_128_CCM", + "TLS_DHE_PSK_WITH_AES_256_CCM", + "TLS_RSA_PSK_WITH_AES_128_CBC_SHA256", + "TLS_RSA_PSK_WITH_AES_256_CBC_SHA384", + "TLS_RSA_PSK_WITH_AES_128_GCM_SHA256", + "TLS_RSA_PSK_WITH_AES_256_GCM_SHA384"]) + fail +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 2f5d7f5..5ea3306 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,4 @@ -rootProject.name = "codyze" +rootProject.name = "codyze-medina" include("generator_orchestrator") project(":generator_orchestrator").projectDir = file("build/generator_orchestrator") diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/CodyzeMedina.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/CodyzeMedina.kt deleted file mode 100644 index d67383b..0000000 --- a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/CodyzeMedina.kt +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -/* - * Copyright (c) 2022, Fraunhofer AISEC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * _____ _ - * / ____| | | - * | | ___ __| |_ _ _______ - * | | / _ \ / _` | | | |_ / _ \ - * | |___| (_) | (_| | |_| |/ / __/ - * \_____\___/ \__,_|\__, /___\___| - * __/ | - * |___/ - * - * This file is part of the MEDINA Framework. - */ -package de.fraunhofer.aisec.codyze.medina - -import de.fraunhofer.aisec.codyze.analysis.AnalysisServer -import de.fraunhofer.aisec.codyze.analysis.Finding -import de.fraunhofer.aisec.codyze.medina.util.* -import java.util.concurrent.Callable -import java.util.concurrent.TimeUnit -import kotlin.streams.toList -import kotlin.system.exitProcess -import mu.KotlinLogging -import org.openapitools.client.orchestrator.model.Metric -import picocli.CommandLine -import picocli.CommandLine.Command -import picocli.CommandLine.Mixin - -@Command(name = "codyze", mixinStandardHelpOptions = true) -class CodyzeMedina(config: Configuration, args: Array<String>) : Callable<Int> { - private val logger = KotlinLogging.logger {} - @Mixin val configuration: Configuration = config - private val cmdlineArgs = args - - /** - * Callable function containing the core program logic - * @author Florian Wendland - * @author Robert Haimerl - * @return 1 when any of the Findings is problematic, 0 else - */ - override fun call(): Int { - configuration.validate() - - val connection = - Connection( - configuration.orchestrator.orchestratorEndpoint.toString(), - configuration.orchestrator.auth.oauthEndpoint.toString(), - configuration.orchestrator.auth.username, - configuration.orchestrator.auth.password - ) - - val config = - de.fraunhofer.aisec.codyze.config.Configuration.initConfig( - configuration.configFile.path.toFile(), - *cmdlineArgs, - "--default-passes", - "--passes+", - "IdentifierPass", - "--passes+", - "EdgeCachePass", - ) - - // do not allow LSP/TUI - if (config.executionMode.isLsp || config.executionMode.isTui) { - logger.warn { "Forbidden execution mode, changing to CLI" } - config.executionMode.isCli = true - config.executionMode.isLsp = false - config.executionMode.isTui = false - } - - val server = AnalysisServer(config) - server.start() - val ctx = server.analyze(config.source)[config.timeout, TimeUnit.MINUTES] - val findings = ctx.findings - - environment = configuration.ci - if (environment == Configuration.Environment.NONE) verifyEnvironment() - addEnvironmentVariables() - fetchGitHash() - printFindings(findings, config) - - parseMapping("mappings.txt") - - // convert the Set<Metric> to a List<String> consisting of the metric names - val metricNameList = - connection - .fetchMetrics() - .stream() - .map { m: Metric -> m.name } - .filter { n: String? -> n != null } - .map { n: String? -> n!! } - .toList() - val assessmentTool = createAssessmentTool(metricNameList) - val evidence = createEvidence(config.output) - val results = - findings - .stream() - .map { f -> findingToAR(f, evidence.id ?: "") } - .toList() - .filterNotNull() - .toTypedArray() - - // FIXME not implemented in orchestrator yet - // connection.registerAssessmentTool(assessmentTool) - connection.storeEvidence(evidence) - connection.sendAssessmentResults(results) - - // Return code based on the existence of violations - return if (findings.stream().anyMatch { obj: Finding -> obj.isProblem }) 1 else 0 - } -} - -private val logger = KotlinLogging.logger {} - -/** - * Entry point of the program - * @args cli arguments provided - * @author Florian Wendland - * @author Robert Haimerl - */ -fun main(args: Array<String>) { - // initialize configuration - val config = Configuration.initialize(args) - // parse remaining CLI options and overwrite defaults and options from config file - val returnValue = - CommandLine(CodyzeMedina(config, args)).setUnmatchedArgumentsAllowed(true).execute(*args) - logger.info { "Exit with return value: $returnValue" } - exitProcess(returnValue) -} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/Connection.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/Connection.kt deleted file mode 100644 index ec560a2..0000000 --- a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/Connection.kt +++ /dev/null @@ -1,329 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -/* - * Copyright (c) 2022, Fraunhofer AISEC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * _____ _ - * / ____| | | - * | | ___ __| |_ _ _______ - * | | / _ \ / _` | | | |_ / _ \ - * | |___| (_) | (_| | |_| |/ / __/ - * \_____\___/ \__,_|\__, /___\___| - * __/ | - * |___/ - * - * This file is part of the MEDINA Framework. - */ -package de.fraunhofer.aisec.codyze.medina - -import de.fraunhofer.aisec.codyze.medina.auth.* -import java.net.InetAddress -import java.net.InetSocketAddress -import java.net.Socket -import java.net.URL -import mu.KotlinLogging -import org.dmfs.oauth2.client.OAuth2AccessToken -import org.openapitools.client.evidence.api.EvidenceStoreApi -import org.openapitools.client.evidence.model.Evidence -import org.openapitools.client.orchestrator.api.OrchestratorApi -import org.openapitools.client.orchestrator.model.AssessmentResult -import org.openapitools.client.orchestrator.model.AssessmentTool -import org.openapitools.client.orchestrator.model.Metric - -/** - * the Connection Object handles the OAuth2 Token as well as the communication with the Orchestrator - * @author Robert Haimerl - * @param orchestratorURL the URL of the Orchestrator instance - * @param tokenURL the URL of the OAuth2 instance - * @param username the OAuth2 username - * @param password the OAuth2 password - */ -class Connection( - private val orchestratorURL: String, - private val tokenURL: String, - username: String, - password: String -) { - private val logger = KotlinLogging.logger {} - private val authClient = getOAuth2Client(tokenURL, username, password) - private val orchestratorClient = - org.openapitools.client.orchestrator.ApiClient().setBasePath(orchestratorURL) - private val orchestratorApi = OrchestratorApi(orchestratorClient) - private val evidenceClient = - org.openapitools.client.evidence.ApiClient().setBasePath(orchestratorURL) - private val evidenceApi = EvidenceStoreApi(evidenceClient) - // Fetches a token instantly after creating the Connection object - private var token: OAuth2AccessToken? = getToken() - - /** - * Test the connection to the OAuth2-Servers - * @author Robert Haimerl - * @return whether a connection could be established - */ - fun testOAuthConnection(): Boolean { - logger.info { "Testing the connection to the OAuth-Server" } - // fist do a simple ping - val tokenHost = URL(tokenURL).host - if (!isReachable(InetAddress.getByName(tokenHost), 80, 2000)) { - logger.error { "Failed to ping OAuth-Server" } - return false - } - // then try to request a token with the given credentials - if (getAccessToken(authClient) == null) { - logger.error { "Failed to retrieve a token with the given credentials" } - return false - } - return true - } - - /** - * Test the connection to the Orchestrator-Server - * @author Robert Haimerl - * @return whether a connection could be established - */ - fun testOrchestratorConnection(): Boolean { - logger.info { "Testing the connection to the Orchestrator" } - // first ping the server - val orchestratorHost = URL(orchestratorURL).host - if (!isReachable(InetAddress.getByName(orchestratorHost), 80, 2000)) { - logger.error { "Failed to ping orchestrator" } - return false - } - // then try to make a simple get request - applyToken() - return isTokenValid() - } - - /** - * retrieves access token with the client created by the given credentials - * @author Robert Haimerl - * @return the token or null if there was an error - */ - private fun getToken(): OAuth2AccessToken? { - return getAccessToken(authClient) - } - - /** - * applies the OAuth2-Token to both the Orchestrator Client and the Evidence Client - * @author Robert Haimerl - * @return whether there was a token that could be applied - */ - private fun applyToken(): Boolean { - val tk = token ?: return false - orchestratorClient.addDefaultHeader( - "Authorization", - "${tk.tokenType()} ${tk.accessToken()}" - ) - evidenceClient.addDefaultHeader("Authorization", "${tk.tokenType()} ${tk.accessToken()}") - logger.info { "Access token successfully applied to the connection" } - return true - } - - /** - * Sends the AssessmentResults to the orchestrator - * @author Robert Haimerl - * @param ar the AssessmentResults to send - * @param retryOnError whether it should retry the process on a connection error - * @return whether sending was successful - */ - fun sendAssessmentResults(ar: Array<AssessmentResult>?, retryOnError: Boolean = true): Boolean { - if (ar == null || ar.isEmpty()) { - logger.warn { "No AssessmentResults to send" } - return true - } - if (token == null) { - logger.error { "No auth token: aborting" } - return false - } - if (!isTokenValid()) { - logger.warn { "Token invalid, requesting a new one" } - token = getToken() - applyToken() - } - var nextElem = 0 - try { - logger.info { - "trying to send AssessmentResults to ${orchestratorApi.apiClient.basePath}" - } - for (result in ar) { - orchestratorApi.orchestratorStoreAssessmentResult(result) - nextElem++ - logger.debug { "Sent assessment result with id " + result.id } - } - } catch (ae: org.openapitools.client.orchestrator.ApiException) { - logger.error { "Could not send to ${orchestratorApi.apiClient.basePath}" } - return if (retryOnError && ae.code == 401) { - logger.info { "Retrying transmission process..." } - // retries the transmission process starting from the failed element - sendAssessmentResults(ar.copyOfRange(nextElem, ar.size), false) - } else false - } - logger.info { "All assessment results sent to ${orchestratorApi.apiClient.basePath}" } - return true - } - - // FIXME: methods surrounding assessment tools seem to be unimplemented - /** - * Registers an AssessmentTool. This previously checks whether the tool was already registered - * @author Robert Haimerl - * @param tool the AssessmentTool to register - * @return whether the tool could be registered successfully - */ - fun registerAssessmentTool(tool: AssessmentTool): Boolean { - if (!confirmToken()) return false - logger.info { "Trying to set the assessment tool to \"${tool.name}\" (${tool.id})" } - // first check whether the tool is already registered - return try { - val registeredTool = orchestratorApi.orchestratorGetAssessmentTool(tool.id) - if (registeredTool == null) { - // if no such tool is known (null returned): try to register - try { - orchestratorApi.orchestratorRegisterAssessmentTool(tool) - logger.info { "Successfully registered new tool" } - true - } catch (e: org.openapitools.client.orchestrator.ApiException) { - logger.error { "Failed to register assessment tool" } - false - } - } else if (!registeredTool.equals(tool)) { - // if the id is known, but it is not identical: try to update - try { - orchestratorApi.orchestratorUpdateAssessmentTool(tool.id, tool) - logger.info { "Successfully updated the tool" } - true - } catch (e: org.openapitools.client.orchestrator.ApiException) { - logger.error { "Failed to update assessment tool" } - false - } - } else { - // if the tool is known and identical: no action required - logger.info { "Tool was already registered" } - true - } - } catch (e: org.openapitools.client.orchestrator.ApiException) { - logger.error { "Failed to fetch assessment tool with id ${tool.id}" } - false - } - } - - /** - * Stores the Evidence in the Orchestrator - * @author Robert Haimerl - * @param evidence the Evidence to store - * @return whether the Evidence could be successfully stored - */ - fun storeEvidence(evidence: Evidence): Boolean { - if (!confirmToken()) return false - return try { - val res = evidenceApi.evidenceStoreStoreEvidence(evidence) - logger.info { "Successfully stored evidence with id ${evidence.id}" } - logger.info { - "Response has status '${res.status}' with status message '${res.statusMessage}'" - } - true - } catch (e: org.openapitools.client.evidence.ApiException) { - logger.error { "Failed to store evidence with id ${evidence.id}" } - false - } - } - - /** - * Fetches all available metrics from the catalog - * @author Robert Haimerl - * @return a Set of the metrics - */ - fun fetchMetrics(): Set<Metric> { - if (!confirmToken()) return setOf() - val metricSet = mutableSetOf<Metric>() - var nextPage: String? = null - try { - do { - val response = orchestratorApi.orchestratorListMetrics(null, nextPage, null, null) - if (response.metrics != null) metricSet.addAll(response.metrics!!) - nextPage = response.nextPageToken - } while (nextPage != null) - } catch (e: org.openapitools.client.orchestrator.ApiException) { - logger.error { "Failed to fetch the metrics from the catalog" } - } catch (e: ClassCastException) { - logger.error { "Response could not be parsed properly" } - } - return metricSet - } - - /** - * Checks if the given address/port combination is reachable. Used since some hosts do not - * respond to regular isReachable on port 7 - * @author Robert Haimerl - * @param addr the INetAddress we try to ping - * @param port the according port - * @param timeout when every attempt should time out (a maximum of two connection attempts are - * made) - * @return whether the address could be reached - */ - @Suppress("SameParameterValue") - private fun isReachable(addr: InetAddress, port: Int = 0, timeout: Int = 2000): Boolean { - // first try default isReachable (which tries ICMP or port 7) - if (!addr.isReachable(timeout)) { - // otherwise, try manual connection to specified port - if (port < 0 || port > 65535) return false - val sockaddr = InetSocketAddress(addr, port) - val sock = Socket() - try { - sock.connect(sockaddr, timeout) - } catch (exc: Exception) { - return false - } - } - return true - } - - /** - * Checks if the current OAuth-token is still valid (uses a simple GET call) - * @author Robert Haimerl - * @returns false on a APIException - */ - private fun isTokenValid(): Boolean { - return try { - applyToken() - orchestratorApi.orchestratorListAssessmentResults(1, null, null, null) - true - } catch (e: org.openapitools.client.orchestrator.ApiException) { - e.printStackTrace() - false - } - } - - /** - * Checks the OAuth-token and requests a new one if the current token is invalid - * @author Robert Haimerl - * @return false if we could not get a valid token - */ - private fun confirmToken(): Boolean { - if (token == null) { - logger.error { "No auth token: aborting" } - return false - } - if (!isTokenValid()) { - logger.warn { "Token invalid, requesting a new one" } - token = getToken() - applyToken() - if (!isTokenValid()) { - logger.error { "Re-requested token is also invalid: aborting" } - return false - } - } - return true - } -} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/assembling/Assembler.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/assembling/Assembler.kt new file mode 100644 index 0000000..2767a96 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/assembling/Assembler.kt @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.assembling + +import de.fraunhofer.aisec.codyze.analysis.Finding +import de.fraunhofer.aisec.codyze.medina.evaluation.EvaluationResult +import de.fraunhofer.aisec.codyze.medina.evaluation.Rule +import de.fraunhofer.aisec.codyze.medina.main.CodyzeMedina +import de.fraunhofer.aisec.codyze.medina.mapping.Mapping +import de.fraunhofer.aisec.codyze.medina.mapping.RESOURCE_TYPE +import de.fraunhofer.aisec.codyze.medina.mapping.Type +import io.github.oshai.kotlinlogging.KotlinLogging +import java.io.IOException +import java.nio.file.Path +import java.util.* +import kotlin.io.path.readText +import org.openapitools.client.evidence.model.Evidence +import org.openapitools.client.orchestrator.model.AssessmentResult +import org.openapitools.client.orchestrator.model.MetricConfiguration + +class Assembler(private val environment: Environment, private val serviceId: String) { + private val logger = KotlinLogging.logger {} + private val toolId = "codyze-medina-${CodyzeMedina.CODYZE_VERSION}" + + /** + * Creates an Evidence based on a Codyze or MEDINA result + * + * @param resultFilePath the path to the results file used as evidence + * @return the resulting Evidence + * @author Robert Haimerl + */ + fun createEvidence(resultFilePath: Path): Evidence { + logger.debug { "Creating new Evidence based on $resultFilePath" } + val ev = Evidence() + + ev.id = UUID.randomUUID().toString() + ev.timestamp = java.time.OffsetDateTime.now() + ev.cloudServiceId = serviceId + ev.toolId = toolId + ev.raw = + try { + resultFilePath.readText() + } catch (e: IOException) { + logger.warn { + "Evidence with id ${ev.id} has no raw content since the results file at $resultFilePath could not be read" + } + "" + } + ev.resource = + object { + @Suppress("unused") val id = environment.getGitHash() + } + return ev + } + + /** + * Creates an AssessmentResult from the EvaluationResult creating from evaluating a MEDINA + * metric + * + * @param rule The rule covered by this result + * @param evaluationResult The result of the evaluation + * @param evidenceId the id of the Evidence belonging to the AssessmentResult + * @return the resulting AssessmentResult or null if not included in mapping.txt + * @author Robert Haimerl + */ + fun convertEvaluationToAssessmentResult( + rule: Rule, + evaluationResult: EvaluationResult, + evidenceId: String, + hash: String + ): AssessmentResult { + logger.debug { + "Creating AssessmentResult with evidence $evidenceId from result $evaluationResult" + } + + val metricConfiguration = MetricConfiguration() + metricConfiguration.operator = rule.operator + metricConfiguration.targetValue = rule.targetValue + metricConfiguration.isDefault = true + metricConfiguration.updatedAt = java.time.OffsetDateTime.now() + metricConfiguration.metricId = rule.name + metricConfiguration.cloudServiceId = serviceId + + val assessmentResult = AssessmentResult() + assessmentResult.metricConfiguration = metricConfiguration + assessmentResult.id = UUID.randomUUID().toString() + assessmentResult.timestamp = java.time.OffsetDateTime.now() + assessmentResult.evidenceId = evidenceId + assessmentResult.resourceId = hash + assessmentResult.addResourceTypesItem(RESOURCE_TYPE) + assessmentResult.metricId = rule.name + assessmentResult.compliant = evaluationResult.isValidated() + assessmentResult.nonComplianceComments = evaluationResult.getDetailedMessage() + assessmentResult.cloudServiceId = serviceId + assessmentResult.toolId = toolId + + return assessmentResult + } + + /** + * Creates AssessmentResults from a Finding + * + * @param findings the Findings produced by Codyze + * @param evidenceId the id of the Evidence belonging to the AssessmentResult + * @return the resulting AssessmentResult or null if not included in mapping.txt + * @author Robert Haimerl + */ + fun convertFindingsToAssessmentResults( + mapping: Mapping, + findings: Set<Finding>, + evidenceId: String, + hash: String + ): Array<AssessmentResult> { + logger.debug { "Creating AssessmentResults with evidence $evidenceId from set of Findings" } + + val results = mutableListOf<AssessmentResult>() + val findingIdentifiers = findings.map { it.identifier }.toList() + for (metric in mapping.metrics) { + logger.debug { "Creating an AssessmentResult for the metric ${metric.name}" } + // considers metric only if all specified rules exist as Findings + if (metric.rules.all { findingIdentifiers.contains(it) }) { + val metricConfiguration = MetricConfiguration() + metricConfiguration.operator = metric.configuration.operator + metricConfiguration.isDefault = metric.configuration.default + // map the target values to their defined type - falls back to String if double + // cannot be parsed + val target = + when (metric.configuration.type) { + Type.NUMBER -> + try { + metric.configuration.target.map { it.toString().toDouble() } + } catch (nfe: NumberFormatException) { + logger.error { + "Error while trying to parse defined target of ${metric.name} (${metric.configuration.target}) as a number. Falling back to String target." + } + metric.configuration.target.map { it.toString() } + } + Type.BOOLEAN -> metric.configuration.target.map { it.toString() == "true" } + else -> metric.configuration.target.map { it.toString() } + } + // give the target value or a list of values if specified + metricConfiguration.targetValue = if (target.size == 1) target[0] else target + metricConfiguration.updatedAt = java.time.OffsetDateTime.now() + metricConfiguration.metricId = metric.name + metricConfiguration.cloudServiceId = serviceId + + val assessmentResult = AssessmentResult() + assessmentResult.metricConfiguration = metricConfiguration + assessmentResult.id = UUID.randomUUID().toString() + assessmentResult.cloudServiceId = serviceId + assessmentResult.timestamp = java.time.OffsetDateTime.now() + assessmentResult.evidenceId = evidenceId + assessmentResult.resourceId = hash + assessmentResult.addResourceTypesItem(RESOURCE_TYPE) + assessmentResult.metricId = metric.name + // only see the metric as compliant if all of its rules are + val relevantFindings = findings.filter { metric.rules.contains(it.identifier) } + assessmentResult.compliant = relevantFindings.all { !it.isProblem } + // merges all log messages of findings that failed + assessmentResult.nonComplianceComments = + relevantFindings + .filter { it.isProblem } + .map { "${it.identifier}: ${it.logMsg}\n" } + .fold(StringBuilder()) { text, line -> text.append(line) } + .toString() + assessmentResult.toolId = toolId + + results.add(assessmentResult) + } + } + return results.toTypedArray() + } +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/assembling/Environment.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/assembling/Environment.kt new file mode 100644 index 0000000..3694b25 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/assembling/Environment.kt @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.assembling + +import de.fraunhofer.aisec.codyze.medina.main.Configuration.CIEnvironment +import io.github.oshai.kotlinlogging.KotlinLogging +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader +import java.nio.file.Path +import kotlin.io.path.absolute + +class Environment(private var ciEnvironment: CIEnvironment, private val configPath: Path) { + // This lateinit variable is initialized at the first call of getGitHash() + private lateinit var gitHash: String + private val logger = KotlinLogging.logger {} + private var environmentVariables = mutableMapOf<String, String>() + + init { + if (ciEnvironment == CIEnvironment.NONE) { + ciEnvironment = getEnvironment() + } + addEnvironmentVariables() + } + + /** + * Tries multiple known environment variables to find out which CI/CD environment is being used + * Currently possible values for environment are ["None", "GitHub", "GitLab", "Jenkins"]. This + * method sets the "environment"-variable and fills the "environmentVariables"-Map specific to + * this environment + * + * @return the matched CIEnvironment + * @author Robert Haimerl + */ + private fun getEnvironment(): CIEnvironment { + // all known CI/CD environments set "CI" to "true" + if (System.getenv("CI") != "true") { + logger.warn { "No CI/CD environment recognized" } + return CIEnvironment.NONE + } + + val githubVars = arrayOf("GITHUB_ACTION", "GITHUB_JOB", "GITHUB_SHA") + val gitlabVars = arrayOf("CI_PIPELINE_ID", "CI_JOB_ID", "CI_COMMIT_SHA") + val jenkinsVars = arrayOf("JOB_NAME", "BUILD_ID", "JENKINS_URL") + + if (githubVars.all { variable -> System.getenv(variable) != null }) { + logger.info { "Determined CI/CD environment to be GitHub" } + return CIEnvironment.GITHUB + } else if (gitlabVars.all { variable -> System.getenv(variable) != null }) { + logger.info { "Determined CI/CD environment to be GitLab" } + return CIEnvironment.GITLAB + } else if (jenkinsVars.all { variable -> System.getenv(variable) != null }) { + logger.info { "Determined CI/CD environment to be Jenkins" } + return CIEnvironment.JENKINS + } + + logger.warn { "CI/CD environment could not be determined" } + return CIEnvironment.NONE + } + + /** + * Adds all relevant environment variables for the detected CI environment to the Map + * + * @author Robert Haimerl + */ + private fun addEnvironmentVariables() { + // jenkins: "SCM-specific variables such as GIT_COMMIT are not automatically defined as + // environment variables; rather you can use the return value of the checkout step" + when (ciEnvironment) { + CIEnvironment.GITHUB -> { + environmentVariables["commit_hash"] = "GITHUB_SHA" + } + CIEnvironment.GITLAB -> { + environmentVariables["commit_hash"] = "CI_COMMIT_SHA" + } + CIEnvironment.JENKINS -> { + environmentVariables["commit_hash"] = "GIT_COMMIT" + } + else -> return + } + } + + /** + * This function tries to initialize the git hash exactly once, after that it will just refer to + * the result of the first attempt. This removes overhead by trying to figure out the commit + * hash every time anew. + * + * The function tries environment variables set by GitHub actions, then GitLab CI/CD before + * ultimately falling back to console commands. + * + * @return The git hash or "invalid" + * @author Robert Haimerl + */ + fun getGitHash(): String { + if (!::gitHash.isInitialized) { + var hash: String? + // Default to "GITHUB_SHA" to prevent NullPointerExceptions from being thrown + hash = System.getenv(environmentVariables["commit_hash"] ?: "GITHUB_SHA") + if (hash == null) { + logger.warn { "Git commit hash could not be parsed from the CI Environment" } + hash = + try { + // Use the location of the configuration file in order to resolve the git + // information + val process = + Runtime.getRuntime() + .exec("git rev-parse HEAD", null, configPath.absolute().toFile()) + val reader = BufferedReader(InputStreamReader(process.inputStream)) + reader.readLine() + } catch (e: IOException) { + logger.error(e) { + "IOException while trying to manually resolve the git commit hash: ${e.localizedMessage}" + } + null + } + } + // If we couldn't figure hash out, log error and return "invalid" + gitHash = + if (hash == null) { + logger.error { "Failed to determine an associated Git commit hash" } + "invalid" + } else { + hash + } + } + return gitHash + } +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/auth/Auth.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/auth/Auth.kt deleted file mode 100644 index 76e762a..0000000 --- a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/auth/Auth.kt +++ /dev/null @@ -1,104 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -/* - * Copyright (c) 2022, Fraunhofer AISEC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * _____ _ - * / ____| | | - * | | ___ __| |_ _ _______ - * | | / _ \ / _` | | | |_ / _ \ - * | |___| (_) | (_| | |_| |/ / __/ - * \_____\___/ \__,_|\__, /___\___| - * __/ | - * |___/ - * - * This file is part of the MEDINA Framework. - */ -package de.fraunhofer.aisec.codyze.medina.auth - -import java.net.URI -import mu.KotlinLogging -import org.dmfs.httpessentials.client.HttpRequestExecutor -import org.dmfs.httpessentials.httpurlconnection.HttpUrlConnectionExecutor -import org.dmfs.oauth2.client.* -import org.dmfs.oauth2.client.grants.ClientCredentialsGrant -import org.dmfs.oauth2.client.grants.TokenRefreshGrant -import org.dmfs.oauth2.client.scope.BasicScope -import org.dmfs.rfc3986.uris.EmptyUri -import org.dmfs.rfc5545.Duration - -private val logger = KotlinLogging.logger {} - -/** - * Creates an OAuth2Client using the URL, username and password. The default Token ttl is set to 10 - * hours - * @author Florian Wendland - * @author Robert Haimerl - * @param tokenURL the URL of the token-granting server - * @param username the username used for authentication - * @param password the password used for authentication - * @return an OAuth2Client used to request tokens - */ -fun getOAuth2Client(tokenURL: String, username: String, password: String): OAuth2Client { - // create OAuth2 provider - val pv: OAuth2AuthorizationProvider = - BasicOAuth2AuthorizationProvider( - null, - URI.create(tokenURL), - Duration(1, 0, 36000) // token expires after 10 hours - ) - // Create OAuth2 client credentials - val cred: OAuth2ClientCredentials = BasicOAuth2ClientCredentials(username, password) - // Create OAuth2 client - return BasicOAuth2Client(pv, cred, EmptyUri()) -} - -/** - * Requests a new access-token - * @author Robert Haimerl - * @param client an OAuth2Client containing the corresponding URL and credentials - * @return an OAuth2AccessToken or <code>null</code> if no token could be retrieved - */ -fun getAccessToken(client: OAuth2Client): OAuth2AccessToken? { - // Create HttpRequestExecutor - val ex: HttpRequestExecutor = HttpUrlConnectionExecutor() - // Client Credentials Grant - return try { - ClientCredentialsGrant(client, BasicScope()).accessToken(ex) - } catch (e: Exception) { - logger.error { "Could not retrieve access token with the given credentials: $e" } - null - } -} - -// Currently, not implemented in the orchestrator -/** - * Refreshes an expiring access-token - * @author Robert Haimerl - * @param client an OAuth2Client containing the corresponding URL and credentials - * @param oldToken the token which should be replaced - * @return a new OAuth2AccessToken or null if no token could be retrieved - */ -fun refreshToken(client: OAuth2Client, oldToken: OAuth2AccessToken): OAuth2AccessToken? { - // Create HttpRequestExecutor - val ex: HttpRequestExecutor = HttpUrlConnectionExecutor() - // Request new access token, providing the previous one - return try { - TokenRefreshGrant(client, oldToken).accessToken(ex) - } catch (e: Exception) { - logger.error { "Could not refresh the access token: $e" } - null - } -} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/connection/ApiManager.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/connection/ApiManager.kt new file mode 100644 index 0000000..17d663a --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/connection/ApiManager.kt @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.connection + +import io.github.oshai.kotlinlogging.KotlinLogging +import java.net.InetAddress +import java.net.URL +import java.util.UUID +import org.openapitools.client.evidence.api.EvidenceStoreApi +import org.openapitools.client.evidence.model.Evidence +import org.openapitools.client.orchestrator.ApiException +import org.openapitools.client.orchestrator.api.OrchestratorApi +import org.openapitools.client.orchestrator.model.AssessmentResult +import org.openapitools.client.orchestrator.model.CloudService + +/** + * The ApiManager handles calls towards the orchestrator and evidence api + * + * @param connection the connection containing this manager + * @param orchestratorURL the URL of the Orchestrator instance + * @author Robert Haimerl + */ +class ApiManager(private val connection: Connection, private val orchestratorURL: String) { + private val logger = KotlinLogging.logger {} + private val orchestratorApi = + OrchestratorApi( + org.openapitools.client.orchestrator.ApiClient().setBasePath(orchestratorURL) + ) + private val evidenceApi = + EvidenceStoreApi(org.openapitools.client.evidence.ApiClient().setBasePath(orchestratorURL)) + + init { + orchestratorApi.customBaseUrl = orchestratorURL + evidenceApi.customBaseUrl = orchestratorURL + } + + /** + * Test the connection to the Orchestrator-Server + * + * @return whether a connection could be established + * @author Robert Haimerl + */ + fun testOrchestratorConnection(): Boolean { + logger.info { "Testing the connection to the Orchestrator" } + // first ping the server + val orchestratorHost = URL(orchestratorURL).host + if (!connection.isReachable(InetAddress.getByName(orchestratorHost), 80, 2000)) { + logger.error { "Failed to ping orchestrator" } + return false + } + // then try to make a apply the Token + return connection + .getTokenManager() + .applyToken(orchestratorApi.apiClient, evidenceApi.apiClient) + } + + /** + * Sends the AssessmentResults to the orchestrator + * + * @param ar the AssessmentResults to send + * @param retryOnError whether it should retry the process on a connection error + * @return whether sending was successful + * @author Robert Haimerl + */ + fun sendAssessmentResults(ar: Array<AssessmentResult>?, retryOnError: Boolean = true): Boolean { + if (ar.isNullOrEmpty()) { + logger.warn { "No AssessmentResults to send" } + return true + } + var nextElem = 0 + try { + logger.info { + "trying to send AssessmentResults to ${orchestratorApi.apiClient.basePath}" + } + for (result in ar) { + orchestratorApi.orchestratorStoreAssessmentResult(result) + nextElem++ + logger.debug { "Sent assessment result with id " + result.id } + } + } catch (ae: org.openapitools.client.orchestrator.ApiException) { + logger.error(ae) { + "Could not send to ${orchestratorApi.apiClient.basePath}: ${ae.localizedMessage}" + } + return if (retryOnError && ae.code == 401) { + logger.info { "Retrying transmission process..." } + // retries the transmission process starting from the failed element + sendAssessmentResults(ar.copyOfRange(nextElem, ar.size), false) + } else false + } + logger.info { "All assessment results sent to ${orchestratorApi.apiClient.basePath}" } + return true + } + /** + * Stores the Evidence in the Orchestrator + * + * @param evidence the Evidence to store + * @return whether the Evidence could be successfully stored + * @author Robert Haimerl + */ + fun storeEvidence(evidence: Evidence): Boolean { + return try { + evidenceApi.evidenceStoreStoreEvidence(evidence) + logger.info { "Successfully stored evidence with id ${evidence.id}" } + true + } catch (e: org.openapitools.client.evidence.ApiException) { + logger.error(e) { + "Failed to store evidence with id ${evidence.id}: ${e.localizedMessage}" + } + false + } + } + + /** + * Tries to get a CloudService defined by its id + * + * @param id The unique UUID of the cloud service + * @return The CloudService defined by the provided id + * @author Robert Haimerl + */ + fun getCloudServiceById(id: UUID): CloudService? { + return try { + orchestratorApi.orchestratorGetCloudService(id.toString()) + } catch (ae: ApiException) { + logger.error(ae) { "Failed to resolve Cloud Service Id ($id): ${ae.localizedMessage}" } + null + } + } + + fun getOrchestratorApi(): OrchestratorApi { + return orchestratorApi + } + + fun getEvidenceApi(): EvidenceStoreApi { + return evidenceApi + } +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/connection/Connection.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/connection/Connection.kt new file mode 100644 index 0000000..fecb41e --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/connection/Connection.kt @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.connection + +import io.github.oshai.kotlinlogging.KotlinLogging +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Socket + +class Connection(orchestratorURL: String, tokenURL: String, username: String, password: String) { + private val logger = KotlinLogging.logger {} + private val apiManager: ApiManager = ApiManager(this, orchestratorURL) + private val oAuthManager: OAuthManager = OAuthManager(this, tokenURL, username, password) + private val tokenManager: TokenManager = TokenManager(this) + + /** + * Checks if the given address/port combination is reachable. Used since some hosts do not + * respond to regular isReachable on port 7 + * + * @param address the INetAddress we try to ping + * @param port the according port + * @param timeout when every attempt should time out (a maximum of two connection attempts are + * made) + * @return whether the address could be reached + * @author Robert Haimerl + */ + @Suppress("SameParameterValue") + fun isReachable(address: InetAddress, port: Int = 0, timeout: Int = 2000): Boolean { + logger.debug { "Testing whether $address:$port is reachable" } + // first try default isReachable (which tries ICMP or port 7) + if (!address.isReachable(timeout)) { + logger.debug { "Could not reach $address:$port, manually retrying" } + // otherwise, try manual connection to specified port + if (port < 0 || port > 65535) return false + val sockAddress = InetSocketAddress(address, port) + val sock = Socket() + try { + sock.connect(sockAddress, timeout) + } catch (exc: Exception) { + logger.warn { "Address $address:$port is unreachable" } + return false + } + } + logger.info { "Address $address:$port can be reached" } + return true + } + + fun getApiManager(): ApiManager { + return this.apiManager + } + + fun getTokenManager(): TokenManager { + return this.tokenManager + } + + fun getOAuthManager(): OAuthManager { + return this.oAuthManager + } +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/connection/OAuthManager.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/connection/OAuthManager.kt new file mode 100644 index 0000000..f8953fe --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/connection/OAuthManager.kt @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.connection + +import io.github.oshai.kotlinlogging.KotlinLogging +import java.net.InetAddress +import java.net.URI +import java.net.URL +import org.dmfs.oauth2.client.* +import org.dmfs.rfc3986.uris.EmptyUri +import org.dmfs.rfc5545.Duration + +/** + * The OAuthManager handles the authentication process. Closely intertwined with the TokenManager + * + * @param connection the connection containing this manager + * @param tokenURL the URL of the OAuth2 instance + * @param username the OAuth2 username + * @param password the OAuth2 password + * @author Robert Haimerl + */ +class OAuthManager( + private val connection: Connection, + private val tokenURL: String, + username: String, + password: String +) { + private val logger = KotlinLogging.logger {} + // Creates the authClient instantly after creating the TokenManager + private val authClient = createOAuth2Client(tokenURL, username, password) + + /** + * Test the connection to the OAuth2-Servers + * + * @return whether a connection could be established + * @author Robert Haimerl + */ + fun testOAuthConnection(): Boolean { + logger.info { "Testing the connection to the OAuth-Server" } + // fist do a simple ping + val tokenHost = URL(tokenURL).host + if (!connection.isReachable(InetAddress.getByName(tokenHost), 80, 2000)) { + logger.error { "Failed to ping OAuth-Server" } + return false + } + // then try to request a token with the given credentials + if (connection.getTokenManager().requestToken(authClient) == null) { + logger.error { "Failed to retrieve a token with the given credentials" } + return false + } + return true + } + + /** + * Creates an OAuth2Client using the URL, username and password. The default Token ttl is set to + * 10 hours + * + * @param tokenURL the URL of the token-granting server + * @param username the username used for authentication + * @param password the password used for authentication + * @return an OAuth2Client used to request tokens + * @author Florian Wendland + * @author Robert Haimerl + */ + private fun createOAuth2Client( + tokenURL: String, + username: String, + password: String + ): OAuth2Client { + // create OAuth2 provider + val pv: OAuth2AuthorizationProvider = + BasicOAuth2AuthorizationProvider( + null, + URI.create(tokenURL), + Duration(1, 0, 36000) // token expires after 10 hours + ) + // Create OAuth2 client credentials + val cred: OAuth2ClientCredentials = BasicOAuth2ClientCredentials(username, password) + // Create OAuth2 client + val client = BasicOAuth2Client(pv, cred, EmptyUri()) + logger.info { "Successfully created the OAuth2-Client" } + return client + } + + fun getAuthClient(): OAuth2Client { + return this.authClient + } +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/connection/TokenManager.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/connection/TokenManager.kt new file mode 100644 index 0000000..f2b0546 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/connection/TokenManager.kt @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.connection + +import io.github.oshai.kotlinlogging.KotlinLogging +import org.dmfs.httpessentials.client.HttpRequestExecutor +import org.dmfs.httpessentials.httpurlconnection.HttpUrlConnectionExecutor +import org.dmfs.oauth2.client.OAuth2AccessToken +import org.dmfs.oauth2.client.OAuth2Client +import org.dmfs.oauth2.client.grants.ClientCredentialsGrant +import org.dmfs.oauth2.client.scope.BasicScope + +/** + * the OAuthManager handles the token necessary for the authentication process + * + * @param connection the connection containing this manager + * @author Robert Haimerl + */ +class TokenManager(private val connection: Connection) { + private val logger = KotlinLogging.logger {} + // Fetches a token instantly after creating the TokenManager + private var token: OAuth2AccessToken? = + requestToken(connection.getOAuthManager().getAuthClient()) + + init { + if ( + !applyToken( + connection.getApiManager().getOrchestratorApi().apiClient, + connection.getApiManager().getEvidenceApi().apiClient, + token + ) + ) { + logger.error { + "Failed to apply the OAuth2 access token while initializing the TokenManager" + } + } + } + + /** + * Requests a new access-token + * + * @param client an OAuth2Client containing the corresponding URL and credentials + * @return an OAuth2AccessToken or <code>null</code> if no token could be retrieved + * @author Robert Haimerl + */ + fun requestToken(client: OAuth2Client): OAuth2AccessToken? { + // Create HttpRequestExecutor + val ex: HttpRequestExecutor = HttpUrlConnectionExecutor() + // Client Credentials Grant + val token = + try { + ClientCredentialsGrant(client, BasicScope()).accessToken(ex) + } catch (e: Exception) { + logger.error(e) { + "Could not retrieve access token with the given credentials: ${e.localizedMessage}" + } + null + } + logger.info { "Successfully requested an access token for the OAuth2-Connection" } + return token + } + + /** + * applies the OAuth2-Token to both the Orchestrator Client and the Evidence Client + * + * @return whether there was a token that could be applied + * @author Robert Haimerl + */ + fun applyToken( + orchestratorClient: org.openapitools.client.orchestrator.ApiClient, + evidenceClient: org.openapitools.client.evidence.ApiClient, + token: OAuth2AccessToken? = this.token + ): Boolean { + val tk = + token + ?: run { + logger.warn { "Tried to apply the OAuth2 access token before it was available" } + return false + } + orchestratorClient.addDefaultHeader( + "Authorization", + "${tk.tokenType()} ${tk.accessToken()}" + ) + evidenceClient.addDefaultHeader("Authorization", "${tk.tokenType()} ${tk.accessToken()}") + logger.info { "Access token successfully applied to the connection" } + return true + } +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/EvaluationResult.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/EvaluationResult.kt new file mode 100644 index 0000000..e327a2c --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/EvaluationResult.kt @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.evaluation + +/** + * The result of the evaluation, its detailedMessage is being used as input for the evidence `raw` + */ +class EvaluationResult( + private val validated: Boolean, + private val message: String, + private val detailedMessage: String +) { + fun isValidated(): Boolean { + return this.validated + } + + fun getMessage(): String { + return this.message + } + + fun getDetailedMessage(): String { + return this.detailedMessage + } + + override fun toString(): String { + return "${if (!validated) "IN" else ""}VALID | [$message] | [$detailedMessage]" + } +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/Evaluator.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/Evaluator.kt new file mode 100644 index 0000000..c214c05 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/Evaluator.kt @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.evaluation + +import de.fraunhofer.aisec.codyze.medina.assembling.Environment +import de.fraunhofer.aisec.codyze.medina.evaluation.base.* +import de.fraunhofer.aisec.codyze.medina.mapping.MedinaMapping +import io.github.oshai.kotlinlogging.KotlinLogging +import java.io.File +import java.lang.IllegalArgumentException +import java.nio.file.Path +import kotlin.reflect.KClass +import kotlin.reflect.full.primaryConstructor + +class Evaluator(private val environment: Environment, private val base: Path) { + /** A mapping from the rule to its respective MetricEvaluator class */ + private val rulesToEvaluators: Map<Rule, KClass<out MetricEvaluator>> = + mapOf( + Rule.CodeSignoff to SignOffEvaluator::class, + Rule.SignedCommits to GPGSignatureEvaluator::class, + Rule.SignedSignoff to SignedSignOffEvaluator::class, + Rule.ApprovedCommitAuthor to ApprovedCommitAuthorEvaluator::class, + ) + + private val logger = KotlinLogging.logger {} + + /** + * Iterates over all metrics defined in the Medina Mapping and tries to evaluate them. When + * found metrics cannot be evaluated they are ignored and a matching log message is created + * + * @param medinaMapping the Medina Mapping as parsed from the respective file + * @return A map mapping each evaluated rule to its evaluation result + * @author Robert Haimerl + */ + fun evaluateAll(medinaMapping: MedinaMapping): Map<Rule, EvaluationResult> { + logger.info { "Starting Evaluation of MEDINA Metrics" } + val results = mutableMapOf<Rule, EvaluationResult>() + for (metric in medinaMapping.metrics) { + try { + val rule = Rule.valueOf(metric.name) + logger.debug { "Starting Evaluation of Metric ${metric.name}" } + // Return custom error message if git hash could not be resolved + if (environment.getGitHash() == "invalid") { + results[rule] = + EvaluationResult( + false, + "Git commit hash could not be resolved", + "Git commit hash could not be resolved" + ) + } else { + // call the evaluation function for the corresponding rule and pass the metric + // configuration + results[rule] = + rulesToEvaluators[rule]!! + .primaryConstructor!! + .call(environment, base) + .evaluate(metric.target) + } + } catch (ie: IllegalArgumentException) { + logger.warn { "No Rule for ${metric.name} could be found" } + } catch (npe: NullPointerException) { + logger.error { "No evaluation function for ${metric.name} was defined" } + } + } + return results + } + + /** + * Tries to import all files at the specified location into GPG as public keys + * + * @param keyLocation A PubKey-File or Directory containing public keys + * @author Robert Haimerl + */ + fun parseKeys(keyLocation: File) { + logger.info { "Trying to import public keys from ${keyLocation.path}" } + val files = + if (keyLocation.isDirectory) { + collectFiles(keyLocation) + } else { + setOf(keyLocation) + } + var success = 0 + for (key in files) { + val process = + Runtime.getRuntime() + .exec(arrayOf("gpg", "--import", key.canonicalPath), null, base.toFile()) + process.waitFor() + if (process.exitValue() != 0) { + logger.warn { "Failed to import public key file ${key.name}" } + } else { + success += 1 + } + } + logger.info { "Successfully registered $success keys" } + } + + /** + * Recursively collects all regular files within a directory into a set + * + * @param directory The starting directory + * @return A set of all regular files contained in the directory and all subdirectories + * @author Robert Haimerl + */ + private fun collectFiles(directory: File): Set<File> { + val files = mutableSetOf<File>() + + for (f in directory.walk()) { + if (f != directory && f.isDirectory) { + files += collectFiles(f) + } else if (f.isFile) { + files += f + } + } + return files + } +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/Rule.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/Rule.kt new file mode 100644 index 0000000..1cd6932 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/Rule.kt @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.evaluation + +/** + * The rules that can be evaluated. Each rule contains information about its associated operator and + * target type + * + * @param operator The operator used for evaluating the result of the rule + * @param targetValue The desired evaluation result + * @param id The stable and unique id of this rule, used for SARIF reports + * @param description A description of the rule, used for SARIF reports + */ +enum class Rule( + val operator: String, + val targetValue: Any, + val id: String, + val description: String +) { + CodeSignoff( + "==", + true, + "MD001", + "Whether the analyzed commit was signed off by a pre-specified subject" + ), + SignedCommits( + "==", + true, + "MD002", + "Whether the analyzed commit contains a valid signature from a pre-specified subject" + ), + SignedSignoff( + "==", + true, + "MD003", + "Whether the analyzed commit contains both a signoff and a valid signature from a pre-specified subject" + ), + ApprovedCommitAuthor( + "==", + true, + "MD004", + "Whether the analyzed commit comes from a permitted author" + ), +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/ApprovedCommitAuthorEvaluator.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/ApprovedCommitAuthorEvaluator.kt new file mode 100644 index 0000000..797adc8 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/ApprovedCommitAuthorEvaluator.kt @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.evaluation.base + +import de.fraunhofer.aisec.codyze.medina.assembling.Environment +import de.fraunhofer.aisec.codyze.medina.evaluation.EvaluationResult +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import java.nio.file.Path + +class ApprovedCommitAuthorEvaluator( + override val environment: Environment, + override val base: Path +) : MetricEvaluator() { + + override val logger: KLogger = KotlinLogging.logger {} + + /** + * Validates commit author against list of approved authors. + * + * @return true if author is approved; otherwise, false + * @author Florian Wendland + */ + override fun evaluate(target: Array<Any>): EvaluationResult { + val gitHash = environment.getGitHash() + val commitAuthor = getCommitAuthorName(gitHash) + + if (target.contains<Any?>(commitAuthor)) { + return EvaluationResult( + true, + "Commit from approved author: ${commitAuthor}", + "Commit from approved author: ${commitAuthor}, expected one of: ${target.contentToString()}" + ) + } + return EvaluationResult( + false, + "Commit from unapproved author: ${commitAuthor}", + "Commit from unapproved author: ${commitAuthor}, expected one of: ${target.contentToString()}" + ) + } +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/GPGSignatureEvaluator.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/GPGSignatureEvaluator.kt new file mode 100644 index 0000000..83d5b4d --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/GPGSignatureEvaluator.kt @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.evaluation.base + +import de.fraunhofer.aisec.codyze.medina.assembling.Environment +import de.fraunhofer.aisec.codyze.medina.evaluation.EvaluationResult +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import java.nio.file.Path + +class GPGSignatureEvaluator(override val environment: Environment, override val base: Path) : + MetricEvaluator() { + + override val logger: KLogger = KotlinLogging.logger {} + + override fun evaluate(target: Array<Any>): EvaluationResult { + val goodSignatures = + getGoodSignatures(getCommitSignatureInformation(environment.getGitHash())) + if (goodSignatures.isEmpty()) { + return EvaluationResult( + false, + "The commit contains no good signatures", + "was: [], expected one of: ${target.contentToString()}" + ) + } + for (signature in goodSignatures) { + if (target.contains(signature.first)) { + return EvaluationResult( + true, + "Commit signed by ${signature.second} <${signature.third}> with the key id ${signature.first}", + "was: ${goodSignatures.map { it.first }}, expected one of: ${target.contentToString()}" + ) + } + } + return EvaluationResult( + false, + "The commit contains good signatures, but none match the specified key ids", + "was: ${goodSignatures.map { it.first }}, expected one of: ${target.contentToString()}" + ) + } + + /** + * Tries to find good signatures in a gnupg status output as returned by + * *getCommitSignatureInformation* + * + * @param gpgStatus The gnupg status output like it is produced when calling git + * verify-signature + * @return A List of Triple<KeyId, Name, Email> that describes valid signatures of the commit + * @author Robert Haimerl + */ + internal fun getGoodSignatures(gpgStatus: List<String>): List<Triple<String, String, String>> { + return gpgStatus + .filter { line -> line.startsWith("[GNUPG:] GOODSIG") } + .map { line -> splitGoodSignatureLine(line) } + .toList() + } + + /** + * Splits a GOODSIG line of the gnupg status output, undefined return value for other argument + * Strings When an Exception occurs while trying to parse the keyId, it will be stored as -1 + * + * @param line The line of the output that shall be split + * @return The Triple<KeyId, Name, Email> + * @author Robert Haimerl + */ + private fun splitGoodSignatureLine(line: String): Triple<String, String, String> { + val splitLine = line.split(" ") + if (splitLine.size < 4) { + logger.error { "Signature line is too short to be parsed properly" } + return Triple("", "", "") + } + val keyId = splitLine[2] + val name = splitLine.subList(3, splitLine.size - 1).joinToString(" ") + val email = splitLine.last().substring(1, splitLine.last().length - 1) + return Triple(keyId, name, email) + } +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/MetricEvaluator.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/MetricEvaluator.kt new file mode 100644 index 0000000..c681cd7 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/MetricEvaluator.kt @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.evaluation.base + +import de.fraunhofer.aisec.codyze.medina.assembling.Environment +import de.fraunhofer.aisec.codyze.medina.evaluation.EvaluationResult +import io.github.oshai.kotlinlogging.KLogger +import java.io.BufferedReader +import java.io.InputStreamReader +import java.nio.file.Path + +abstract class MetricEvaluator { + + abstract val environment: Environment + abstract val base: Path + abstract val logger: KLogger + + /** + * Abstract evaluation function to be overwritten by each specific Evaluator + * + * @param target The target values for this evaluation + * @return An EvaluationResult containing a message and whether the metric could be validated + */ + abstract fun evaluate(target: Array<Any>): EvaluationResult + + /** + * Fetches the name of the commit author from the git console commands + * + * @return The full author name or null if it could not be found + * @author Robert Haimerl + */ + internal fun getCommitAuthorName(commitHash: String): String { + val process = + Runtime.getRuntime().exec("git show -s --format=%an $commitHash", null, base.toFile()) + val reader = BufferedReader(InputStreamReader(process.inputStream)) + return reader.readLine() + } + + /** + * Fetches the name of the commit author from the git console commands + * + * @return The full author name or null if it could not be found + * @author Robert Haimerl + */ + internal fun getCommitAuthorEmail(commitHash: String): String { + val process = + Runtime.getRuntime().exec("git show -s --format=%ae $commitHash", null, base.toFile()) + val reader = BufferedReader(InputStreamReader(process.inputStream)) + return reader.readLine() + } + + /** + * Searches the commit in question for sign-offs. A sign-off is identified by verifying whether + * the last line starts with the phrase "Signed-off-by: " + * + * @return A List of the found sign-off lines, may be empty + * @author Robert Haimerl + */ + internal fun getSignOff(commitHash: String): List<String> { + // try to read the commit message as console command + val process = + Runtime.getRuntime() + .exec("git show --pretty=format:\"%B\" --no-patch $commitHash", null, base.toFile()) + val commitMessageLines = + BufferedReader(InputStreamReader(process.inputStream)).readLines().toMutableList() + val signOffList: MutableList<String> = mutableListOf() + for (line in commitMessageLines) { + if (line.startsWith("Signed-off-by: ")) { + signOffList += line + } + } + return signOffList + } + + /** + * Calls git verify-commit raw and returns the response as a list of the separate lines of the + * response + * + * @param commitHash The commit of which we want to check the signature + * @return The separate lines of a machine-readable gpg status output, may be empty + * @author Robert Haimerl + */ + internal fun getCommitSignatureInformation(commitHash: String): List<String> { + val process = + Runtime.getRuntime().exec("git verify-commit --raw $commitHash", null, base.toFile()) + return BufferedReader(InputStreamReader(process.errorStream)).readLines().toList() + } +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/SignOffEvaluator.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/SignOffEvaluator.kt new file mode 100644 index 0000000..2c8e0f0 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/SignOffEvaluator.kt @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.evaluation.base + +import de.fraunhofer.aisec.codyze.medina.assembling.Environment +import de.fraunhofer.aisec.codyze.medina.evaluation.EvaluationResult +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import java.nio.file.Path + +class SignOffEvaluator(override val environment: Environment, override val base: Path) : + MetricEvaluator() { + + override val logger: KLogger = KotlinLogging.logger {} + + /** + * Tries to get all sign-offs of the current commit and compares it to the name and email + * associated with the commit author + * + * @return True only if the commit contains a sign-off which matches the expected author + * @author Robert Haimerl + */ + override fun evaluate(target: Array<Any>): EvaluationResult { + val gitHash = environment.getGitHash() + val signOffList = getSignOff(gitHash).map { parseSignOff(it) } + for (signOff in signOffList) { + if ( + signOff.first == getCommitAuthorName(gitHash) && + signOff.second == getCommitAuthorEmail(gitHash) && + target.contains(signOff.first) + ) + return EvaluationResult( + true, + "Message contains a sign-off by ${signOff.first}", + "was: ${signOffList.map { it.first }}, expected one of: ${target.contentToString()}" + ) + } + return EvaluationResult( + false, + "Message is not signed off by any of the specified signers", + "was: ${signOffList.map { it.first }}, expected one of: ${target.contentToString()}" + ) + } + + /** + * Parses a sign-off line into a name and email + * + * @return The Pair <Name, Email> + * @author Robert Haimerl + */ + internal fun parseSignOff(signOff: String): Pair<String, String> { + val signOffParts = signOff.split(" ") + val signOffName = + signOffParts.subList(1, signOffParts.size - 1).joinToString(separator = " ") + val signOffEmail = signOffParts.last().substring(1, signOffParts.last().length - 1) + return Pair(signOffName, signOffEmail) + } +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/SignedSignOffEvaluator.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/SignedSignOffEvaluator.kt new file mode 100644 index 0000000..651d9f9 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/evaluation/base/SignedSignOffEvaluator.kt @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.evaluation.base + +import de.fraunhofer.aisec.codyze.medina.assembling.Environment +import de.fraunhofer.aisec.codyze.medina.evaluation.EvaluationResult +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import java.nio.file.Path + +class SignedSignOffEvaluator(override val environment: Environment, override val base: Path) : + MetricEvaluator() { + + override val logger: KLogger = KotlinLogging.logger {} + + private val signOffEvaluator: SignOffEvaluator = SignOffEvaluator(environment, base) + private val gpgSignatureEvaluator: GPGSignatureEvaluator = + GPGSignatureEvaluator(environment, base) + + /** + * This evaluator expects the target to be a map with the following keys: + * - name + * - email + * - pub-key-id + */ + @Suppress("UNCHECKED_CAST") + override fun evaluate(target: Array<Any>): EvaluationResult { + // NOTE: we can not just check whether both the Signature and the SignOff evaluate correctly + // since both can be from different sources which is not enough for this evaluation. + // To avoid code duplication we still re-use the functionality provided by those two + // Evaluators + val gitCommitHash = environment.getGitHash() + val goodSignatures = + gpgSignatureEvaluator.getGoodSignatures( + gpgSignatureEvaluator.getCommitSignatureInformation(gitCommitHash) + ) + val signOffs = signOffEvaluator.getSignOff(gitCommitHash) + + // Try to cast the target values into maps + val targetMaps: Array<LinkedHashMap<String, String>> + try { + targetMaps = target.map { t -> t as LinkedHashMap<String, String> }.toTypedArray() + } catch (e: ClassCastException) { + logger.error { + "The target for \"SignedSignoff\" is not correctly formatted. Expected an array of maps:\n\ttarget:\n\t\t- name: ...\n\t\t email: ...\n\t\t pub-key-id: ..." + } + return EvaluationResult( + false, + "Metric target was not correctly formatted", + "was: ${target.javaClass.simpleName}, Expected an array of maps" + ) + } + + // Compare the results with the specified targets for this metric by iterating over all + // valid gpg signatures + for (signature in goodSignatures) { + // First check whether a sign-off matches the signature + for (signOff in signOffs) { + val signOffTuple = signOffEvaluator.parseSignOff(signOff) + if ( + signOffTuple.first == signature.second && signOffTuple.second == signature.third + ) { + // Then check whether this signer is specified as one of the targets in the + // configuration + for (map in targetMaps) { + if ( + map["name"] == signOffTuple.first && + map["email"] == signOffTuple.second && + map["pub-key-id"] == signature.first + ) { + // Finally, check whether this matches the git commit author + if ( + signOffEvaluator.getCommitAuthorName(gitCommitHash) == + map["name"] && + signOffEvaluator.getCommitAuthorEmail(gitCommitHash) == + map["email"] + ) { + return EvaluationResult( + true, + "The commit was signed off and contains a good signature from ${map["name"]} <${map["email"]}> with the key id ${map["pub-key-id"]}", + "was: ${goodSignatures.map { it.second }}, expected one of: ${targetMaps.map { it["name"] }}" + ) + } + return EvaluationResult( + false, + "The commit was signed off and contains a good signature, but the signer does not match the commit author", + "was: ${goodSignatures.map { it.second }}, expected one of: ${targetMaps.map { it["name"] }}" + ) + } + } + return EvaluationResult( + false, + "The commit was signed off and contains a good signature, but the signer is not specified as a valid target", + "was: ${goodSignatures.map { it.second }}, expected one of: ${targetMaps.map { it["name"] }}" + ) + } + return EvaluationResult( + false, + "The commit contains a good signature, but matching sign-off could be found", + "was: ${goodSignatures.map { it.second }}, expected one of: ${targetMaps.map { it["name"] }}" + ) + } + return EvaluationResult( + false, + "The commit contains no sign-off", + "was: ${signOffs}, expected one of: ${targetMaps.map { it["name"] }}" + ) + } + return EvaluationResult( + false, + "The commit contains no good GPG signature", + "was: ${goodSignatures.map { it.second }}, expected one of: ${targetMaps.map { it["name"] }}" + ) + } +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/main/CodyzeMedina.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/main/CodyzeMedina.kt new file mode 100644 index 0000000..9d4e363 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/main/CodyzeMedina.kt @@ -0,0 +1,428 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.main + +import de.fraunhofer.aisec.codyze.analysis.AnalysisServer +import de.fraunhofer.aisec.codyze.analysis.Finding +import de.fraunhofer.aisec.codyze.medina.assembling.Assembler +import de.fraunhofer.aisec.codyze.medina.assembling.Environment +import de.fraunhofer.aisec.codyze.medina.connection.Connection +import de.fraunhofer.aisec.codyze.medina.evaluation.EvaluationResult +import de.fraunhofer.aisec.codyze.medina.evaluation.Evaluator +import de.fraunhofer.aisec.codyze.medina.evaluation.Rule +import de.fraunhofer.aisec.codyze.medina.mapping.Mapping +import de.fraunhofer.aisec.codyze.medina.mapping.parseMappingTree +import de.fraunhofer.aisec.codyze.medina.mapping.parseMedinaMapping +import de.fraunhofer.aisec.codyze.medina.util.* +import io.github.oshai.kotlinlogging.KotlinLogging +import java.io.File +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import java.util.* +import java.util.concurrent.Callable +import java.util.concurrent.TimeUnit +import kotlin.io.path.absolute +import kotlin.system.exitProcess +import org.apache.logging.log4j.Level +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.core.LoggerContext +import org.openapitools.client.orchestrator.ApiException +import picocli.CommandLine +import picocli.CommandLine.Command +import picocli.CommandLine.Mixin + +@Command( + name = "codyze-medina", + version = ["Codyze for MEDINA ${CodyzeMedina.CODYZE_VERSION}"], + mixinStandardHelpOptions = true +) +class CodyzeMedina(config: Configuration, args: Array<String>) : Callable<Int> { + private val logger = KotlinLogging.logger {} + @Mixin val configuration: Configuration = config + private val cmdlineArgs = args + + /** + * Callable function containing the core program logic + * + * @return A code specified by the function handleReturn() + * @author Florian Wendland + * @author Robert Haimerl + */ + override fun call(): Int { + // Check if all necessary options are set + configuration.validate() + val basePath = configuration.configFile.path.absolute().parent + + // Prepare the connection to the Orchestrator + val connection = + Connection( + configuration.orchestrator.orchestratorEndpoint.toString(), + configuration.orchestrator.auth.oauthEndpoint.toString(), + configuration.orchestrator.auth.username, + configuration.orchestrator.auth.password + ) + + // Prepare the configuration for Codyze + val config = + de.fraunhofer.aisec.codyze.config.Configuration.initConfig( + configuration.configFile.path.toFile(), + *cmdlineArgs, + "--default-passes", + "--passes+", + "de.fraunhofer.aisec.cpg.passes.IdentifierPass", + "--passes+", + "de.fraunhofer.aisec.cpg.passes.EdgeCachePass" + ) + + // do not allow LSP/TUI + if (config.executionMode.isLsp || config.executionMode.isTui) { + logger.warn { "Forbidden execution mode, changing to CLI" } + config.executionMode.isCli = true + config.executionMode.isLsp = false + config.executionMode.isTui = false + } + + // overwrite the mark file path with our resolved locations + var cc = config.getCodyzeConfiguration() + cc.mark = + convertMarkConfig( + configuration.mark.builtinMark, + configuration.mark.projectMark, + basePath + ) + .map { string -> File(string) } + .toTypedArray() + + // overwrite `no-good-findings` in Codyze configuration to always return all results + cc.noGoodFindings = false + + config.setCodyzeConfiguration(cc) + + // Create necessary helper objects + val environment = Environment(configuration.ci, basePath) + val assembler = Assembler(environment, configuration.id) + val evaluator = Evaluator(environment, basePath) + var analysisFailure = false + var numberOfNegativeFindings = 0 + var connectionError = false + val gitCommitHash = environment.getGitHash() + + // Check whether the CloudServiceId is recognized + val validUUID = + try { + val uuid = UUID.fromString(configuration.id) + val cloudService = connection.getApiManager().getCloudServiceById(uuid) + logger.info { + "Found ${cloudService?.name ?: "nothing"} when looking up the cloud service id" + } + cloudService != null + } catch (e: IllegalArgumentException) { + connectionError = true + logger.error { + "Failed parsing orchestrator response when checking the service id. Make sure the API is up to date" + } + false + } catch (e: ApiException) { + // error code 404 -> cloud service not found, otherwise connection failed + if (e.code != 404) { + logger.warn { + "Connection to the orchestrator failed while trying to look up the cloud service id" + } + connectionError = true + } else { + logger.warn { "Cloud service id could not be found" } + } + false + } + + // Evaluate the rules defined in the Medina.yaml + val medinaMapping = + parseMedinaMapping(relativeLocation(configuration.rules.toFile(), basePath)) + var evaluationResults: Map<Rule, EvaluationResult> = mapOf() + if (medinaMapping != null) { + evaluator.parseKeys(relativeLocation(configuration.keyLocation.toFile(), basePath)) + evaluationResults = evaluator.evaluateAll(medinaMapping) + if (evaluationResults.filter { result -> !result.value.isValidated() }.isNotEmpty()) + analysisFailure = true + } + + val mappingsToFindings = mutableMapOf<Mapping, Set<Finding>>() + val codyzeMarkConfig = config.getCodyzeConfiguration().mark + val sources = config.source.map { relativeLocation(it, basePath) }.toTypedArray() + + // Parse the Mapping for each mark path contained in the configuration + for (i in codyzeMarkConfig.indices) { + val markDirectory = relativeLocation(codyzeMarkConfig[i], basePath) + val mappingToDirectory = parseMappingTree(markDirectory) + // Iterate over each mapping and start an analysis with the included rules + logger.info { + "Starting the runs for ${mappingToDirectory.size} mappings within $markDirectory" + } + for (entry in mappingToDirectory.entries) { + // Modify Codyze config to only include relevant Mark rules + cc = config.getCodyzeConfiguration() + cc.mark = arrayOf(entry.value) + config.setCodyzeConfiguration(cc) + + // Start the server and generate Findings + val server = AnalysisServer(config) + server.start() + val ctx = server.analyze(sources)[config.timeout, TimeUnit.MINUTES] + val findings = ctx.findings + + // Check whether any negative findings were produced + numberOfNegativeFindings += findings.filter { f -> f.isProblem }.size + // Add the new findings to the aggregate + mappingsToFindings[entry.key] = findings + } + } + analysisFailure = numberOfNegativeFindings != 0 || analysisFailure + + val resultLocation = relativeLocation(configuration.resultFile.toFile(), basePath).toPath() + // Print result files before sending the results to the Orchestrator + printFindings(mappingsToFindings.values.flatten().toSet(), config) + + val fallbackReport = false + var moveError = false + val resolvedOutputLocation = relativeLocation(File(config.output), basePath).canonicalPath + if (configuration.combinedOutput) { + try { + amendExistingReport(Path.of(config.output), resultLocation, evaluationResults) + } catch (e: Exception) { + logger.error { + "Failed to amend the Codyze report at ${config.output}: ${e.localizedMessage}. Falling back to separate report" + } + printSarifReport(evaluationResults, resultLocation) + } + } + if (!configuration.combinedOutput || fallbackReport) { + // We explicitely need to move the original Codyze SARIF Output if the location was not + // an absolute path + try { + Files.move( + Path.of(config.output), + Path.of(resolvedOutputLocation), + StandardCopyOption.REPLACE_EXISTING + ) + } catch (e: IOException) { + moveError = true + logger.error { + "Failed to move the Codyze output file from ${config.output} to the target directory $resolvedOutputLocation" + } + } + + printSarifReport(evaluationResults, resultLocation) + } + val eventualCodyzeOutputLocation = if (moveError) config.output else resolvedOutputLocation + + connectionError = + connectionError || + !sendResults( + mappingsToFindings, + evaluationResults, + assembler, + environment.getGitHash(), + connection, + resultLocation, + Path.of(eventualCodyzeOutputLocation), + configuration.combinedOutput + ) + createLogReport( + evaluationResults, + numberOfNegativeFindings, + eventualCodyzeOutputLocation, + resultLocation, + configuration.combinedOutput + ) + + // determine the return value + return handleReturn( + connectionError && configuration.orchestrator.requiredReachable, + !validUUID, + gitCommitHash == "invalid", + analysisFailure + ) + } + + companion object { + const val CODYZE_VERSION = "1.6.0" + } +} + +private val logger = KotlinLogging.logger {} +/** + * Entry point of the program + * + * @author Florian Wendland + * @author Robert Haimerl + * @args cli arguments provided + */ +fun main(args: Array<String>) { + // dynamically set the log level according to CODYZE_LOG_LEVEL + setLogLevel() + // initialize configuration + val config = Configuration.initialize(args) + // parse remaining CLI options and overwrite defaults and options from config file + val returnValue = + CommandLine(CodyzeMedina(config, args)).setUnmatchedArgumentsAllowed(true).execute(*args) + logger.info { "Exit with return value: $returnValue" } + exitProcess(returnValue) +} + +/** + * Returns the appropriate code and prints an error message to the error stream if needed: + * - 126 when connection to Orchestrator failed + * - else 3 when cloud service id is not recognizable + * - else 127 for general application errors + * - else 1 when only Analysis produced negative findings + * - else 0 + */ +fun handleReturn( + connectionError: Boolean, + idError: Boolean, + generalError: Boolean, + analysisFailure: Boolean +): Int { + return if (connectionError) { + System.err.println("Connection to the orchestrator failed") + 126 + } else if (idError) { + System.err.println("Cloud service id was not recognized") + 3 + } else if (generalError) { + System.err.println( + "Errors while executing codyze-medina. Consult the logs for more details" + ) + 127 + } else if (analysisFailure) { + System.err.println("Analysis completed with violations") + 1 + } else { + 0 + } +} + +/** + * Sets the logLevel of Log4J according to the environment variable "CODYZE_LOG_LEVEL" if given This + * is done via an environment variable instead of a configuration parameter to be able to set this + * before evaluating the config file + */ +fun setLogLevel() { + // instantly return when the variable is not set, log a warning when the value could not be + // translated to a level + val envValue = System.getenv("CODYZE_LOG_LEVEL") ?: return + val logLevel = + Level.getLevel(envValue) + ?: run { + logger.warn { + "The value of \"CODYZE_LOG_LEVEL\" (\"$envValue\") could not be translated to a log level" + } + return + } + // update the configuration of the root logger + val ctx = LogManager.getContext(false) as LoggerContext + val cfg = ctx.configuration + val loggerConfig = cfg.getLoggerConfig(LogManager.ROOT_LOGGER_NAME) + loggerConfig.level = logLevel + ctx.updateLoggers() +} + +/** + * Creates the Evidences and AssessmentResults and sends them to the Orchestrator. This method + * assumes that the respective SARIF files already exist. + * + * @param findings All Findings resulting from the analysis, associated with the mapping that caused + * them + * @param evaluationResults All EvaluationResults from evaluating MEDINA metrics + * @param assembler The assembler used to create Evidences and AssessmentResults + * @param gitHash The git hash of the analyzed project + * @param connection The (already established) connection to the Orchestrator + * @param medinaResultFile The SARIF file carrying MEDINA results + * @param codyzeResultFile The SARIF file carrying Codyze results + * @param combinedResults Whether the SARIF files were combined + * @return whether the upload was successful + */ +fun sendResults( + findings: Map<Mapping, Set<Finding>>, + evaluationResults: Map<Rule, EvaluationResult>, + assembler: Assembler, + gitHash: String, + connection: Connection, + medinaResultFile: Path, + codyzeResultFile: Path, + combinedResults: Boolean +): Boolean { + var error = false + // start by sending MEDINA metrics + val mEvidence = assembler.createEvidence(medinaResultFile) + error = error || !connection.getApiManager().storeEvidence(mEvidence) + for (evResult in evaluationResults) { + val assessmentResult = + assembler.convertEvaluationToAssessmentResult( + evResult.key, + evResult.value, + mEvidence.id ?: "", + gitHash + ) + error = + error || !connection.getApiManager().sendAssessmentResults(arrayOf(assessmentResult)) + } + // then send Codyze Findings + val cEvidence = if (combinedResults) mEvidence else assembler.createEvidence(codyzeResultFile) + if (!combinedResults) error = error || !connection.getApiManager().storeEvidence(cEvidence) + for (entry in findings) { + if (entry.value.isEmpty()) { + continue + } + val results = + assembler.convertFindingsToAssessmentResults( + entry.key, + entry.value, + cEvidence.id ?: "", + gitHash + ) + error = error || !connection.getApiManager().sendAssessmentResults(results) + } + return !error +} + +/** + * Resolves a filepath relative to the location of the given base path. If the path is absolute, it + * does not change + * + * @param filePath The relative or absolute path of the file + * @param base The base path + * @return The same path evaluated relative to the configuration file + * @author Robert Haimerl + */ +fun relativeLocation(filePath: File, base: Path): File { + return base.resolve(filePath.toPath()).toFile() +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/Configuration.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/main/Configuration.kt similarity index 69% rename from src/main/kotlin/de/fraunhofer/aisec/codyze/medina/Configuration.kt rename to src/main/kotlin/de/fraunhofer/aisec/codyze/medina/main/Configuration.kt index 7de6b5a..1c2fe52 100644 --- a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/Configuration.kt +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/main/Configuration.kt @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 /* - * Copyright (c) 2022, Fraunhofer AISEC. All rights reserved. + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,16 +26,16 @@ * * This file is part of the MEDINA Framework. */ -package de.fraunhofer.aisec.codyze.medina +package de.fraunhofer.aisec.codyze.medina.main import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.dataformat.yaml.YAMLMapper +import io.github.oshai.kotlinlogging.KotlinLogging import java.io.IOException import java.net.URL import java.nio.file.Path -import mu.KotlinLogging import picocli.CommandLine import picocli.CommandLine.Mixin import picocli.CommandLine.Option @@ -43,6 +43,7 @@ import picocli.CommandLine.Spec /** * Configuration class containing all needed variables passed in the configuration file and cli + * * @author Florian Wendland * @author Robert Haimerl */ @@ -55,28 +56,77 @@ class Configuration { @Mixin val orchestrator = Orchestrator() + @Mixin val mark = Mark() + /** The currently supported CI Environments */ - enum class Environment { + enum class CIEnvironment { NONE, GITLAB, GITHUB, JENKINS } + @JsonProperty("ci") @Option( names = ["--ci"], - required = false, paramLabel = "ENUM", description = ["CI environment currently being used"] ) - var ci: Environment = Environment.NONE + var ci: CIEnvironment = CIEnvironment.NONE + + @JsonProperty("id") + @Option( + names = ["--id"], + paramLabel = "STRING", + description = ["Id of the analyzed cloud service"] + ) + lateinit var id: String + + @JsonProperty("rules") + @Option( + names = ["--rules"], + paramLabel = "FILE", + description = ["Mapping file for Medina rules.\nDefault: 'codyze-medina-metrics.yaml'"] + ) + var rules: Path = Path.of("codyze-medina-metrics.yaml") + + @JsonProperty("medina-output") + @Option( + names = ["--medina-output"], + paramLabel = "PATH", + description = + [ + "File where MEDINA rule evaluation will be stored\nDefault: 'codyze-medina.sarif'\nIn case `combined-output` is set to true, this is the location of the combined result file" + ] + ) + var resultFile: Path = Path.of("codyze-medina.sarif") + + @JsonProperty("combined-output") + @Option( + names = ["--combined-output"], + paramLabel = "BOOLEAN", + description = + [ + "Whether the respective SARIF files created by Codyze and the MEDINA evaluation should be merged" + ] + ) + var combinedOutput: Boolean = true + + @JsonProperty("key-location") + @Option( + names = ["--key-location"], + paramLabel = "PATH", + description = ["Location of the public key files needed to evaluate GPG Signatures"] + ) + var keyLocation: Path = Path.of("public-keys") class ConfigFile { + @JsonProperty("config") @Option( names = ["--config"], paramLabel = "FILE", - defaultValue = "codyze.yaml", - description = ["Configuration file for Codyze.\nDefault: 'codyze.yaml'"] + defaultValue = "codyze-medina.yaml", + description = ["Configuration file for Codyze.\nDefault: 'codyze-medina.yaml'"] ) lateinit var path: Path } @@ -85,6 +135,14 @@ class Configuration { @Mixin val auth = Auth() + @JsonProperty("required") + @Option( + names = ["--required"], + paramLabel = "BOOLEAN", + description = ["Whether the Program fails when the Orchestrator is not reachable"] + ) + var requiredReachable: Boolean = true + @JsonProperty("endpoint") @Option( names = ["--endpoint"], @@ -131,8 +189,29 @@ class Configuration { fun isOrchestratorEndpointInitialized(): Boolean = ::orchestratorEndpoint.isInitialized } + class Mark { + @JsonProperty("builtin") + @Option( + names = ["--mark-builtin"], + paramLabel = "LIST<FILE>", + description = ["The builtin MARK files used to analyze the project."] + ) + var builtinMark: List<String> = listOf() + + @JsonProperty("project") + @Option( + names = ["--mark-project"], + paramLabel = "LIST<FILE>", + description = ["The external MARK files used to analyze the project."] + ) + var projectMark: List<String> = listOf() + } + + private fun isCloudServiceIdInitialized(): Boolean = ::id.isInitialized + /** * Checks whether all necessary fields were initialized + * * @author Florian Wendland * @author Robert Haimerl */ @@ -161,6 +240,12 @@ class Configuration { "Missing orchestrator endpoint URL ('--endpoint')" ) } + if (!isCloudServiceIdInitialized()) { + throw CommandLine.ParameterException( + spec.commandLine(), + "Missing cloud service id ('--id')" + ) + } logger.info { "Successfully validated the configuration" } } @@ -169,9 +254,10 @@ class Configuration { /** * Initializes the Configuration object by parsing the configuration file - * @author Florian Wendland + * * @param args the command-line arguments * @return a fully initialized Configuration object + * @author Florian Wendland */ fun initialize(args: Array<String>): Configuration { logger.info { "Initializing the configuration" } @@ -185,15 +271,16 @@ class Configuration { /** * Parses a configuration file - * @author Florian Wendland + * * @param configFile the path pointing to the file * @return a Configuration object + * @author Florian Wendland */ private fun parse(configFile: Path): Configuration { - logger.info { "Trying to parse the configuration file" } + logger.info { "Trying to parse the configuration file at $configFile" } val file = configFile.toFile() if (!file.exists() || !file.isFile || !file.canRead()) { - logger.warn { "Could not read from the configuration file" } + logger.warn { "Could not read from the configuration file at $configFile" } return Configuration() } @@ -206,7 +293,7 @@ class Configuration { } catch (e: IOException) { // also catches more specific Jackson exceptions logger.error(e) { - "Failed creating a valid configuration from the configuration file: $e" + "Failed creating a valid configuration from the configuration file: ${e.localizedMessage}" } } return Configuration() diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/main/MarkResolver.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/main/MarkResolver.kt new file mode 100644 index 0000000..8909a1f --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/main/MarkResolver.kt @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.main + +import io.github.oshai.kotlinlogging.KotlinLogging +import java.io.File +import java.nio.file.Path + +private val logger = KotlinLogging.logger {} + +/** + * Tries to resolve the location of the builtin MARK files. This assumes the following structure of + * the codyze-medina installation: + * - bin/ + * - codyze-medina + * - lib/ + * - ... + * - mark/ + * - ... + * + * @return The Path of the parent directory containing all builtin MARK rules + */ +private fun resolveMarkLocation(): Path { + // The following resolves the location of the codyze-medina-{version}.jar within lib/ and from + // there navigates to mark/ + val path = + Path.of(CodyzeMedina.Companion::class.java.protectionDomain.codeSource.location.toURI()) + .resolve("../../mark/") + .normalize() + logger.debug { "Determined the location of the MARK files as $path" } + return path +} + +/** + * Converts the MARK location arguments of the configuration to a single list that can be passed to + * Codyze + * + * @param builtin The argument list of builtin MARK files + * @param project The argument list of external MARK files + * @param base The base path used for relative MARK file locations + * @return A combined list of resolved paths to the needed MARK files + */ +fun convertMarkConfig(builtin: List<String>, project: List<String>, base: Path): List<String> { + val markList = mutableListOf<String>() + val markLocation = resolveMarkLocation().toString() + for (name in builtin) { + val mark = "$markLocation/$name" + markList.add(mark) + } + for (path in project) { + val resolvedPath = relativeLocation(File(path), base) + markList.add(resolvedPath.canonicalPath) + } + markList.forEach { logger.debug { "Added MARK file location ${println(it)}" } } + return markList +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/mapping/Mapping.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/mapping/Mapping.kt new file mode 100644 index 0000000..b207222 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/mapping/Mapping.kt @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.mapping + +/** Maps Mark rules of Findings to the respective AssessmentResults */ +@Suppress("unused") +class Mapping { + lateinit var metrics: Array<Metric> +} + +@Suppress("unused") +class Metric { + lateinit var name: String // the name of the metric + lateinit var rules: Array<String> // the name of the rules which need to be fulfilled + lateinit var configuration: Configuration // the configuration for this metric +} + +@Suppress("unused") +class Configuration { + var default: Boolean = true // whether this is a default metric + lateinit var operator: String // which operator compares this metric + lateinit var type: Type // of which type the target values are + lateinit var target: Array<Any> // what values are desired for this metric +} + +@Suppress("unused") +enum class Type { + STRING, + BOOLEAN, + NUMBER +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/mapping/MappingTree.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/mapping/MappingTree.kt new file mode 100644 index 0000000..fb7a222 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/mapping/MappingTree.kt @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.mapping + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import io.github.oshai.kotlinlogging.KotlinLogging +import java.io.File + +private val logger = KotlinLogging.logger {} +const val RESOURCE_TYPE = "Application" +const val MAP_FILE_NAME = "mapping.yaml" + +/** + * This function recursively steps through each branch of the hierarchy tree of the mark rule + * directory until it finds a mapping which is applied for all rules specified within this + * directory. Multiple mapping files can be specified in different directories within the same + * hierarchy layer. Rules in directories with no mappings will be ignored. All mapping files in + * lower hierarchy layers will be ignored. + * + * @param markOrigin the Path specified as the origin of the Mark rules + * @return a Map assigning each specified mapping to its directory + * @author Robert Haimerl + */ +fun parseMappingTree(markOrigin: File): Map<Mapping, File> { + // stop if we arrived at single file + if (!markOrigin.isDirectory) return mapOf() + + // if this directory contains a valid mapping, stop and add it to the map + val mapFile = File(markOrigin, MAP_FILE_NAME) + if (File(markOrigin, MAP_FILE_NAME).exists()) { + val mapping = parseMapping(mapFile) + if (mapping != null) { + return mapOf(mapping to markOrigin) + } else { + logger.warn { "Ignoring invalid mapping file at $mapFile" } + } + } + + // if there is no valid mapping, recursively search subdirectories for mappings + val mappingToDirectory = mutableMapOf<Mapping, File>() + for (branch: File in markOrigin.listFiles()!!) { + mappingToDirectory += parseMappingTree(branch) + } + return mappingToDirectory +} + +/** + * Parses the given file which contains instructions on how to map Findings to AssessmentResults. + * + * @param mapFile the File which includes the mapping information + * @return the Mapping object derived from the specification or null if parsing failed + * @author Robert Haimerl + */ +fun parseMapping(mapFile: File): Mapping? { + logger.info { "Parsing mappings from ${mapFile.name}" } + + // parse the yaml file into a Mapping object + val yamlMapper = ObjectMapper(YAMLFactory()) + return try { + yamlMapper.readValue(mapFile, Mapping::class.java) + } catch (e: Exception) { + logger.error(e) { + "Failed to parse the mapping file located at $mapFile: ${e.localizedMessage}" + } + null + } +} + +/** + * Parses the given file which contains instructions on how to construct AssessmentResults from + * Medina checks + * + * @param mapFile the File which includes the mapping information + * @return the MedinaMapping object derived from the specification + * @author Robert Haimerl + */ +fun parseMedinaMapping(mapFile: File): MedinaMapping? { + logger.info { "Parsing Medina mappings from ${mapFile.name}" } + + // parse the yaml file into a Mapping object, throw a RuntimeException on Error + val yamlMapper = ObjectMapper(YAMLFactory()) + return try { + yamlMapper.readValue(mapFile, MedinaMapping::class.java) + } catch (e: Exception) { + logger.warn { "Medina Mapping file was not found or cannot be parsed." } + null + } +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/mapping/MedinaMapping.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/mapping/MedinaMapping.kt new file mode 100644 index 0000000..8c624cd --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/mapping/MedinaMapping.kt @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.mapping + +/** Similar to the Mapping, but only specifies the name and the target values */ +@Suppress("unused") +class MedinaMapping { + lateinit var metrics: Array<MedinaMetric> +} + +@Suppress("unused") +class MedinaMetric { + lateinit var name: String // the name of the metric + lateinit var target: Array<Any> // the targets for this metrics +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/util/Assemblers.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/util/Assemblers.kt deleted file mode 100644 index 5314507..0000000 --- a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/util/Assemblers.kt +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -/* - * Copyright (c) 2022, Fraunhofer AISEC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * _____ _ - * / ____| | | - * | | ___ __| |_ _ _______ - * | | / _ \ / _` | | | |_ / _ \ - * | |___| (_) | (_| | |_| |/ / __/ - * \_____\___/ \__,_|\__, /___\___| - * __/ | - * |___/ - * - * This file is part of the MEDINA Framework. - */ -package de.fraunhofer.aisec.codyze.medina.util - -import java.io.File -import java.io.IOException -import java.util.* -import mu.KotlinLogging -import org.openapitools.client.evidence.model.Evidence -import org.openapitools.client.orchestrator.model.AssessmentTool - -private val logger = KotlinLogging.logger {} -private val codyzeVersion = "1.0.0" - -/** - * Creates a Codyze AssessmentTool - * @author Robert Haimerl - * @param metrics a list of all available metrics for this tool - * @return the resulting AssessmentTool - */ -fun createAssessmentTool(metrics: List<String>): AssessmentTool { - logger.debug { "Creating the AssessmentTool" } - - val at = AssessmentTool() - at.id = "codyze-$codyzeVersion" - at.name = "MEDINA Codyze v$codyzeVersion" - // Description taken from the introduction of the GitHub README - at.description = - "Codyze is a static code analyzer that focuses on verifying security compliance in source code. It operates on code property graphs and is thus able to handle non-compiling or even incomplete code fragments." - // TODO: hardcoded for now, needs to be able to fetch metrics from available rules - at.availableMetrics = metrics - - return at -} - -/** - * Creates an Evidence - * @author Robert Haimerl - * @param resultFilePath the path to the results file used as evidence - * @return the resulting Evidence - */ -fun createEvidence(resultFilePath: String): Evidence { - logger.debug { "Creating new Evidence based on $resultFilePath" } - val ev = Evidence() - - ev.id = UUID.randomUUID().toString() - ev.timestamp = java.time.OffsetDateTime.now() - ev.serviceId = null - ev.toolId = "codyze-$codyzeVersion" - ev.raw = - try { - File(resultFilePath).bufferedReader().use { it.readText() } - } catch (e: IOException) { - "" - } - ev.resource = - object { - @Suppress("unused") val id = hash - } - return ev -} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/util/Environment.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/util/Environment.kt deleted file mode 100644 index 990cd38..0000000 --- a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/util/Environment.kt +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -/* - * Copyright (c) 2022, Fraunhofer AISEC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * _____ _ - * / ____| | | - * | | ___ __| |_ _ _______ - * | | / _ \ / _` | | | |_ / _ \ - * | |___| (_) | (_| | |_| |/ / __/ - * \_____\___/ \__,_|\__, /___\___| - * __/ | - * |___/ - * - * This file is part of the MEDINA Framework. - */ -package de.fraunhofer.aisec.codyze.medina.util - -import de.fraunhofer.aisec.codyze.medina.Configuration.Environment -import java.io.BufferedReader -import java.io.InputStreamReader -import mu.KotlinLogging - -private val logger = KotlinLogging.logger {} -var environment = Environment.NONE -var environmentVariables = mutableMapOf<String, String>() -var hash: String? = null - -/** - * Tries multiple known environment variables to find out which CI/CD environment is being used - * Currently possible values for environment are ["None", "GitHub", "GitLab", "Jenkins"]. This - * method sets the "environment"-variable and fills the "environmentVariables"-Map specific to this - * environment - * @author Robert Haimerl - */ -fun verifyEnvironment() { - // all known CI/CD environments set "CI" to "true" - if (System.getenv("CI") != "true") { - logger.warn { "No CI/CD environment recognized" } - return - } - - val githubVars = arrayOf("GITHUB_ACTION", "GITHUB_JOB", "GITHUB_SHA") - val gitlabVars = arrayOf("CI_PIPELINE_ID", "CI_JOB_ID", "CI_COMMIT_SHA") - val jenkinsVars = arrayOf("JOB_NAME", "BUILD_ID", "JENKINS_URL") - - if (githubVars.all { variable -> System.getenv(variable) != null }) - environment = Environment.GITHUB - else if (gitlabVars.all { variable -> System.getenv(variable) != null }) - environment = Environment.GITLAB - else if (jenkinsVars.all { variable -> System.getenv(variable) != null }) - environment = Environment.JENKINS - - if (environment == Environment.NONE) logger.warn { "CI/CD environment could not be determined" } - else logger.info { "Determined CI/CD environment to be $environment" } -} - -/** - * Adds all relevant environment variables for the detected CI environment to the Map - * @author Robert Haimerl - */ -fun addEnvironmentVariables() { - // jenkins: "SCM-specific variables such as GIT_COMMIT are not automatically defined as - // environment variables; rather you can use the return value of the checkout step" - when (environment) { - Environment.GITHUB -> environmentVariables["commit_hash"] = "GITHUB_SHA" - Environment.GITLAB -> environmentVariables["commit_hash"] = "CI_COMMIT_SHA" - else -> return - } -} - -/** - * Tries environment variables set by GitHub actions, then GitLab CI/CD before ultimately falling - * back to console commands. This method modifies the "hash" member. - * @author Robert Haimerl - */ -@kotlin.jvm.Throws(RuntimeException::class) -fun fetchGitHash() { - // Default to "GITHUB_SHA" to prevent NullPointerExceptions from being thrown - hash = System.getenv(environmentVariables["commit_hash"] ?: "GITHUB_SHA") - if (hash == null) { - val process = Runtime.getRuntime().exec("git rev-parse HEAD") - val reader = BufferedReader(InputStreamReader(process.inputStream)) - hash = reader.readLine() - } - // If we couldn't figure hash out, abort - if (hash == null) - throw RuntimeException("Unable to fetch the Git commit hash for the analyzed project") -} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/util/MedinaSarif.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/util/MedinaSarif.kt new file mode 100644 index 0000000..9997cea --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/util/MedinaSarif.kt @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.util + +import de.fraunhofer.aisec.codyze.medina.evaluation.EvaluationResult +import de.fraunhofer.aisec.codyze.medina.evaluation.Rule +import de.fraunhofer.aisec.codyze.medina.main.CodyzeMedina +import io.github.detekt.sarif4k.* +import java.nio.file.Path +import java.util.stream.Collectors +import kotlin.io.path.deleteIfExists +import kotlin.io.path.readText +import kotlin.io.path.writeText + +// The rules added to the ToolComponent, derived from evaluation.Rule +// NOTE: the order of rules is important as it is later referenced in the field "ruleIndex" +val rules: List<ReportingDescriptor> = + Rule.entries + .stream() + .map { + ReportingDescriptor( + name = it.name, + id = it.id, + fullDescription = MultiformatMessageString(text = it.description) + ) + } + .collect(Collectors.toList()) + +// The tool with Codyze-Medina as its driver and all custom rules +val codyzeMedinaComponent: ToolComponent = + ToolComponent( + name = "CodyzeMedina", + fullName = "CodyzeMedina ${CodyzeMedina.CODYZE_VERSION}", + organization = "Fraunhofer AISEC", + version = CodyzeMedina.CODYZE_VERSION, + rules = rules, + ) + +/** + * Prints a separate SARIF report from the MEDINA evaluation result. + * + * @param evaluationResults The MEDINA evaluation results + * @param filepath The location of the resulting SARIF file + * @author Robert Haimerl + */ +fun printSarifReport(evaluationResults: Map<Rule, EvaluationResult>, filepath: Path) { + val report = createSarifReport(evaluationResults) + filepath.writeText(SarifSerializer.toJson(report)) +} + +/** + * Takes the last run of an existing SARIF report (e.g. from the Codyze library) and adds it to the + * MEDINA report as an extension. + * + * @param oldLocation The location of the previous SARIF report. MUST be valid SARIF + * @param newLocation The location of the new SARIF report after amending. + * @param evaluationResults The MEDINA evaluation results + * @author Robert Haimerl + */ +fun amendExistingReport( + oldLocation: Path, + newLocation: Path, + evaluationResults: Map<Rule, EvaluationResult> +) { + val originalReport = oldLocation.readText() + oldLocation.deleteIfExists() + val originalSarif = SarifSerializer.fromJson(originalReport) + val lastRun = originalSarif.runs.last() + + // Add Codyze-Medina as the new driver and move the original driver to extensions + val newTool = + lastRun.tool.copy( + driver = codyzeMedinaComponent, + extensions = (lastRun.tool.extensions?.plus(listOf(lastRun.tool.driver))) + ) + val newResults = lastRun.results?.plus(collectResults(evaluationResults)) + + // Create the new runs by using the changed last run + val newRun = lastRun.copy(tool = newTool, results = newResults) + val newRuns: MutableList<Run> = + originalSarif.runs.subList(0, originalSarif.runs.size - 1).toMutableList() + newRuns.add(newRun) + + // Create the new SARIF by using the new runs + val newSarif = originalSarif.copy(runs = newRuns) + val newReport = SarifSerializer.toJson(newSarif) + newLocation.writeText(newReport) +} + +/** + * This function creates a valid SARIF report from the MEDINA rule evaluations It is a separate + * report with CodyzeMedina as a separate tool driver and not an extension + * + * @param evaluationResults The results of evaluation the MEDINA rules + * @author Robert Haimerl + */ +private fun createSarifReport(evaluationResults: Map<Rule, EvaluationResult>): SarifSchema210 { + val results = collectResults(evaluationResults) + return SarifSchema210( + schema = + "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + version = Version.The210, + runs = + listOf( + Run( + tool = Tool(driver = codyzeMedinaComponent), + results = results, + ) + ) + ) +} + +/** + * Converts the evaluation results to SARIF results + * + * @param evaluationResults The MEDINA evaluation results + * @return the corresponding SARIF results + * @author Robert Haimerl + */ +private fun collectResults(evaluationResults: Map<Rule, EvaluationResult>): List<Result> { + return evaluationResults.entries + .stream() + .map { (rule, evResult) -> + Result( + ruleID = rule.id, + kind = if (evResult.isValidated()) ResultKind.Pass else ResultKind.Fail, + message = Message(text = rule.description), + ) + } + .collect(Collectors.toList()) +} diff --git a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/util/Util.kt b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/util/Util.kt index d9368df..f433609 100644 --- a/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/util/Util.kt +++ b/src/main/kotlin/de/fraunhofer/aisec/codyze/medina/util/Util.kt @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 /* - * Copyright (c) 2022, Fraunhofer AISEC. All rights reserved. + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,23 +30,22 @@ package de.fraunhofer.aisec.codyze.medina.util import de.fraunhofer.aisec.codyze.analysis.Finding import de.fraunhofer.aisec.codyze.config.Configuration +import de.fraunhofer.aisec.codyze.medina.evaluation.EvaluationResult +import de.fraunhofer.aisec.codyze.medina.evaluation.Rule import de.fraunhofer.aisec.codyze.printer.LegacyPrinter import de.fraunhofer.aisec.codyze.printer.Printer import de.fraunhofer.aisec.codyze.printer.SarifPrinter -import java.io.InputStreamReader -import java.util.* -import mu.KotlinLogging -import org.openapitools.client.orchestrator.model.AssessmentResult -import org.openapitools.client.orchestrator.model.MetricConfiguration +import io.github.oshai.kotlinlogging.KotlinLogging +import java.nio.file.Path private val logger = KotlinLogging.logger {} -private val mapping = mutableMapOf<String, Pair<String, MetricConfiguration>>() /** * Prints the Findings the way specified in the configuration - * @author Robert Haimerl + * * @param findings the set of Findings - * @param config the config containing printing details (path and SARIF) + * @param config the configuration of codyze-medina + * @author Robert Haimerl */ fun printFindings(findings: Set<Finding>, config: Configuration) { val printer: Printer = @@ -59,80 +58,45 @@ fun printFindings(findings: Set<Finding>, config: Configuration) { } /** - * Creates an AssessmentResult from a Finding - * @author Robert Haimerl - * @param finding the Finding to be transformed - * @param evidenceId the id of the Evidence belonging to the AssessmentResult - * @return the resulting AssessmentResult or null if not included in mapping.txt - */ -fun findingToAR(finding: Finding, evidenceId: String): AssessmentResult? { - logger.debug { "Creating AssessmentResult for finding with id ${finding.identifier}" } - val ar = AssessmentResult() - - // TODO: currently ignores EVERY finding not specified in mappings.txt - if (mapping[finding.identifier] == null) { - logger.debug { "Finding with id ${finding.identifier} not specified; Ignoring" } - return null - } - - val mc = mapping[finding.identifier]!!.second - - ar.metricConfiguration = mc - ar.id = UUID.randomUUID().toString() - ar.timestamp = java.time.OffsetDateTime.now() - ar.evidenceId = evidenceId - ar.resourceId = hash - ar.metricId = mapping[finding.identifier]!!.first - ar.compliant = !finding.isProblem - ar.nonComplianceComments = finding.logMsg - - return ar -} - -/** - * Parses the given file which contains instructions on how to map Findings to AssessmentResults. - * The Resulting mappings are being added to "mapping" + * Creates a concluding analysis report, intended at to be at the very end + * + * @param medinaEvaluation The results from evaluating the Medina Metrics + * @param markViolationNumber The number of violations of MARK rules as returned by Codyze + * @param codyzeOutput The location of the output from the Codyze library + * @param output The location of the output as specified in the configuration + * @param combinedOutput Whether both output files were combined * @author Robert Haimerl - * @param mapFilePath the path of the file which includes the mapping information */ -fun parseMapping(mapFilePath: String) { - logger.info { "Parsing mappings from $mapFilePath" } +fun createLogReport( + medinaEvaluation: Map<Rule, EvaluationResult>, + markViolationNumber: Int, + codyzeOutput: String, + output: Path, + combinedOutput: Boolean +) { + logger.debug { "Creating the final report" } + // Filter for violated Rules + val negativeEvaluation = medinaEvaluation.filter { eval -> !eval.value.isValidated() } - val mapfile = InputStreamReader(ClassLoader.getSystemResourceAsStream(mapFilePath) ?: return) - // read from mappings.txt - val rawMappings = mapfile.useLines { it.toList() } - // parse each mapping and add it to a map - for (raw in rawMappings) { - val ar = raw.split("->")[1].drop(1).dropLast(1).split(";") - val kind = ar[4] - val targetValue: Any = - // in case we have a list of target values - if (ar[3].contains(":")) - when (kind) { - "N" -> ar[3].split(":").map { v: String -> v.toDouble() } - "B" -> ar[3].split(":").map { v: String -> v == "true" } - else -> ar[3].split(":") - } - // single values - else - when (kind) { - "N" -> ar[3].toDouble() - "B" -> ar[3] == "true" - else -> ar[3] - } + // Create the header of the summary + val summaryHeader = "\n-----------\n" + "Summary of Analysis\n" + "-----------\n\n" + // Create the summary of the MARK evaluation + val markSummary = + "Found $markViolationNumber violations of MARK rules.\n" + + "The Analysis has been written to ${if (combinedOutput) output else codyzeOutput}\n\n" - // create a new MetricConfiguration according to the file - val mc = MetricConfiguration() - mc.isDefault = ar[1] == "true" - mc.operator = ar[2] - mc.targetValue = targetValue - - // use the metric name and configuration as values of the map - val mapValue = Pair(ar[0], mc) + // Create the summary of the MEDINA evaluation + var medinaSummary = + "Found ${if (negativeEvaluation.isNotEmpty()) negativeEvaluation.size else "no"} violations of MEDINA rules" + + if (negativeEvaluation.isNotEmpty()) ":\n" else "\n" + for (eval in negativeEvaluation) { + medinaSummary += "\t- Rule ${eval.key}\n" + "\t\t \"${eval.value.getMessage()}\"\n" + } - // the findings we want to map to this assessmentResult - val findings = raw.split("->")[0].drop(1).dropLast(1).split(";") - for (mapKey in findings) if (mapKey != "") mapping[mapKey] = mapValue + // Print the whole message into the log + if (negativeEvaluation.isEmpty() && markViolationNumber == 0) { + logger.info { summaryHeader + markSummary + medinaSummary } + } else { + logger.error { summaryHeader + markSummary + medinaSummary } } - logger.debug { "Mappings: $mapping" } } diff --git a/src/main/resources/evidence.yaml b/src/main/resources/evidence.yaml index 9edfed1..4497d59 100644 --- a/src/main/resources/evidence.yaml +++ b/src/main/resources/evidence.yaml @@ -41,8 +41,17 @@ paths: description: Returns all stored evidences. Part of the public API, also exposed as REST. operationId: EvidenceStore_ListEvidences parameters: + - name: filter.cloudServiceId + in: query + schema: + type: string + - name: filter.toolId + in: query + schema: + type: string - name: pageSize in: query + description: 'page_size: 0 = default (50 is default value), > 0 = set value (i.e. page_size = 5 -> SQL-Limit = 5)' schema: type: integer format: int32 @@ -71,6 +80,33 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' + /v1/evidence_store/evidences/{evidenceId}: + get: + tags: + - EvidenceStore + description: |- + Returns a particular stored evidence. Part of the public API, also exposed + as REST. + operationId: EvidenceStore_GetEvidence + parameters: + - name: evidenceId + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Evidence' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' components: schemas: Evidence: @@ -83,7 +119,7 @@ components: type: string description: time of evidence creation format: date-time - serviceId: + cloudServiceId: type: string description: Reference to a service this evidence was gathered from toolId: @@ -91,7 +127,7 @@ components: description: Reference to the tool which provided the evidence raw: type: string - description: Contains the evidence in its original form without following a defined schema, e.g. the raw JSON + description: Optional. Contains the evidence in its original form without following a defined schema, e.g. the raw JSON resource: $ref: '#/components/schemas/GoogleProtobufValue' description: An evidence resource @@ -132,10 +168,7 @@ components: description: 'The `Status` type defines a logical error model that is suitable for different programming environments, including REST APIs and RPC APIs. It is used by [gRPC](https://github.com/grpc). Each `Status` message contains three pieces of data: error code, error message, and error details. You can find out more about this error model and how to work with it in the [API Design Guide](https://cloud.google.com/apis/design/errors).' StoreEvidenceResponse: type: object - properties: - status: - type: boolean - statusMessage: - type: string + properties: {} + description: StoreEvidenceResponse belongs to StoreEvidence, which uses a custom unary RPC and therefore requires a response message according to the style convention. Since no return values are required, this is empty. tags: - name: EvidenceStore diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties new file mode 100644 index 0000000..bda1b36 --- /dev/null +++ b/src/main/resources/log4j.properties @@ -0,0 +1,9 @@ +# Set root logger level to WARN and its only appender to A1. +log4j.rootLogger=WARN, A1 + +# A1 is set to be a ConsoleAppender. +log4j.appender.A1=org.apache.log4j.ConsoleAppender + +# A1 uses PatternLayout. +log4j.appender.A1.layout=org.apache.log4j.PatternLayout +log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n \ No newline at end of file diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index 1d1b7e1..23bbbda 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -5,7 +5,7 @@ <patternLayout pattern="%d{ABSOLUTE} %5p %c{1}:%L - %m%n" /> </console> - <file name="fileout" fileName="medina-codyze.log"> + <file name="fileout" fileName="codyze-medina.log"> <patternLayout pattern="%d{ABSOLUTE} %5p %c{1}:%L - %m%n"/> </file> </appenders> diff --git a/src/main/resources/orchestrator.yaml b/src/main/resources/orchestrator.yaml index bc269e1..e3a1200 100644 --- a/src/main/resources/orchestrator.yaml +++ b/src/main/resources/orchestrator.yaml @@ -14,6 +14,37 @@ paths: description: List all assessment results. Part of the public API, also exposed as REST. operationId: Orchestrator_ListAssessmentResults parameters: + - name: filter.cloudServiceId + in: query + description: Optional. List only assessment results of a specific cloud service. + schema: + type: string + - name: filter.compliant + in: query + description: Optional. List only compliant assessment results. + schema: + type: boolean + - name: filter.metricIds + in: query + description: Optional. List only assessment results of a specific metric id. + schema: + type: array + items: + type: string + - name: filter.metricId + in: query + schema: + type: string + - name: filter.toolId + in: query + description: Optional. List only assessment result from a specific assessment tool. + schema: + type: string + - name: latestByResourceId + in: query + description: Optional. Latest results grouped by resource_id and metric_id. + schema: + type: boolean - name: pageSize in: query schema: @@ -68,6 +99,31 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' + /v1/orchestrator/assessment_results/{id}: + get: + tags: + - Orchestrator + description: Get an assessment result by ID + operationId: Orchestrator_GetAssessmentResult + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/AssessmentResult' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' /v1/orchestrator/assessment_tools: get: tags: @@ -77,9 +133,30 @@ paths: passed metric id operationId: Orchestrator_ListAssessmentTools parameters: - - name: metricId + - name: filter.cloudServiceId + in: query + description: Optional. List only assessment results of a specific cloud service. + schema: + type: string + - name: filter.compliant + in: query + description: Optional. List only compliant assessment results. + schema: + type: boolean + - name: filter.metricIds + in: query + description: Optional. List only assessment results of a specific metric id. + schema: + type: array + items: + type: string + - name: filter.metricId in: query - description: filter tools by metric id + schema: + type: string + - name: filter.toolId + in: query + description: Optional. List only assessment result from a specific assessment tool. schema: type: string - name: pageSize @@ -136,18 +213,24 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' - /v1/orchestrator/assessment_tools/{toolId}: - get: + /v1/orchestrator/assessment_tools/{tool.id}: + put: tags: - Orchestrator - description: Returns assessment tool given by the passed tool id - operationId: Orchestrator_GetAssessmentTool + description: Updates the assessment tool given by the passed id + operationId: Orchestrator_UpdateAssessmentTool parameters: - - name: toolId + - name: tool.id in: path required: true schema: type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AssessmentTool' + required: true responses: "200": description: OK @@ -161,23 +244,18 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' - put: + /v1/orchestrator/assessment_tools/{toolId}: + get: tags: - Orchestrator - description: Updates the assessment tool given by the passed id - operationId: Orchestrator_UpdateAssessmentTool + description: Returns assessment tool given by the passed tool id + operationId: Orchestrator_GetAssessmentTool parameters: - name: toolId in: path required: true schema: type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/AssessmentTool' - required: true responses: "200": description: OK @@ -214,12 +292,14 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' - /v1/orchestrator/certificates: + /v1/orchestrator/catalogs: get: tags: - Orchestrator - description: Lists all target certificates - operationId: Orchestrator_ListCertificates + description: |- + Lists all security controls catalogs. Each catalog includes a list of its + categories but no additional sub-resources. + operationId: Orchestrator_ListCatalogs parameters: - name: pageSize in: query @@ -244,7 +324,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ListCertificatesResponse' + $ref: '#/components/schemas/ListCatalogsResponse' default: description: Default error response content: @@ -254,13 +334,13 @@ paths: post: tags: - Orchestrator - description: Creates a new certificate - operationId: Orchestrator_CreateCertificate + description: Creates a new security controls catalog + operationId: Orchestrator_CreateCatalog requestBody: content: application/json: schema: - $ref: '#/components/schemas/Certificate' + $ref: '#/components/schemas/Catalog' required: true responses: "200": @@ -268,62 +348,65 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Certificate' + $ref: '#/components/schemas/Catalog' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' - /v1/orchestrator/certificates/{certificateId}: - get: + /v1/orchestrator/catalogs/{catalog.id}: + put: tags: - Orchestrator - description: Retrieves a certificate - operationId: Orchestrator_GetCertificate + description: Updates an existing certificate + operationId: Orchestrator_UpdateCatalog parameters: - - name: certificateId + - name: catalog.id in: path required: true schema: type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Catalog' + required: true responses: "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/Certificate' + $ref: '#/components/schemas/Catalog' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' - put: + /v1/orchestrator/catalogs/{catalogId}: + get: tags: - Orchestrator - description: Updates an existing certificate - operationId: Orchestrator_UpdateCertificate + description: |- + Retrieves a specific catalog by it's ID. The catalog includes a list of all + of it categories as well as the first level of controls in each category. + operationId: Orchestrator_GetCatalog parameters: - - name: certificateId + - name: catalogId in: path required: true schema: type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Certificate' - required: true responses: "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/Certificate' + $ref: '#/components/schemas/Catalog' default: description: Default error response content: @@ -333,10 +416,10 @@ paths: delete: tags: - Orchestrator - description: Removes a certificate - operationId: Orchestrator_RemoveCertificate + description: Removes a catalog + operationId: Orchestrator_RemoveCatalog parameters: - - name: certificateId + - name: catalogId in: path required: true schema: @@ -351,12 +434,84 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' - /v1/orchestrator/cloud_services: + /v1/orchestrator/catalogs/{catalogId}/categories/{categoryName}/controls/{controlId}: get: tags: - Orchestrator - description: Lists all target cloud services - operationId: Orchestrator_ListCloudServices + description: |- + Retrieves a control specified by the catalog ID, the control's category + name and the control ID. If present, it also includes a list of + sub-controls if present or a list of metrics if no sub-controls but metrics + are present. + operationId: Orchestrator_GetControl + parameters: + - name: catalogId + in: path + required: true + schema: + type: string + - name: categoryName + in: path + required: true + schema: + type: string + - name: controlId + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Control' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v1/orchestrator/catalogs/{catalogId}/category/{categoryName}: + get: + tags: + - Orchestrator + description: |- + Retrieves a category of a catalog specified by the catalog ID and the + category name. It includes the first level of controls within each + category. + operationId: Orchestrator_GetCategory + parameters: + - name: catalogId + in: path + required: true + schema: + type: string + - name: categoryName + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Category' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v1/orchestrator/certificates: + get: + tags: + - Orchestrator + description: Lists all target certificates + operationId: Orchestrator_ListCertificates parameters: - name: pageSize in: query @@ -381,7 +536,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ListCloudServicesResponse' + $ref: '#/components/schemas/ListCertificatesResponse' default: description: Default error response content: @@ -391,13 +546,13 @@ paths: post: tags: - Orchestrator - description: Registers a new target cloud service - operationId: Orchestrator_RegisterCloudService + description: Creates a new certificate + operationId: Orchestrator_CreateCertificate requestBody: content: application/json: schema: - $ref: '#/components/schemas/CloudService' + $ref: '#/components/schemas/Certificate' required: true responses: "200": @@ -405,62 +560,63 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/CloudService' + $ref: '#/components/schemas/Certificate' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' - /v1/orchestrator/cloud_services/{serviceId}: - get: + /v1/orchestrator/certificates/{certificate.id}: + put: tags: - Orchestrator - description: Retrieves a target cloud service - operationId: Orchestrator_GetCloudService + description: Updates an existing certificate + operationId: Orchestrator_UpdateCertificate parameters: - - name: serviceId + - name: certificate.id in: path required: true schema: type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Certificate' + required: true responses: "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/CloudService' + $ref: '#/components/schemas/Certificate' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' - put: + /v1/orchestrator/certificates/{certificateId}: + get: tags: - Orchestrator - description: Registers a new target cloud service - operationId: Orchestrator_UpdateCloudService + description: Retrieves a certificate + operationId: Orchestrator_GetCertificate parameters: - - name: serviceId + - name: certificateId in: path required: true schema: type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/CloudService' - required: true responses: "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/CloudService' + $ref: '#/components/schemas/Certificate' default: description: Default error response content: @@ -470,10 +626,10 @@ paths: delete: tags: - Orchestrator - description: Removes a target cloud service - operationId: Orchestrator_RemoveCloudService + description: Removes a certificate + operationId: Orchestrator_RemoveCertificate parameters: - - name: serviceId + - name: certificateId in: path required: true schema: @@ -488,43 +644,174 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' - /v1/orchestrator/cloud_services/{serviceId}/metric_configurations: + /v1/orchestrator/cloud_services: get: tags: - Orchestrator - description: |- - Lists all a metric configurations (target value and operator) for a - specific service ID - operationId: Orchestrator_ListMetricConfigurations + description: Lists all target cloud services + operationId: Orchestrator_ListCloudServices parameters: - - name: serviceId - in: path - required: true + - name: pageSize + in: query + schema: + type: integer + format: int32 + - name: pageToken + in: query + schema: + type: string + - name: orderBy + in: query schema: type: string + - name: asc + in: query + schema: + type: boolean responses: "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/ListMetricConfigurationResponse' + $ref: '#/components/schemas/ListCloudServicesResponse' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' - /v1/orchestrator/cloud_services/{serviceId}/metric_configurations/{metricId}: - get: + post: tags: - Orchestrator - description: |- - Retrieves a metric configuration (target value and operator) for a specific - service and metric ID + description: Registers a new target cloud service + operationId: Orchestrator_RegisterCloudService + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CloudService' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CloudService' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v1/orchestrator/cloud_services/statistics: + get: + tags: + - Orchestrator + description: Retrieves target cloud service statistics + operationId: Orchestrator_GetCloudServiceStatistics + parameters: + - name: cloudServiceId + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetCloudServiceStatisticsResponse' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v1/orchestrator/cloud_services/{cloudServiceId}: + get: + tags: + - Orchestrator + description: Retrieves a target cloud service + operationId: Orchestrator_GetCloudService + parameters: + - name: cloudServiceId + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CloudService' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + delete: + tags: + - Orchestrator + description: Removes a target cloud service + operationId: Orchestrator_RemoveCloudService + parameters: + - name: cloudServiceId + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: {} + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v1/orchestrator/cloud_services/{cloudServiceId}/metric_configurations: + get: + tags: + - Orchestrator + description: |- + Lists all a metric configurations (target value and operator) for a + specific service ID + operationId: Orchestrator_ListMetricConfigurations + parameters: + - name: cloudServiceId + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListMetricConfigurationResponse' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v1/orchestrator/cloud_services/{cloudServiceId}/metric_configurations/{metricId}: + get: + tags: + - Orchestrator + description: |- + Retrieves a metric configuration (target value and operator) for a specific + service and metric ID. operationId: Orchestrator_GetMetricConfiguration parameters: - - name: serviceId + - name: cloudServiceId in: path required: true schema: @@ -555,7 +842,7 @@ paths: service and metric ID operationId: Orchestrator_UpdateMetricConfiguration parameters: - - name: serviceId + - name: cloudServiceId in: path required: true schema: @@ -577,7 +864,345 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/MetricConfiguration' + $ref: '#/components/schemas/MetricConfiguration' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v1/orchestrator/cloud_services/{cloudServiceId}/toes/{catalogId}: + get: + tags: + - Orchestrator + description: Retrieves a Target of Evaluation + operationId: Orchestrator_GetTargetOfEvaluation + parameters: + - name: cloudServiceId + in: path + required: true + schema: + type: string + - name: catalogId + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/TargetOfEvaluation' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + delete: + tags: + - Orchestrator + description: Removes a Target of Evaluation + operationId: Orchestrator_RemoveTargetOfEvaluation + parameters: + - name: cloudServiceId + in: path + required: true + schema: + type: string + - name: catalogId + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: {} + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v1/orchestrator/cloud_services/{cloudServiceId}/toes/{catalogId}/controls_in_scope: + get: + tags: + - Orchestrator + description: Lists all controls in scope of a target of evaluation. + operationId: Orchestrator_ListControlsInScope + parameters: + - name: cloudServiceId + in: path + required: true + schema: + type: string + - name: catalogId + in: path + required: true + schema: + type: string + - name: pageSize + in: query + schema: + type: integer + format: int32 + - name: pageToken + in: query + schema: + type: string + - name: orderBy + in: query + schema: + type: string + - name: asc + in: query + schema: + type: boolean + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListControlsInScopeResponse' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + ? /v1/orchestrator/cloud_services/{cloudServiceId}/toes/{catalogId}/controls_in_scope/categories/{controlCategoryName}/controls/{controlId} + : delete: + tags: + - Orchestrator + description: Adds the selected control as "in scope" for the target of evaluation. + operationId: Orchestrator_RemoveControlFromScope + parameters: + - name: cloudServiceId + in: path + required: true + schema: + type: string + - name: catalogId + in: path + required: true + schema: + type: string + - name: controlCategoryName + in: path + required: true + schema: + type: string + - name: controlId + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: {} + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v1/orchestrator/cloud_services/{cloud_service.id}: + put: + tags: + - Orchestrator + description: Registers a new target cloud service + operationId: Orchestrator_UpdateCloudService + parameters: + - name: cloud_service.id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CloudService' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CloudService' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + ? /v1/orchestrator/cloud_services/{scope.target_of_evaluation_cloud_service_id}/toes/{scope.target_of_evaluation_catalog_id}/controls_in_scope + : post: + tags: + - Orchestrator + description: Adds the selected control as "in scope" for the target of evaluation. + operationId: Orchestrator_AddControlToScope + parameters: + - name: scope.target_of_evaluation_cloud_service_id + in: path + required: true + schema: + type: string + - name: scope.target_of_evaluation_catalog_id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ControlInScope' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ControlInScope' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + ? /v1/orchestrator/cloud_services/{scope.target_of_evaluation_cloud_service_id}/toes/{scope.target_of_evaluation_catalog_id}/controls_in_scope/categories/{scope.control_category_name}/controls/{scope.control_id} + : put: + tags: + - Orchestrator + description: Updates a particular control in scope, e.g., its monitoring status. + operationId: Orchestrator_UpdateControlInScope + parameters: + - name: scope.target_of_evaluation_cloud_service_id + in: path + required: true + schema: + type: string + - name: scope.target_of_evaluation_catalog_id + in: path + required: true + schema: + type: string + - name: scope.control_category_name + in: path + required: true + schema: + type: string + - name: scope.control_id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ControlInScope' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ControlInScope' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v1/orchestrator/cloud_services/{target_of_evaluation.cloud_service_id}/toes/{target_of_evaluation.catalog_id}: + put: + tags: + - Orchestrator + description: Updates an existing Target of Evaluation + operationId: Orchestrator_UpdateTargetOfEvaluation + parameters: + - name: target_of_evaluation.cloud_service_id + in: path + required: true + schema: + type: string + - name: target_of_evaluation.catalog_id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TargetOfEvaluation' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/TargetOfEvaluation' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v1/orchestrator/controls: + get: + tags: + - Orchestrator + description: |- + If no additional parameters are specified, this lists all controls. If a + catalog ID and a category name is specified, then only controls containing + in this category are returned. + operationId: Orchestrator_ListControls + parameters: + - name: catalogId + in: query + description: return either all controls or only the controls of the specified category + schema: + type: string + - name: categoryName + in: query + schema: + type: string + - name: pageSize + in: query + schema: + type: integer + format: int32 + - name: pageToken + in: query + schema: + type: string + - name: orderBy + in: query + schema: + type: string + - name: asc + in: query + schema: + type: boolean + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListControlsResponse' default: description: Default error response content: @@ -645,38 +1270,45 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' - /v1/orchestrator/metrics/{metricId}: - get: + /v1/orchestrator/metrics/{implementation.metric_id}/implementation: + put: tags: - Orchestrator - description: Returns the metric with the passed metric id - operationId: Orchestrator_GetMetric + description: Updates an existing metric implementation + operationId: Orchestrator_UpdateMetricImplementation parameters: - - name: metricId + - name: implementation.metric_id in: path required: true schema: type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MetricImplementation' + required: true responses: "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/Metric' + $ref: '#/components/schemas/MetricImplementation' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' + /v1/orchestrator/metrics/{metric.id}: put: tags: - Orchestrator description: Updates an existing metric operationId: Orchestrator_UpdateMetric parameters: - - name: metricId + - name: metric.id in: path required: true schema: @@ -700,52 +1332,43 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' - /v1/orchestrator/metrics/{metricId}/implementation: + /v1/orchestrator/metrics/{metricId}: get: tags: - Orchestrator - description: Returns the metric implementation of the passed metric id - operationId: Orchestrator_GetMetricImplementation + description: Returns the metric with the passed metric id + operationId: Orchestrator_GetMetric parameters: - name: metricId in: path required: true schema: type: string - - name: lang - in: query - schema: - type: string responses: "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/MetricImplementation' + $ref: '#/components/schemas/Metric' default: description: Default error response content: application/json: schema: $ref: '#/components/schemas/Status' - put: + /v1/orchestrator/metrics/{metricId}/implementation: + get: tags: - Orchestrator - description: Updates an existing metric implementation - operationId: Orchestrator_UpdateMetricImplementation + description: Returns the metric implementation of the passed metric id + operationId: Orchestrator_GetMetricImplementation parameters: - name: metricId in: path required: true schema: type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/MetricImplementation' - required: true responses: "200": description: OK @@ -759,12 +1382,79 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' - /v1/orchestrator/requirements: + /v1/orchestrator/public/certificates: + get: + tags: + - Orchestrator + description: Lists all target certificates without state history + operationId: Orchestrator_ListPublicCertificates + parameters: + - name: pageSize + in: query + schema: + type: integer + format: int32 + - name: pageToken + in: query + schema: + type: string + - name: orderBy + in: query + schema: + type: string + - name: asc + in: query + schema: + type: boolean + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListPublicCertificatesResponse' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v1/orchestrator/runtime_info: get: tags: - Orchestrator - operationId: Orchestrator_ListRequirements + description: Get Runtime Information + operationId: Orchestrator_GetRuntimeInfo + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Runtime' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v1/orchestrator/toes: + get: + tags: + - Orchestrator + description: Lists all Targets of Evaluation + operationId: Orchestrator_ListTargetsOfEvaluation parameters: + - name: cloudServiceId + in: query + description: We cannot create additional bindings when the parameter is optional so we check for != "" in the method to see if it is set when the service is specified, return all Targets of Evaluation that evaluate the given service for any catalog + schema: + type: string + - name: catalogId + in: query + description: when the catalog is specified, return all Targets of Evaluation that evaluate the given catalog for any service + schema: + type: string - name: pageSize in: query schema: @@ -788,7 +1478,31 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ListRequirementsResponse' + $ref: '#/components/schemas/ListTargetsOfEvaluationResponse' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + post: + tags: + - Orchestrator + description: Creates a new Target of Evaluation + operationId: Orchestrator_CreateTargetOfEvaluation + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TargetOfEvaluation' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/TargetOfEvaluation' default: description: Default error response content: @@ -837,6 +1551,12 @@ components: nonComplianceComments: type: string description: Some comments on the reason for non-compliance + cloudServiceId: + type: string + description: The cloud service which this assessment result belongs to + toolId: + type: string + description: Reference to the tool which provided the assessment result description: A result resource, representing the result after assessing the cloud resource with id resource_id. AssessmentTool: type: object @@ -853,6 +1573,52 @@ components: type: string description: a list of metrics that this tool can assess, referred by their ids description: Represents an external tool or service that offers assessments according to certain metrics. + Catalog: + type: object + properties: + id: + type: string + name: + type: string + description: + type: string + categories: + type: array + items: + $ref: '#/components/schemas/Category' + allInScope: + type: boolean + description: Certain security catalogs do not allow to select the scope of the controls, but all controls are automatically "in scope", however they can be set to a DELEGATED status. + assuranceLevels: + type: array + items: + type: string + description: A list of the assurance levels, e.g., basic, substantial and high for the EUCS catalog. + shortName: + type: string + description: Catalogs short name, e.g. EUCS + metadata: + $ref: '#/components/schemas/Catalog_Metadata' + Catalog_Metadata: + type: object + properties: + color: + type: string + description: a color for the cloud service used by the UI + Category: + type: object + properties: + name: + type: string + catalogId: + type: string + description: Reference to the catalog this category belongs to. + description: + type: string + controls: + type: array + items: + $ref: '#/components/schemas/Control' Certificate: type: object properties: @@ -860,7 +1626,7 @@ components: type: string name: type: string - serviceId: + cloudServiceId: type: string issueDate: type: string @@ -889,15 +1655,120 @@ components: type: string description: type: string - requirements: - $ref: '#/components/schemas/CloudService_Requirements' - CloudService_Requirements: + catalogsInScope: + type: array + items: + $ref: '#/components/schemas/Catalog' + configuredMetrics: + type: array + items: + $ref: '#/components/schemas/Metric' + createdAt: + type: string + description: creation time of the cloud_service + format: date-time + updatedAt: + type: string + description: last update time of the cloud_service + format: date-time + metadata: + $ref: '#/components/schemas/CloudService_Metadata' + CloudService_Metadata: + type: object + properties: + labels: + type: object + additionalProperties: + type: string + description: a map of key/value pairs, e.g., env:prod + icon: + type: string + description: an icon for the cloud service used by the UI + Control: type: object properties: - requirementIds: + id: + type: string + description: A short name of the control, e.g. OPS-01, as used in OSCAL; it is not a unique ID! + categoryName: + type: string + categoryCatalogId: + type: string + name: + type: string + description: Human-readable name of the control + description: + type: string + description: Description of the control + controls: type: array items: - type: string + $ref: '#/components/schemas/Control' + description: List of sub - controls - this is in accordance with the OSCAL model. + metrics: + type: array + items: + $ref: '#/components/schemas/Metric' + description: metrics contains either a list of reference to metrics - in this case only the id field of the metric is populated - or a list of populated metric meta-data, most likely returned by the database. + parentControlId: + type: string + description: Reference to the parent category this control belongs to. + parentControlCategoryName: + type: string + parentControlCategoryCatalogId: + type: string + assuranceLevel: + type: string + description: An assurance level is not offered by every catalog, therefore it is optional. + description: Control represents a certain Control that needs to be fulfilled. It could be a Control in a certification catalog. It follows the OSCAL model. A requirement in the EUCS terminology, e.g., is represented as the lowest sub-control. + ControlInScope: + type: object + properties: + targetOfEvaluationCloudServiceId: + type: string + targetOfEvaluationCatalogId: + type: string + controlId: + type: string + controlCategoryName: + type: string + controlCategoryCatalogId: + type: string + monitoringStatus: + enum: + - MONITORING_STATUS_UNSPECIFIED + - MONITORING_STATUS_AUTOMATICALLY_MONITORED + - MONITORING_STATUS_MANUALLY_MONITORED + - MONITORING_STATUS_DELEGATED + type: string + format: enum + description: ControlInScope defines a control which is "in scope" of a target of evaluation. Additional meta-data can be defined when a control is in scope, e.g., its monitoring status (continuously monitored, delegated, etc.) + Dependency: + type: object + properties: + path: + type: string + version: + type: string + GetCloudServiceStatisticsResponse: + type: object + properties: + numberOfDiscoveredResources: + type: integer + description: number of discovered resources per cloud service + format: int64 + numberOfAssessmentResults: + type: integer + description: number of assessment results per cloud service + format: int64 + numberOfEvidences: + type: integer + description: number of evidences per cloud service + format: int64 + numberOfSelectedCatalogs: + type: integer + description: number of selected catalogs per cloud service + format: int64 GoogleProtobufAny: type: object properties: @@ -926,6 +1797,15 @@ components: $ref: '#/components/schemas/AssessmentTool' nextPageToken: type: string + ListCatalogsResponse: + type: object + properties: + catalogs: + type: array + items: + $ref: '#/components/schemas/Catalog' + nextPageToken: + type: string ListCertificatesResponse: type: object properties: @@ -944,6 +1824,24 @@ components: $ref: '#/components/schemas/CloudService' nextPageToken: type: string + ListControlsInScopeResponse: + type: object + properties: + controlsInScope: + type: array + items: + $ref: '#/components/schemas/ControlInScope' + nextPageToken: + type: string + ListControlsResponse: + type: object + properties: + controls: + type: array + items: + $ref: '#/components/schemas/Control' + nextPageToken: + type: string ListMetricConfigurationResponse: type: object properties: @@ -961,13 +1859,22 @@ components: $ref: '#/components/schemas/Metric' nextPageToken: type: string - ListRequirementsResponse: + ListPublicCertificatesResponse: + type: object + properties: + certificates: + type: array + items: + $ref: '#/components/schemas/Certificate' + nextPageToken: + type: string + ListTargetsOfEvaluationResponse: type: object properties: - requirements: + targetOfEvaluation: type: array items: - $ref: '#/components/schemas/Requirement' + $ref: '#/components/schemas/TargetOfEvaluation' nextPageToken: type: string Metric: @@ -1000,6 +1907,8 @@ components: type: integer description: The interval in seconds the evidences must be collected for the respective metric. For now, we are not able to use google.protobuf.Duration because it is converted to a custom object in OpenAPI (https://github.com/google/gnostic/issues/351) format: int64 + implementation: + $ref: '#/components/schemas/MetricImplementation' description: A metric resource MetricConfiguration: type: object @@ -1016,6 +1925,12 @@ components: type: string description: The last time of update format: date-time + metricId: + type: string + description: The metric this configuration belongs to + cloudServiceId: + type: string + description: The service this configuration belongs to description: Defines the operator and a target value for an individual metric MetricImplementation: type: object @@ -1026,7 +1941,7 @@ components: lang: enum: - LANGUAGE_UNSPECIFIED - - REGO + - LANGUAGE_REGO type: string description: The language this metric is implemented in format: enum @@ -1068,24 +1983,30 @@ components: minMax: $ref: '#/components/schemas/MinMax' description: A range resource representing the range of values - Requirement: + Runtime: type: object properties: - id: + releaseVersion: type: string - name: + description: release_version is the latest Clouditor release version for this commit + vcs: type: string - description: + description: vcs is the used version control system + commitHash: type: string - metrics: + description: commit_hash is the current Clouditor commit hash + commitTime: + type: string + description: commit_time is the time of the Clouditor commit + format: date-time + golangVersion: + type: string + description: golang_version is the used golang version + dependencies: type: array items: - $ref: '#/components/schemas/Metric' - description: metrics contains either a list of reference to metrics - in this case only the id field of the metric is populated - or a list of populated metric meta-data, most likely returned by the database. - category: - type: string - description: category can be used to categorize requirements, e.g., according to a specific security catalog or something that groups requirements together, e.g., a security control. - description: Requirement represents a certain requirement that needs to be fulfilled. It could be a control in a certification catalog. + $ref: '#/components/schemas/Dependency' + description: dependency is a list of used runtime dependencies State: type: object properties: @@ -1119,11 +2040,24 @@ components: description: A list of messages that carry the error details. There is a common set of message types for APIs to use. description: 'The `Status` type defines a logical error model that is suitable for different programming environments, including REST APIs and RPC APIs. It is used by [gRPC](https://github.com/grpc). Each `Status` message contains three pieces of data: error code, error message, and error details. You can find out more about this error model and how to work with it in the [API Design Guide](https://cloud.google.com/apis/design/errors).' StoreAssessmentResultResponse: + type: object + properties: {} + description: StoreAssessmentResultReponse belongs to StoreAssessmentResult, which uses a custom unary RPC and therefore requires a response message according to the style convention. Since no return values are required, this is empty. + TargetOfEvaluation: type: object properties: - status: - type: boolean - statusMessage: + cloudServiceId: + type: string + catalogId: type: string + assuranceLevel: + type: string + description: an assurance level is not offered by every catalog, therefore it is optional + controlsInScope: + type: array + items: + $ref: '#/components/schemas/Control' + description: 'the controls that are in scope of this ToE. Note: For some security catalogs, e.g., the EUCS, a specific set of controls (in the "worst case": all) are automatically in scope. In this case, this list needs auto-filled at an appropriate time, e.g,. in CreateTargetOfEvaluation. Note: Because of limitations of our ORM framework, this field only contains a list of controls that are in scope of the target, but not the actual meta-data associated it with it (which is of message type ControlInScope). In order to retrieve the meta-data of the controls, the RPC ListControlsInScope (or the associated REST path) must be called.' + description: A Target of Evaluation binds a cloud service to a catalog, so the service is evaluated regarding this catalog's controls tags: - name: Orchestrator diff --git a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/AuthTest.kt b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/AuthTest.kt index c41601b..c2c06d7 100644 --- a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/AuthTest.kt +++ b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/AuthTest.kt @@ -28,6 +28,7 @@ */ package de.fraunhofer.aisec.codyze.medina +import de.fraunhofer.aisec.codyze.medina.connection.Connection import java.time.OffsetDateTime import java.util.* import org.junit.jupiter.api.Assertions @@ -44,6 +45,7 @@ class AuthTest { /** * Tests the OAuth connection to a local instance of the Orchestrator + * * @author Florian Wendland * @author Robert Haimerl */ @@ -52,8 +54,8 @@ class AuthTest { fun testLocalAuthConnection() { val connection = Connection(orchestratorURL, tokenURL, testUsername, testPassword) - Assertions.assertTrue(connection.testOAuthConnection()) - Assertions.assertTrue(connection.testOrchestratorConnection()) + Assertions.assertTrue(connection.getOAuthManager().testOAuthConnection()) + Assertions.assertTrue(connection.getApiManager().testOrchestratorConnection()) // create a MetricConfiguration (used for both AssessmentResults below) val mConfig = MetricConfiguration() @@ -84,11 +86,12 @@ class AuthTest { compAR.nonComplianceComments = "does not comply" val resultsToSend = arrayOf(nCompAR, compAR) - Assertions.assertTrue(connection.sendAssessmentResults(resultsToSend)) + Assertions.assertTrue(connection.getApiManager().sendAssessmentResults(resultsToSend)) } /** * Tests the OAuth connection to a remote instance of the Orchestrator + * * @author Florian Wendland */ @Disabled @@ -101,6 +104,6 @@ class AuthTest { val connection = Connection("", remoteTokenURI, username, password) - Assertions.assertTrue(connection.testOAuthConnection()) + Assertions.assertTrue(connection.getOAuthManager().testOAuthConnection()) } } diff --git a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/CliTest.kt b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/CliTest.kt index b2e8566..fae7b2f 100644 --- a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/CliTest.kt +++ b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/CliTest.kt @@ -28,6 +28,7 @@ */ package de.fraunhofer.aisec.codyze.medina +import de.fraunhofer.aisec.codyze.medina.main.Configuration import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import picocli.CommandLine @@ -36,12 +37,15 @@ class CliTest { /** * Tests the fail response on a missing OAuth-Endpoint + * * @author Florian Wendland */ @Test fun missingOAuth() { val args = arrayOf( + "--id", + "cli-test", "--username", "user", "--password", @@ -57,12 +61,15 @@ class CliTest { /** * Tests the fail response on a missing OAuth-Username + * * @author Florian Wendland */ @Test fun missingUsername() { val args = arrayOf( + "--id", + "cli-test", "--oauth-endpoint", "https://localhost:8080/", "--password", @@ -78,12 +85,15 @@ class CliTest { /** * Tests the fail response on a missing Orchestrator-Endpoint + * * @author Florian Wendland */ @Test fun missingOrchestrator() { val args = arrayOf( + "--id", + "cli-test", "--oauth-endpoint", "https://localhost:8080/", "--username", @@ -97,14 +107,41 @@ class CliTest { Assertions.assertThrows(CommandLine.ParameterException::class.java) { config.validate() } } + /** + * Tests the fail response on a missing cloudServiceId + * + * @author Robert Haimerl + */ + @Test + fun missingId() { + val args = + arrayOf( + "--oauth-endpoint", + "https://localhost:8080/", + "--username", + "user", + "--password", + "pw", + "--endpoint", + "http://localhost:8080/" + ) + + val config = Configuration() + CommandLine(config).parseArgs(*args) + Assertions.assertThrows(CommandLine.ParameterException::class.java) { config.validate() } + } + /** * Tests the response for a valid configuration + * * @author Florian Wendland */ @Test fun valid() { val args = arrayOf( + "--id", + "cli-test", "--oauth-endpoint", "https://localhost:8080/", "--username", diff --git a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/CodyzeCliPassThroughTest.kt b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/CodyzeCliPassThroughTest.kt index af5d0ea..009a68e 100644 --- a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/CodyzeCliPassThroughTest.kt +++ b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/CodyzeCliPassThroughTest.kt @@ -28,6 +28,7 @@ */ package de.fraunhofer.aisec.codyze.medina +import de.fraunhofer.aisec.codyze.medina.main.Configuration import java.io.File import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test @@ -36,12 +37,15 @@ import picocli.CommandLine class CodyzeCliPassThroughTest { /** * Tests whether cli args are correctly passed on to codyze + * * @author Florian Wendland */ @Test fun cliArgs() { val args = arrayOf( + "--id", + "cli-pass-through-test", "--username", "user", "--password", @@ -62,6 +66,7 @@ class CodyzeCliPassThroughTest { /** * Tests whether config file args are correctly passed on to codyze + * * @author Florian Wendland */ @Test @@ -71,8 +76,9 @@ class CodyzeCliPassThroughTest { CodyzeCliPassThroughTest::class .java .classLoader - .getResource("codyze.yaml")!! - .toURI() + .getResource("codyze.yaml") + ?.toURI() + ?: Assertions.fail("Could not load codyze.yaml") ) .toPath() .toString() diff --git a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/ConfigurationTest.kt b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/ConfigurationTest.kt index 87797aa..02f4607 100644 --- a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/ConfigurationTest.kt +++ b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/ConfigurationTest.kt @@ -28,6 +28,7 @@ */ package de.fraunhofer.aisec.codyze.medina +import de.fraunhofer.aisec.codyze.medina.main.Configuration import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import picocli.CommandLine @@ -36,6 +37,7 @@ class ConfigurationTest { /** * Tests parsing of the cli arguments + * * @author Florian Wendland */ @Test diff --git a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/ConverterTest.kt b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/ConverterTest.kt new file mode 100644 index 0000000..c50fbcd --- /dev/null +++ b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/ConverterTest.kt @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina + +import de.fraunhofer.aisec.codyze.analysis.Finding +import de.fraunhofer.aisec.codyze.medina.assembling.Assembler +import de.fraunhofer.aisec.codyze.medina.assembling.Environment +import de.fraunhofer.aisec.codyze.medina.main.Configuration +import de.fraunhofer.aisec.codyze.medina.mapping.parseMapping +import de.fraunhofer.aisec.codyze.sarif.schema.Result +import de.fraunhofer.aisec.mark.markDsl.Action +import java.io.File +import java.net.URI +import java.nio.file.Path +import kotlin.io.path.Path +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class ConverterTest { + /** + * Tests the conversion from Findings to compliant AssessmentResults + * + * @author Robert Haimerl + */ + @Test + fun testCompliant() { + // create some Finding object + val logMsg = "Variable cm not initialized" + val artifactUri = URI.create("file:///tmp/test.cpp") + val id = "WrongUseOfBotan_CipherMode" + val kind = Result.Kind.PASS + val f1 = Finding(id, Action.INFO, logMsg, artifactUri, listOf(), kind) + + val assembler = + Assembler(Environment(Configuration.CIEnvironment.NONE, Path.of("")), "test-id") + val assessmentResults = + assembler.convertFindingsToAssessmentResults( + parseMapping(File("src/test/resources/Mark/exampleMapping.yaml"))!!, + setOf(f1), + "", + "123" + ) + + assertNotNull(assessmentResults) + assertEquals(1, assessmentResults.size) + val result = assessmentResults.first() + assertTrue(result.compliant ?: false) + assertEquals(arrayListOf(1.23, 3.14), result.metricConfiguration!!.targetValue) + assertEquals("", result.nonComplianceComments) + assertEquals("TestMetric1", result.metricId) + } + + /** + * Tests the conversion from Findings to non-compliant AssessmentResults + * + * @author Robert Haimerl + */ + @Test + fun testNonCompliant() { + // create some Finding object + val logMsg = "You should not do that" + val artifactUri = URI.create("file:///tmp/test.cpp") + val id = "VariableNotInitialized" + val kind = Result.Kind.FAIL + val f2 = Finding(id, Action.FAIL, logMsg, artifactUri, listOf(), kind) + + val assembler = + Assembler(Environment(Configuration.CIEnvironment.NONE, Path("")), "test-id") + val assessmentResults = + assembler.convertFindingsToAssessmentResults( + parseMapping(File("src/test/resources/Mark/exampleMapping.yaml"))!!, + setOf(f2), + "", + "123" + ) + + assertNotNull(assessmentResults) + assertEquals(1, assessmentResults.size) + val result = assessmentResults.first() + assertTrue(!(result.compliant ?: true)) + assertEquals(true, result.metricConfiguration!!.targetValue) + assertEquals("${f2.identifier}: ${f2.logMsg}\n", result.nonComplianceComments) + assertEquals("TestMetric2", result.metricId) + } +} diff --git a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/DemoTest.kt b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/DemoTest.kt index cd62e0b..58dcdc2 100644 --- a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/DemoTest.kt +++ b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/DemoTest.kt @@ -36,6 +36,7 @@ class DemoTest { /** * Implements a demonstration printing the findings of the TlsServer.java class + * * @author Florian Wendland */ @Test diff --git a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/IntegrationTest.kt b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/IntegrationTest.kt new file mode 100644 index 0000000..53080b3 --- /dev/null +++ b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/IntegrationTest.kt @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022-2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina + +import de.fraunhofer.aisec.codyze.analysis.AnalysisServer +import de.fraunhofer.aisec.codyze.medina.assembling.Assembler +import de.fraunhofer.aisec.codyze.medina.assembling.Environment +import de.fraunhofer.aisec.codyze.medina.connection.Connection +import de.fraunhofer.aisec.codyze.medina.main.Configuration +import java.io.File +import java.util.* +import java.util.concurrent.TimeUnit +import kotlin.io.path.Path +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.openapitools.client.orchestrator.model.AssessmentResult +import org.openapitools.client.orchestrator.model.MetricConfiguration +import picocli.CommandLine + +class IntegrationTest { + + private val args = arrayOf("--config", "src/test/resources/codyze-test.yaml") + + @Test + @Disabled + fun testCodyzeIntegration() { + // 1 ----------- Test Configuration + // parse configuration + val config = Configuration.initialize(args) + CommandLine(config).setUnmatchedArgumentsAllowed(true).parseArgs(*args) + // try to convert to codyze config + val codyzeConfig = + de.fraunhofer.aisec.codyze.config.Configuration.initConfig( + config.configFile.path.toFile(), + *args, + "--default-passes", + "--passes+", + "de.fraunhofer.aisec.cpg.passes.IdentifierPass", + "--passes+", + "de.fraunhofer.aisec.cpg.passes.EdgeCachePass" + ) + // assert that the specified configuration was taken over to the codyzeConfig + Assertions.assertTrue( + Path(codyzeConfig.output).endsWith(Path("src/test/resources/codyze.sarif")) + ) + Assertions.assertEquals(1, codyzeConfig.source.size) + Assertions.assertTrue( + codyzeConfig.source[0].endsWith(File("src/test/resources/exampleFiles/2_1_2_1_02.cpp")) + ) + Assertions.assertTrue(codyzeConfig.sarifOutput) + + // 2 ----------- Test Server Creation + val server = AnalysisServer(codyzeConfig) + server.start() + // assert that the server could be initialized + Assertions.assertNotNull(server) + + // 3 ----------- Test Analysis + val findings = + server.analyze(codyzeConfig.source)[codyzeConfig.timeout, TimeUnit.MINUTES].findings + // assert if findings are as expected + Assertions.assertEquals(3, findings.size) + Assertions.assertTrue(findings.stream().anyMatch { it.identifier == "Cipher_Mode_Order" }) + Assertions.assertTrue(findings.stream().anyMatch { it.identifier == "RNGOrder" }) + Assertions.assertTrue( + findings.stream().anyMatch { it.identifier == "WrongUseOfBotan_CipherMode" } + ) + } + + /** + * To successfully run this test, the CODYZE_PWD environment variable needs to be set to the + * OAuth2 password for the Orchestrator + */ + @Test + @Disabled + fun testOrchestratorIntegration() { + // create codyzeConfig + val config = Configuration.initialize(args) + CommandLine(config).setUnmatchedArgumentsAllowed(true).parseArgs(*args) + val codyzeConfig = + de.fraunhofer.aisec.codyze.config.Configuration.initConfig( + config.configFile.path.toFile(), + *args, + "--default-passes", + "--passes+", + "de.fraunhofer.aisec.cpg.passes.IdentifierPass", + "--passes+", + "de.fraunhofer.aisec.cpg.passes.EdgeCachePass" + ) + + // 1 ----------- Test Connection Creation + val connection = + Connection( + config.orchestrator.orchestratorEndpoint.toString(), + config.orchestrator.auth.oauthEndpoint.toString(), + config.orchestrator.auth.username, + config.orchestrator.auth.password + ) + // assert that Object could be created and connection is possible + Assertions.assertNotNull(connection) + Assertions.assertTrue(connection.getOAuthManager().testOAuthConnection()) + + // 2 ----------- Test Evidence Storage + val assembler = + Assembler(Environment(Configuration.CIEnvironment.NONE, Path("")), "integration-test") + val evidence = assembler.createEvidence(Path(codyzeConfig.output)) + evidence.resource = + object { + @Suppress("unused") val id = "123" + } + // assert that evidence could be stored without problems + Assertions.assertTrue(connection.getApiManager().storeEvidence(evidence)) + + // 3 ----------- Test Result Storage + // create an AssessmentResult + val ar = AssessmentResult() + val mc = MetricConfiguration() + + mc.isDefault = true + mc.operator = "=" + mc.targetValue = 5 + ar.metricConfiguration = mc + ar.id = UUID.randomUUID().toString() + ar.timestamp = java.time.OffsetDateTime.now() + ar.evidenceId = evidence.id + ar.resourceId = "123" + ar.metricId = "test-metric" + ar.compliant = false + ar.nonComplianceComments = "" + + // assert that this result could be stored without problems + Assertions.assertTrue(connection.getApiManager().sendAssessmentResults(arrayOf(ar))) + } +} diff --git a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/LoggingTest.kt b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/LoggingTest.kt new file mode 100644 index 0000000..a5641c3 --- /dev/null +++ b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/LoggingTest.kt @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina + +import io.github.oshai.kotlinlogging.KotlinLogging +import java.io.File +import org.apache.log4j.AppenderSkeleton +import org.apache.log4j.LogManager +import org.apache.log4j.spi.LoggingEvent +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +class LoggingTest { + private val logger = KotlinLogging.logger("TestLogger") + @TempDir private var logFile = File("test-log.log") + + @Test + fun testFileCreation() { + logger.info { "Testing file creation" } + Assertions.assertTrue(logFile.exists()) + } + + @Test + fun testLogContents() { + val logImpl = LogManager.getLogger("TestLogger") + val appender = TestAppender() + logImpl.addAppender(appender) + + logImpl.debug("debug") + logImpl.info("info") + logImpl.warn("warn") + logImpl.error("error") + + logImpl.removeAppender(appender) + val logLines = appender.getLog().map { event -> event.renderedMessage } + + // test whether log content is as specified + Assertions.assertEquals("warn", logLines[0]) + Assertions.assertEquals("error", logLines[1]) + } + + // create new Appender just for testing the logging + internal class TestAppender : AppenderSkeleton() { + private val log: MutableList<LoggingEvent> = ArrayList<LoggingEvent>() + + override fun requiresLayout(): Boolean { + return false + } + + override fun append(loggingEvent: LoggingEvent) { + log.add(loggingEvent) + } + + override fun close() {} + + fun getLog(): List<LoggingEvent> { + return ArrayList<LoggingEvent>(log) + } + } +} diff --git a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/MappingTreeTest.kt b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/MappingTreeTest.kt new file mode 100644 index 0000000..d947213 --- /dev/null +++ b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/MappingTreeTest.kt @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2022, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina + +import de.fraunhofer.aisec.codyze.medina.mapping.parseMappingTree +import java.io.File +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class MappingTreeTest { + /** + * Tests whether the tree structure of the directory gets resolved correctly + * + * @author Robert Haimerl + */ + @Test + fun testTreeParsing() { + val mappingMap = parseMappingTree(File("src/test/resources/mappingTreeTestStructure")) + Assertions.assertEquals(3, mappingMap.size) + val directoryArray = + setOf( + File("src/test/resources/mappingTreeTestStructure/botan"), + File("src/test/resources/mappingTreeTestStructure/bouncycastle/bc1"), + File("src/test/resources/mappingTreeTestStructure/bouncycastle/bc2") + ) + Assertions.assertEquals(directoryArray, mappingMap.values.toSet()) + } +} diff --git a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/MedinaMetrikTest.kt b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/MedinaMetrikTest.kt new file mode 100644 index 0000000..beabf07 --- /dev/null +++ b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/MedinaMetrikTest.kt @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina + +import de.fraunhofer.aisec.codyze.medina.assembling.Environment +import de.fraunhofer.aisec.codyze.medina.evaluation.base.ApprovedCommitAuthorEvaluator +import de.fraunhofer.aisec.codyze.medina.main.Configuration +import kotlin.io.path.Path +import org.junit.jupiter.api.Test + +class MedinaMetrikTest { + + @Test + fun testCommitAuthor() { + val env = Environment(Configuration.CIEnvironment.NONE, Path("./")) + val eval = ApprovedCommitAuthorEvaluator(env, java.nio.file.Path.of(".")) + + eval.evaluate(arrayOf()) + } +} diff --git a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/ParserTest.kt b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/ParserTest.kt deleted file mode 100644 index 7925c56..0000000 --- a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/ParserTest.kt +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -/* - * Copyright (c) 2022, Fraunhofer AISEC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * _____ _ - * / ____| | | - * | | ___ __| |_ _ _______ - * | | / _ \ / _` | | | |_ / _ \ - * | |___| (_) | (_| | |_| |/ / __/ - * \_____\___/ \__,_|\__, /___\___| - * __/ | - * |___/ - * - * This file is part of the MEDINA Framework. - */ -package de.fraunhofer.aisec.codyze.medina - -import de.fraunhofer.aisec.codyze.analysis.Finding -import de.fraunhofer.aisec.codyze.medina.util.findingToAR -import de.fraunhofer.aisec.codyze.sarif.schema.Result -import de.fraunhofer.aisec.cpg.sarif.Region -import de.fraunhofer.aisec.mark.markDsl.Action -import java.net.URI -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test - -class ParserTest { - /** - * Tests the conversion from Findings to compliant AssessmentResults - * @author Robert Haimerl - */ - @Disabled - @Test - fun testCompliant() { - // create some Finding object - val logMsg = "Variable cm not initialized" - val artifactUri = URI.create("file:///tmp/test.cpp") - val id = "WrongUseOfBotan_CipherMode" - val regions = listOf(Region(0, 2, 10, 12)) - val kind = Result.Kind.PASS - val f1 = Finding(id, Action.INFO, logMsg, artifactUri, regions, kind) - - val assessmentResult = findingToAR(f1, "") - - assertNotNull(assessmentResult) - assertTrue(assessmentResult?.compliant ?: false) - assertEquals(f1.identifier, assessmentResult?.metricId, "Wrong metricID") - assertEquals( - f1.logMsg, - assessmentResult?.nonComplianceComments, - "The comment for non-compliance differs from the logMsg" - ) - } - - /** - * Tests the conversion from Findings to non-compliant AssessmentResults - * @author Robert Haimerl - */ - @Disabled - @Test - fun testNonCompliant() { - // create some Finding object - val logMsg = "You should not do that" - val artifactUri = URI.create("file:///tmp/test.cpp") - val id = "VariableNotInitialized" - val regions = listOf(Region(0, 2, 10, 12)) - val kind = Result.Kind.FAIL - val f2 = Finding(id, Action.FAIL, logMsg, artifactUri, regions, kind) - - val assessmentResult = findingToAR(f2, "") - - assertNotNull(assessmentResult) - assertFalse(assessmentResult?.compliant ?: false) - assertEquals(f2.identifier, assessmentResult?.metricId, "Wrong metricID") - assertEquals( - f2.logMsg, - assessmentResult?.nonComplianceComments, - "The comment for non-compliance differs from the logMsg" - ) - } -} diff --git a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/SarifTest.kt b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/SarifTest.kt new file mode 100644 index 0000000..dffd97e --- /dev/null +++ b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/SarifTest.kt @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina + +import de.fraunhofer.aisec.codyze.medina.evaluation.EvaluationResult +import de.fraunhofer.aisec.codyze.medina.evaluation.Rule +import de.fraunhofer.aisec.codyze.medina.util.amendExistingReport +import de.fraunhofer.aisec.codyze.medina.util.codyzeMedinaComponent +import io.github.detekt.sarif4k.* +import kotlin.io.path.exists +import kotlin.io.path.readText +import kotlin.io.path.writeText +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class SarifTest { + + private val sampleReport = + SarifSchema210( + schema = + "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + version = Version.The210, + runs = + listOf( + Run( + tool = + Tool( + driver = + ToolComponent( + name = "test-driver", + ), + extensions = listOf() + ), + results = + listOf( + Result(message = Message(text = "this is result 1")), + Result(message = Message(text = "this is result 2")), + Result(message = Message(text = "this is result 3")) + ) + ) + ) + ) + + @Test + fun testAmend() { + val evaluationResults: Map<Rule, EvaluationResult> = + mapOf( + Rule.CodeSignoff to + EvaluationResult(true, "valid sign-off", "valid sign-off by xyz"), + Rule.ApprovedCommitAuthor to + EvaluationResult(false, "wrong commit author", "commit authored by xyz") + ) + + val reportPath = kotlin.io.path.createTempFile() + val targetPath = kotlin.io.path.createTempFile() + + reportPath.writeText(SarifSerializer.toJson(sampleReport)) + amendExistingReport(reportPath, targetPath, evaluationResults) + val newReport = SarifSerializer.fromJson(targetPath.readText()) + + // Whether the old report was deleted + Assertions.assertFalse(reportPath.exists(), "The old report was not deleted after amending") + // Whether all results are present + Assertions.assertArrayEquals( + listOf( + Result(message = Message(text = "this is result 1")), + Result(message = Message(text = "this is result 2")), + Result(message = Message(text = "this is result 3")), + Result( + message = Message(text = Rule.CodeSignoff.description), + ruleID = Rule.CodeSignoff.id, + kind = ResultKind.Pass + ), + Result( + message = Message(text = Rule.ApprovedCommitAuthor.description), + ruleID = Rule.ApprovedCommitAuthor.id, + kind = ResultKind.Fail + ) + ) + .toTypedArray(), + newReport.runs[0].results!!.toTypedArray(), + "The results of the new report are not as expected" + ) + // Whether the extension was entered correctly + Assertions.assertEquals( + "test-driver", + newReport.runs[0].tool.extensions!![0].name, + "The old driver was not added as an extension" + ) + Assertions.assertEquals( + codyzeMedinaComponent, + newReport.runs[0].tool.driver, + "The new driver was not set correctly" + ) + } +} diff --git a/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/assembling/AssemblerTest.kt b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/assembling/AssemblerTest.kt new file mode 100644 index 0000000..2fbc2d3 --- /dev/null +++ b/src/test/kotlin/de/fraunhofer/aisec/codyze/medina/assembling/AssemblerTest.kt @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * _____ _ + * / ____| | | + * | | ___ __| |_ _ _______ + * | | / _ \ / _` | | | |_ / _ \ + * | |___| (_) | (_| | |_| |/ / __/ + * \_____\___/ \__,_|\__, /___\___| + * __/ | + * |___/ + * + * This file is part of the MEDINA Framework. + */ +package de.fraunhofer.aisec.codyze.medina.assembling + +import de.fraunhofer.aisec.codyze.analysis.Finding +import de.fraunhofer.aisec.codyze.medina.evaluation.EvaluationResult +import de.fraunhofer.aisec.codyze.medina.evaluation.Rule +import de.fraunhofer.aisec.codyze.medina.mapping.Configuration +import de.fraunhofer.aisec.codyze.medina.mapping.Mapping +import de.fraunhofer.aisec.codyze.medina.mapping.Metric +import de.fraunhofer.aisec.codyze.medina.mapping.Type +import java.time.OffsetDateTime +import java.util.* +import kotlin.io.path.Path +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.junit.jupiter.MockitoExtension + +@ExtendWith(MockitoExtension::class) +class AssemblerTest { + + private val nullId = UUID(0, 0) + private val tool = "codyze-medina" + private val gitHash = "ffffff" + + @Mock private lateinit var environment: Environment + + private lateinit var assembler: Assembler + + @BeforeEach + fun inject() { + assembler = Assembler(environment, nullId.toString()) + } + + @Test + fun testCreateEvidenceFromResultFile() { + `when`(environment.getGitHash()).thenReturn(gitHash) + + val currentTime = OffsetDateTime.now() + val evidence = assembler.createEvidence(Path("invalid_path")) + + // Assert that the timestamp is exact +- 5 seconds + assertTrue(evidence.timestamp?.isAfter(currentTime.minusSeconds(5)) ?: false) + assertTrue(evidence.timestamp?.isBefore(currentTime.plusSeconds(5)) ?: false) + // Assert that the cloud service id was correctly set + assertEquals(nullId.toString(), evidence.cloudServiceId) + // Assert that the tool id contains "codyze-medina" + assertTrue(evidence.toolId?.contains(tool, ignoreCase = true) ?: false) + + verify(environment).getGitHash() + + assertTrue(true) + } + + @Test + fun testConvertEvaluationToAssessmentResult() { + val rule = Rule.CodeSignoff + val detailedMessage = "detailed-message" + val validated = false + val evaluationResult = EvaluationResult(validated, "message", detailedMessage) + val hash = gitHash + val evidenceId = nullId.toString() + + val currentTime = OffsetDateTime.now() + val assessmentResult = + assembler.convertEvaluationToAssessmentResult(rule, evaluationResult, evidenceId, hash) + + assertEquals(rule.operator, assessmentResult.metricConfiguration?.operator) + assertEquals(rule.targetValue, assessmentResult.metricConfiguration?.targetValue) + assertTrue(assessmentResult.metricConfiguration?.isDefault ?: false) + assertTrue( + assessmentResult.metricConfiguration?.updatedAt?.isAfter(currentTime.minusSeconds(5)) + ?: false + ) + assertTrue( + assessmentResult.metricConfiguration?.updatedAt?.isBefore(currentTime.plusSeconds(5)) + ?: false + ) + assertEquals(rule.name, assessmentResult.metricConfiguration?.metricId) + assertEquals(nullId.toString(), assessmentResult.metricConfiguration?.cloudServiceId) + + assertTrue(assessmentResult.timestamp?.isAfter(currentTime.minusSeconds(5)) ?: false) + assertTrue(assessmentResult.timestamp?.isBefore(currentTime.plusSeconds(5)) ?: false) + assertEquals(evidenceId, assessmentResult.evidenceId) + assertEquals(hash, assessmentResult.resourceId) + assertEquals(rule.name, assessmentResult.metricId) + assertEquals(validated, assessmentResult.compliant) + assertEquals(nullId.toString(), assessmentResult.cloudServiceId) + } + + @Test + fun testCreateFindingsToAssessmentResult() { + val message = "message" + val rule = "test-rule" + val name = "test-metric" + val targetValue = true + val isDefault = false + val type = Type.BOOLEAN + val operator = "==" + + // initialize a simple mapping + val configuration = Configuration() + configuration.default = isDefault + configuration.type = type + configuration.target = arrayOf(targetValue) + configuration.operator = operator + val metric = Metric() + metric.name = name + metric.configuration = configuration + metric.rules = arrayOf(rule) + val mapping = Mapping() + mapping.metrics = arrayOf(metric) + + // create a finding with a rule id covered by the mapping + val finding = Finding(rule, null, message, null, 0, 0, 0, 0) + val currentTime = OffsetDateTime.now() + + val assessmentResult = + assembler + .convertFindingsToAssessmentResults( + mapping, + setOf(finding), + nullId.toString(), + gitHash + )[0] + + assertEquals(metric.configuration.operator, assessmentResult.metricConfiguration?.operator) + assertEquals(targetValue, assessmentResult.metricConfiguration?.targetValue) + assertEquals(isDefault, assessmentResult.metricConfiguration?.isDefault) + assertTrue( + assessmentResult.metricConfiguration?.updatedAt?.isAfter(currentTime.minusSeconds(5)) + ?: false + ) + assertTrue( + assessmentResult.metricConfiguration?.updatedAt?.isBefore(currentTime.plusSeconds(5)) + ?: false + ) + assertEquals(name, assessmentResult.metricConfiguration?.metricId) + assertEquals(nullId.toString(), assessmentResult.metricConfiguration?.cloudServiceId) + + assertTrue(assessmentResult.timestamp?.isAfter(currentTime.minusSeconds(5)) ?: false) + assertTrue(assessmentResult.timestamp?.isBefore(currentTime.plusSeconds(5)) ?: false) + assertEquals(nullId.toString(), assessmentResult.evidenceId) + assertEquals(gitHash, assessmentResult.resourceId) + assertEquals(name, assessmentResult.metricId) + assertEquals(false, assessmentResult.compliant) + assertEquals(nullId.toString(), assessmentResult.cloudServiceId) + } +} diff --git a/src/test/resources/Mark/botan/mapping.yaml b/src/test/resources/Mark/botan/mapping.yaml new file mode 100644 index 0000000..be0dd4b --- /dev/null +++ b/src/test/resources/Mark/botan/mapping.yaml @@ -0,0 +1,29 @@ +metrics: + - name: "TestMetric1" + rules: + - "WrongUseOfBotan_CipherMode" + configuration: + default: false + operator: "==" + type: NUMBER + target: + - "1.23" + - "3.14" + - name: "TestMetric2" + rules: + - "RNGOrder" + configuration: + default: false + operator: "==" + type: BOOLEAN + target: + - "true" + - name: "TestMetric3" + rules: + - "Cipher_Mode_Order" + configuration: + default: false + operator: ">=" + type: NUMBER + target: + - "2" \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/AlgorithmParameterGenerator.mark b/src/test/resources/Mark/bouncycastle/AlgorithmParameterGenerator.mark deleted file mode 100644 index 6edb8a7..0000000 --- a/src/test/resources/Mark/bouncycastle/AlgorithmParameterGenerator.mark +++ /dev/null @@ -1,43 +0,0 @@ -package java.jca - -entity AlgorithmParameterGenerator { - - var algorithm; - var provider; - var size; - var random; - var genParamSpec; - var params; - - - op instantiate { - java.security.AlgorithmParameterGenerator.getInstance( - algorithm : java.lang.String - ); - java.security.AlgorithmParameterGenerator.getInstance( - algorithm : java.lang.String, - provider : java.lang.String | java.security.Provider - ); - } - - op initialize { - java.security.AlgorithmParameterGenerator.init( - size : int - ); - java.security.AlgorithmParameterGenerator.init( - size : int, - random : java.security.SecureRandom - ); - java.security.AlgorithmParameterGenerator.init( - genParamSpec : java.security.spec.AlgorithmParameterSpec - ); - java.security.AlgorithmParameterGenerator.init( - genParamSpec : java.security.spec.AlgorithmParameterSpec, - random : java.security.SecureRandom - ); - } - - op generate { - params = java.security.AlgorithmParameterGenerator.generateParameters(); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/AlgorithmParameters.mark b/src/test/resources/Mark/bouncycastle/AlgorithmParameters.mark deleted file mode 100644 index 268c912..0000000 --- a/src/test/resources/Mark/bouncycastle/AlgorithmParameters.mark +++ /dev/null @@ -1,34 +0,0 @@ -package java.jca - -entity AlgorithmParameters { - - var algorithm; - var provider; - var params; - var format; - var paramSpec; - - - op instantiate { - java.security.AlgorithmParameters.getInstance( - algorithm : java.lang.String - ); - java.security.AlgorithmParameters.getInstance( - algorithm : java.lang.String, - provider : java.lang.String | java.security.Provider - ); - } - - op initialize { - java.security.AlgorithmParameters.init( - params : byte[] - ); - java.security.AlgorithmParameters.init( - params : byte[], - format : java.lang.String - ); - java.security.AlgorithmParameters.init( - paramSpec : java.security.spec.AlgorithmParameterSpec - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/CertPathBuilder.mark b/src/test/resources/Mark/bouncycastle/CertPathBuilder.mark deleted file mode 100644 index ec8c264..0000000 --- a/src/test/resources/Mark/bouncycastle/CertPathBuilder.mark +++ /dev/null @@ -1,24 +0,0 @@ -package jav.jca - -entity CertPathBuilder { - - var algorithm; - var provider; - var params; - - op instantiate { - java.security.cert.CertPathBuilder.getInstance( - algorithm : java.lang.String - ); - java.security.cert.CertPathBuilder.getInstance( - algorithm : java.lang.String, - provider : java.lang.String | java.security.Provider - ); - } - - op build { - java.security.cert.CertPathBuilder.build( - params : java.security.cert.CertPathParameters - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/CertPathValidator.mark b/src/test/resources/Mark/bouncycastle/CertPathValidator.mark deleted file mode 100644 index 76dd8af..0000000 --- a/src/test/resources/Mark/bouncycastle/CertPathValidator.mark +++ /dev/null @@ -1,28 +0,0 @@ -package java.jca - -entity CertPathValidator { - - var algorithm; - var provider; - - var certPath; - var params; - - - op instantiate { - java.security.cert.CertPathValidator.getInstance( - algorithm : java.lang.String - ); - java.security.cert.CertPathValidator.getInstance( - algorithm : java.lang.String, - provider : java.lang.String | java.security.Provider - ); - } - - op validate { - java.security.cert.CertPathValidator.validate( - certPath : java.security.cert.CertPath, - params : java.security.cert.CertPathParameters - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/CertStore.mark b/src/test/resources/Mark/bouncycastle/CertStore.mark deleted file mode 100644 index 2e6af2a..0000000 --- a/src/test/resources/Mark/bouncycastle/CertStore.mark +++ /dev/null @@ -1,34 +0,0 @@ -package java.jca - -entity CertStore { - - var type; - var params; - var provider; - - var selector; - var certificates; - var crls; - - - op instantiate { - java.security.cert.CertStore.getInstance( - type : java.lang.String, - params : java.security.cert.CertStoreParameters - ); - java.security.cert.CertStore.getInstance( - type : java.lang.String, - params : java.security.cert.CertStoreParameters, - provider : java.lang.String | java.security.Provider - ); - } - - op get { - certificates = java.security.cert.CertStore.getCertificates( - selector : java.security.cert.CertSelector - ); - crls = java.security.cert.CertStore.getCRLs( - selector : java.security.cert.CRLSelector - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/CertificateFactory.mark b/src/test/resources/Mark/bouncycastle/CertificateFactory.mark deleted file mode 100644 index dafaed2..0000000 --- a/src/test/resources/Mark/bouncycastle/CertificateFactory.mark +++ /dev/null @@ -1,56 +0,0 @@ -package java.jca - -entity CertificateFactory { - - var type; - var provider; - - var inStream; - var certificate; - - var encoding; - var certificates; - var certpath; - - - op instantiate { - java.security.cert.CertificateFactory.getInstance( - type : java.lang.String - ); - java.security.cert.CertificateFactory.getInstance( - type : java.lang.String, - provider : java.lang.String | java.security.Provider - ); - } - - op generateCertificate { - certificate = java.security.cert.CertificateFactory.generateCertificate( - inStream : java.io.InputStream - ); - certificates = java.security.cert.CertificateFactory.generateCertificates( // TODO how to denote that this returns Collection<? extends Certificate> - inStream : java.io.InputStream - ); - } - - op generateCertPath { - certpath = java.security.cert.CertificateFactory.generateCertPath( - inStream : java.io.InputStream - ); - certificates = java.security.cert.CertificateFactory.generateCertPath( - inStream : java.io.InputStream, - encoding : java.lang.String - ); - certificates = java.security.cert.CertificateFactory.generateCertPath( - certificates : List/* <? extends Certificate> */ // TODO how to handle generic list? what is actually seen by cpg - ); - } - - op generateCRL { - certificate = java.security.cert.CertificateFactory.generateCRL( - inStream : java.io.InputStream - ); - certificates = java.security.cert.CertificateFactory.generateCRLs( // TODO how to denote that this returns Collection<? extends CRL> - inStream : java.io.InputStream - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/ChaCha20ParameterSpec.mark b/src/test/resources/Mark/bouncycastle/ChaCha20ParameterSpec.mark deleted file mode 100644 index a57fc83..0000000 --- a/src/test/resources/Mark/bouncycastle/ChaCha20ParameterSpec.mark +++ /dev/null @@ -1,14 +0,0 @@ -package java.jca - -entity ChaCha20ParameterSpec { - - var nonce; - var counter; - - op instantiate { - javax.crypto.spec.ChaCha20ParameterSpec( - nonce : byte[], - counter : int - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/DHGenParameterSpec.mark b/src/test/resources/Mark/bouncycastle/DHGenParameterSpec.mark deleted file mode 100644 index b90a800..0000000 --- a/src/test/resources/Mark/bouncycastle/DHGenParameterSpec.mark +++ /dev/null @@ -1,15 +0,0 @@ -package java.jca - -entity DHGenParameterSpec { - - var primeSize; - var exponentSize; - - - op instantiate { - javax.crypto.spec.DHGenParameterSpec( - primeSize : int, - exponentSize : int - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/DHParameterSpec.mark b/src/test/resources/Mark/bouncycastle/DHParameterSpec.mark deleted file mode 100644 index 9d15c16..0000000 --- a/src/test/resources/Mark/bouncycastle/DHParameterSpec.mark +++ /dev/null @@ -1,22 +0,0 @@ -package java.jca - -entity DHParameterSpec { - - var p; - var g; - var l; - - - op instantiate { - javax.crypto.spec.DHParameterSpec( - p : java.math.BigInteger, - g : java.math.BigInteger - ); - javax.crypto.spec.DHParameterSpec( - p : java.math.BigInteger, - g : java.math.BigInteger, - l : int - ); - } - -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/DHPrivateKeySpec.mark b/src/test/resources/Mark/bouncycastle/DHPrivateKeySpec.mark deleted file mode 100644 index d505965..0000000 --- a/src/test/resources/Mark/bouncycastle/DHPrivateKeySpec.mark +++ /dev/null @@ -1,16 +0,0 @@ -package java.jca - -entity DHPrivateKeySpec { - - var x; - var p; - var g; - - op instantiate { - javax.crypto.spec.DHPrivateKeySpec( - x : java.math.BigInteger, - p : java.math.BigInteger, - g : java.math.BigInteger - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/DHPublicKeySpec.mark b/src/test/resources/Mark/bouncycastle/DHPublicKeySpec.mark deleted file mode 100644 index 4a563dd..0000000 --- a/src/test/resources/Mark/bouncycastle/DHPublicKeySpec.mark +++ /dev/null @@ -1,16 +0,0 @@ -package java.jca - -entity DHPublicKeySpec { - - var y; - var p; - var g; - - op instantiate { - javax.crypto.spec.DHPublicKeySpec( - y : java.math.BigInteger, - p : java.math.BigInteger, - g : java.math.BigInteger - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/DSAGenParameterSpec.mark b/src/test/resources/Mark/bouncycastle/DSAGenParameterSpec.mark deleted file mode 100644 index b4c3130..0000000 --- a/src/test/resources/Mark/bouncycastle/DSAGenParameterSpec.mark +++ /dev/null @@ -1,22 +0,0 @@ -package java.jca - -entity DSAGenParameterSpec { - - var primePLen; - var subprimeQLen; - var seedLen; - - - op instantiate { - java.security.spec.DSAGenParameterSpec( - primePLen : int, - subprimeQLen : int - ); - java.security.spec.DSAGenParameterSpec( - primePLen : int, - subprimeQLen : int, - seedLen : int - ); - } - -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/DSAParameterSpec.mark b/src/test/resources/Mark/bouncycastle/DSAParameterSpec.mark deleted file mode 100644 index 612f3bd..0000000 --- a/src/test/resources/Mark/bouncycastle/DSAParameterSpec.mark +++ /dev/null @@ -1,17 +0,0 @@ -package java.jca - -entity DSAParameterSpec { - - var p; - var q; - var g; - - - op intialize { - java.security.spec.DSAParameterSpec( - p : java.math.BigInteger, - q : java.math.BigInteger, - g : java.math.BigInteger - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/DSAPrivateKeySpec.mark b/src/test/resources/Mark/bouncycastle/DSAPrivateKeySpec.mark deleted file mode 100644 index cd1a37f..0000000 --- a/src/test/resources/Mark/bouncycastle/DSAPrivateKeySpec.mark +++ /dev/null @@ -1,18 +0,0 @@ -package java.jca - -entity DSAPrivateKeySpec { - - var x; - var p; - var q; - var g; - - op instantiate { - java.security.spec.DSAPrivateKeySpec( - x : java.math.BigInteger, - p : java.math.BigInteger, - q : java.math.BigInteger, - g : java.math.BigInteger - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/DSAPublicKeySpec.mark b/src/test/resources/Mark/bouncycastle/DSAPublicKeySpec.mark deleted file mode 100644 index cfe6735..0000000 --- a/src/test/resources/Mark/bouncycastle/DSAPublicKeySpec.mark +++ /dev/null @@ -1,18 +0,0 @@ -package java.jca - -entity DSAPublicKeySpec { - - var y; - var p; - var q; - var g; - - op instantiate { - java.security.spec.DSAPublicKeySpec( - y : java.math.BigInteger, - p : java.math.BigInteger, - q : java.math.BigInteger, - g : java.math.BigInteger - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/ECFieldF2m.mark b/src/test/resources/Mark/bouncycastle/ECFieldF2m.mark deleted file mode 100644 index e069f48..0000000 --- a/src/test/resources/Mark/bouncycastle/ECFieldF2m.mark +++ /dev/null @@ -1,22 +0,0 @@ -package java.jca - -entity ECFieldF2m { - - var m; - var ks; - var rp; - - op instantiate { - java.security.spec.ECFieldF2m( - m : int - ); - java.security.spec.ECFieldF2m( - m : int, - ks : int[] - ); - java.security.spec.ECFieldF2m( - m : int, - rp : java.math.BigInteger - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/ECFieldFp.mark b/src/test/resources/Mark/bouncycastle/ECFieldFp.mark deleted file mode 100644 index 0199f16..0000000 --- a/src/test/resources/Mark/bouncycastle/ECFieldFp.mark +++ /dev/null @@ -1,13 +0,0 @@ -package java.jca - -entity ECFieldFp { - - var p; - - - op instantiate { - java.security.spec.ECFieldFp( - p : java.math.BigInteger - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/ECGenParameterSpec.mark b/src/test/resources/Mark/bouncycastle/ECGenParameterSpec.mark deleted file mode 100644 index c0e451e..0000000 --- a/src/test/resources/Mark/bouncycastle/ECGenParameterSpec.mark +++ /dev/null @@ -1,14 +0,0 @@ -package java.jca - -entity ECGenParameterSpec { - - var stdName; - - - op instantiate { - java.security.spec.ECGenParameterSpec( - stdName : java.lang.String - ); - } - -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/ECKey.mark b/src/test/resources/Mark/bouncycastle/ECKey.mark deleted file mode 100644 index e80cae5..0000000 --- a/src/test/resources/Mark/bouncycastle/ECKey.mark +++ /dev/null @@ -1,5 +0,0 @@ -package java.jca - -entity ECKey { - -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/ECParameterSpec.mark b/src/test/resources/Mark/bouncycastle/ECParameterSpec.mark deleted file mode 100644 index 3eb4049..0000000 --- a/src/test/resources/Mark/bouncycastle/ECParameterSpec.mark +++ /dev/null @@ -1,19 +0,0 @@ -package java.jca - -entity ECParameterSpec { - - var curve; - var g; - var n; - var h; - - - op instantiate { - java.security.spec.ECParameterSpec( - curve : java.security.spec.EllipticCurve, - g : java.security.spec.ECPoint, - n : java.math.BigInteger, - h : int - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/ECPoint.mark b/src/test/resources/Mark/bouncycastle/ECPoint.mark deleted file mode 100644 index fb9a58a..0000000 --- a/src/test/resources/Mark/bouncycastle/ECPoint.mark +++ /dev/null @@ -1,14 +0,0 @@ -package java.jca - -entity ECPoint { - - var x; - var y; - - op instantiate { - java.security.spec.ECPoint( - x : java.math.BigInteger, - y : java.math.BigInteger - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/ECPrivateKeySpec.mark b/src/test/resources/Mark/bouncycastle/ECPrivateKeySpec.mark deleted file mode 100644 index 1f91cca..0000000 --- a/src/test/resources/Mark/bouncycastle/ECPrivateKeySpec.mark +++ /dev/null @@ -1,16 +0,0 @@ -package java.jca - -entity ECPrivateKeySpec { - - var s; - var params; - - - op instantiate { - java.security.spec.ECPrivateKeySpec( - s : java.math.BigInteger, - params : java.security.spec.ECParameterSpec - ); - } - -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/ECPublicKeySpec.mark b/src/test/resources/Mark/bouncycastle/ECPublicKeySpec.mark deleted file mode 100644 index 17bf1a5..0000000 --- a/src/test/resources/Mark/bouncycastle/ECPublicKeySpec.mark +++ /dev/null @@ -1,14 +0,0 @@ -package java.jca - -entity ECPublicKeySpec { - - var w; - var params; - - op instantiate { - java.security.spec.ECPublicKeySpec( - w : java.security.spec.ECPoint, - params : java.security.spec.ECParameterSpec - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/EncodedKeySpec.mark b/src/test/resources/Mark/bouncycastle/EncodedKeySpec.mark deleted file mode 100644 index eb52527..0000000 --- a/src/test/resources/Mark/bouncycastle/EncodedKeySpec.mark +++ /dev/null @@ -1,18 +0,0 @@ -package java.jca - -entity EncodedKeySpec { - - var encodedKey; - var algorithm; - - - op instantiate { - java.security.spec.EncodedKeySpec( - encodedKey : byte[] - ); - java.security.spec.EncodedKeySpec( - encodedKey : byte[], - algorithm : java.lang.String - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/GCMParameterSpec.mark b/src/test/resources/Mark/bouncycastle/GCMParameterSpec.mark deleted file mode 100644 index ed3c7f2..0000000 --- a/src/test/resources/Mark/bouncycastle/GCMParameterSpec.mark +++ /dev/null @@ -1,22 +0,0 @@ -package java.jca - -entity GCMParameterSpec { - - var tLen; - var src; - var offset; - var len; - - op instantiate { - javax.crypto.spec.GCMParameterSpec( - tLen : int, - src : byte[] - ); - javax.crypto.spec.GCMParameterSpec( - tLen : int, - src : byte[], - offset : int, - len : int - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/HMACParameterSpec.mark b/src/test/resources/Mark/bouncycastle/HMACParameterSpec.mark deleted file mode 100644 index a243d33..0000000 --- a/src/test/resources/Mark/bouncycastle/HMACParameterSpec.mark +++ /dev/null @@ -1,12 +0,0 @@ -package java.jca - -entity HMACParameterSpec { - - var outputLength; - - op instantiate { - javax.xml.crypto.dsig.spec.HMACParameterSpec( - outputLength : int - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/IvParameterSpec.mark b/src/test/resources/Mark/bouncycastle/IvParameterSpec.mark deleted file mode 100644 index 2ccc1a8..0000000 --- a/src/test/resources/Mark/bouncycastle/IvParameterSpec.mark +++ /dev/null @@ -1,19 +0,0 @@ -package java.jca - -entity IvParameterSpec { - - var iv; - var offset; - var len; - - op instantiate { - javax.crypto.spec.IvParameterSpec( - iv : byte[] - ); - javax.crypto.spec.IvParameterSpec( - iv : byte[], - offset : int, - len : int - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/KeyFactory.mark b/src/test/resources/Mark/bouncycastle/KeyFactory.mark deleted file mode 100644 index 7e7bb89..0000000 --- a/src/test/resources/Mark/bouncycastle/KeyFactory.mark +++ /dev/null @@ -1,31 +0,0 @@ -package java.jca - -entity KeyFactory { - - var algorithm; - var provider; - - var keyspec; - var prikey; - var pubkey; - - var inkey; - var outkey; - - op instantiate { - java.security.KeyFactory.getInstance(algorithm : java.lang.String); - java.security.KeyFactory.getInstance( - algorithm : java.lang.String, - provider : java.lang.String | java.security.Provider - ); - } - - op generate { - prikey = java.security.KeyFactory.generatePrivate(keyspec : java.security.spec.KeySpec); - pubkey = java.security.KeyFactory.generatePublic(keyspec : java.security.spec.KeySpec); - } - - op translate { - outkey = java.security.KeyFactory.translateKey(inkey : java.security.Key); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/KeyGenerator.mark b/src/test/resources/Mark/bouncycastle/KeyGenerator.mark deleted file mode 100644 index 718e1a4..0000000 --- a/src/test/resources/Mark/bouncycastle/KeyGenerator.mark +++ /dev/null @@ -1,40 +0,0 @@ -package java.jca - -entity KeyGenerator { - - var algorithm; - var provider; - - var keysize; - var random; - var params; - - var key; - - - op instantiate { - javax.crypto.KeyGenerator.getInstance(algorithm : java.lang.String); - javax.crypto.KeyGenerator.getInstance( - algorithm : java.lang.String, - provider : java.lang.String | java.security.Provider - ); - } - - op init { - javax.crypto.KeyGenerator.init(keysize : int); - javax.crypto.KeyGenerator.init( - keysize : int, - random : java.security.SecureRandom - ); - javax.crypto.KeyGenerator.init(random : java.security.SecureRandom); - javax.crypto.KeyGenerator.init(params : java.security.spec.AlgorithmParameterSpecs); - javax.crypto.KeyGenerator.init( - params : java.security.spec.AlgorithmParameterSpec, - random : java.security.SecureRandom - ); - } - - op generate { - key = javax.crypto.KeyGenerator.generateKey(); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/KeyPair.mark b/src/test/resources/Mark/bouncycastle/KeyPair.mark deleted file mode 100644 index 5067b13..0000000 --- a/src/test/resources/Mark/bouncycastle/KeyPair.mark +++ /dev/null @@ -1,14 +0,0 @@ -package java.jca - -entity KeyPair { - - var publicKey; - var privateKey; - - op instantiate { - java.security.KeyPair( - publicKey : java.security.PublicKey, - privateKey : java.security.PrivateKey - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/KeyPairGenerator.mark b/src/test/resources/Mark/bouncycastle/KeyPairGenerator.mark deleted file mode 100644 index b9f919f..0000000 --- a/src/test/resources/Mark/bouncycastle/KeyPairGenerator.mark +++ /dev/null @@ -1,46 +0,0 @@ -package java.jca - -entity KeyPairGenerator { - - var algorithm; - var provider; - - var keysize; - var random; - var params; - - var keypair; - - - op instantiate { - java.security.KeyPairGenerator.getInstance( - algorithm : java.lang.String - ); - java.security.KeyPairGenerator.getInstance( - algorithm : java.lang.String, - provider : java.lang.String | java.security.Provider - ); - } - - op initialize { - java.security.KeyPairGenerator.initialize( - keysize : int - ); - java.security.KeyPairGenerator.initialize( - keysize : int, - random : java.security.SecureRandom - ); - java.security.KeyPairGenerator.initialize( - params : java.security.spec.AlgorithmParameterSpec - ); - java.security.KeyPairGenerator.initialize( - params : java.security.spec.AlgorithmParameterSpec, - random : java.security.SecureRandom - ); - } - - op generate { - keypair = java.security.KeyPairGenerator.generateKeyPair(); - keypair = java.security.KeyPairGenerator.genKeyPair(); - } -} diff --git a/src/test/resources/Mark/bouncycastle/KeyStore.PasswordProtection.mark b/src/test/resources/Mark/bouncycastle/KeyStore.PasswordProtection.mark deleted file mode 100644 index 64a91ba..0000000 --- a/src/test/resources/Mark/bouncycastle/KeyStore.PasswordProtection.mark +++ /dev/null @@ -1,19 +0,0 @@ -package java.jca - -entity KeyStore.PasswordProtection { - - var password; - var protectionAlgorithm; - var protectionParameters; - - op instantiate { - java.security.KeyStore.PasswordProtection( - password : char[] - ); - java.security.KeyStore.PasswordProtection( - password : char[], - protectionAlgorithm : java.lang.String, - protectionParameters : java.security.spec.AlgorithmParameterSpec - ); - } -} diff --git a/src/test/resources/Mark/bouncycastle/KeyStore.mark b/src/test/resources/Mark/bouncycastle/KeyStore.mark deleted file mode 100644 index e92b2a1..0000000 --- a/src/test/resources/Mark/bouncycastle/KeyStore.mark +++ /dev/null @@ -1,60 +0,0 @@ -package java.jca - -entity KeyStore { - - var file; - var password; - var param; - var type; - var provider; - - var alias; - var cert; - var entry; - var protParam; - var rawKey; - var certChain; - var key; - - - op instantiate { - java.security.KeyStore.getInstance( - file : java.io.File, - password : char[] - ); - java.security.KeyStore.getInstance( - file : java.io.File, - param : java.security.KeyStore.LoadStoreParameter - ); - java.security.KeyStore.getInstance( - type : java.lang.String - ); - java.security.KeyStore.getInstance( - type : java.lang.String, - provider : java.lang.String | java.security.Provider - ); - } - - op store { - java.security.KeyStore.setCertificateEntry( - alias : java.lang.String, - cert : java.security.cert.Certificate - ); - java.security.KeyStore.setEntry( - alias : java.lang.String, - entry : java.security.KeyStore.Entry, - protParam : java.security.KeyStore.ProtectionParameter - ); - java.security.KeyStore.setKeyEntry( - alias : java.lang.String, - rawKey : byte[], - certChain : java.security.cert.Certificate[] - ); - java.security.KeyStore.setKeyEntry( - alias : java.lang.String, - key : java.security.Key, - password : char[], - certChain : java.security.cert.Certificate[] - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/MGF1ParameterSpec.mark b/src/test/resources/Mark/bouncycastle/MGF1ParameterSpec.mark deleted file mode 100644 index 208f2ae..0000000 --- a/src/test/resources/Mark/bouncycastle/MGF1ParameterSpec.mark +++ /dev/null @@ -1,12 +0,0 @@ -package java.jca - -entity MGF1ParameterSpec { - - var mdName; - - op instantiate { - java.security.spec.MGF1ParameterSpec( - mdName : java.lang.String - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/MessageDigest.mark b/src/test/resources/Mark/bouncycastle/MessageDigest.mark deleted file mode 100644 index 1ce9bd7..0000000 --- a/src/test/resources/Mark/bouncycastle/MessageDigest.mark +++ /dev/null @@ -1,39 +0,0 @@ -package java.jca - -/* - * Represents java.security.MessageDigest - */ -entity MessageDigest { - - var algorithm; - var provider; - var input; - var digest; - - op instantiate { - java.security.MessageDigest.getInstance(algorithm : java.lang.String); - java.security.MessageDigest.getInstance( - algorithm : java.lang.String, - provider : java.lang.String | java.security.Provider - ); - } - - op update { - java.security.MessageDigest.update(input : byte | byte[] | java.nio.ByteBuffer); - java.security.MessageDigest.update( - input : byte[], - ... - ); - } - - op digest { - digest = java.security.MessageDigest.digest(); - digest = java.security.MessageDigest.digest(input : byte[]); - java.security.MessageDigest.digest(digest : byte[], ...); - } - - op reset { - java.security.MessageDigest.reset(); - } - -} diff --git a/src/test/resources/Mark/bouncycastle/OAEPParameterSpec.mark b/src/test/resources/Mark/bouncycastle/OAEPParameterSpec.mark deleted file mode 100644 index 4d9060a..0000000 --- a/src/test/resources/Mark/bouncycastle/OAEPParameterSpec.mark +++ /dev/null @@ -1,19 +0,0 @@ -package java.jca - -entity OAEPParameterSpec { - - var mdName; - var mgfName; - var mgfSpec; - var pSrc; - - op instantiate { - javax.crypto.spec.OAEPParameterSpec( - mdName : java.lang.String, - mgfName : java.lang.String, - mgfSpec : java.security.spec.AlgorithmParameterSpec, - pSrc : javax.crypto.spec.PSource - ); - } - -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/PBEKey.mark b/src/test/resources/Mark/bouncycastle/PBEKey.mark deleted file mode 100644 index b19111e..0000000 --- a/src/test/resources/Mark/bouncycastle/PBEKey.mark +++ /dev/null @@ -1,5 +0,0 @@ -package java.jca - -entity PBEKey { - -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/PBEKeySpec.mark b/src/test/resources/Mark/bouncycastle/PBEKeySpec.mark deleted file mode 100644 index aa14dcb..0000000 --- a/src/test/resources/Mark/bouncycastle/PBEKeySpec.mark +++ /dev/null @@ -1,26 +0,0 @@ -package java.jca - -entity PBEKeySpec { - - var password; - var salt; - var iterationCount; - var keyLength; - - op instantiate { - javax.crypto.spec.PBEKeySpec( - password : byte[] - ); - javax.crypto.spec.PBEKeySpec( - password : byte[], - salt : byte[], - iterationCount : int - ); - javax.crypto.spec.PBEKeySpec( - password : byte[], - salt : byte[], - iterationCount : int, - keyLength : int - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/PBEParameterSpec.mark b/src/test/resources/Mark/bouncycastle/PBEParameterSpec.mark deleted file mode 100644 index debd301..0000000 --- a/src/test/resources/Mark/bouncycastle/PBEParameterSpec.mark +++ /dev/null @@ -1,20 +0,0 @@ -package java.jca - -entity PBEParameterSpec { - - var salt; - var iterationCount; - var paramSpec; - - op instantiate { - javax.crypto.spec.PBEParameterSpec( - salt : byte[], - iterationCount : int - ); - javax.crypto.spec.PBEParameterSpec( - salt : byte[], - iterationCount : int, - paramSpec : java.security.AlgorithmParameter - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/PKCS8EncodedKeySpec.mark b/src/test/resources/Mark/bouncycastle/PKCS8EncodedKeySpec.mark deleted file mode 100644 index bb19813..0000000 --- a/src/test/resources/Mark/bouncycastle/PKCS8EncodedKeySpec.mark +++ /dev/null @@ -1,18 +0,0 @@ -package java.jca - -entity PKCS8EncodedKeySpec { - - var encodedKey; - var algorithm; - - op instantiate { - java.security.spec.PKCS8EncodedKeySpec( - encodedKey : byte[] - ); - java.security.spec.PKCS8EncodedKeySpec( - encodedKey : byte[], - algorithm : java.lang.String - ); - } - -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/PSSParameterSpec.mark b/src/test/resources/Mark/bouncycastle/PSSParameterSpec.mark deleted file mode 100644 index 5b7256e..0000000 --- a/src/test/resources/Mark/bouncycastle/PSSParameterSpec.mark +++ /dev/null @@ -1,23 +0,0 @@ -package java.jca - -entity PSSParameterSpec { - - var saltLen; - var mdName; - var mgfName; - var mgfSpec; - var trailerField; - - op instantiate { - java.security.spec.PSSParameterSpec( - saltLen : int - ); - java.security.spec.PSSParameterSpec( - mdName : java.lang.String, - mgfName : java.lang.String, - mgfSpec : java.security.spec.AlgorithmParameterSpec, - saltLen : int, - trailerField : int - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/PrivateKey.mark b/src/test/resources/Mark/bouncycastle/PrivateKey.mark deleted file mode 100644 index 48d7aa1..0000000 --- a/src/test/resources/Mark/bouncycastle/PrivateKey.mark +++ /dev/null @@ -1,5 +0,0 @@ -package java.jca - -entity PrivateKey { - -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/PublicKey.mark b/src/test/resources/Mark/bouncycastle/PublicKey.mark deleted file mode 100644 index 44b8aff..0000000 --- a/src/test/resources/Mark/bouncycastle/PublicKey.mark +++ /dev/null @@ -1,5 +0,0 @@ -package java.jca - -entity PublicKey { - -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/RSAKeyGenParameterSpec.mark b/src/test/resources/Mark/bouncycastle/RSAKeyGenParameterSpec.mark deleted file mode 100644 index b8b938e..0000000 --- a/src/test/resources/Mark/bouncycastle/RSAKeyGenParameterSpec.mark +++ /dev/null @@ -1,20 +0,0 @@ -package java.jca - -entity RSAKeyGenParameterSpec { - - var keySize; - var publicExponent; - var keyParams; - - op instantiate { - java.security.spec.RSAKeyGenParameterSpec( - keysize : int, - publicExponent : java.math.BigInteger - ); - java.security.spec.RSAKeyGenParameterSpec( - keysize : int, - publicExponent : java.math.BigInteger, - keyParams : java.security.spec.AlgorithmParameterSpec - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/RSAMultiPrimePrivateCrtKeySpec.mark b/src/test/resources/Mark/bouncycastle/RSAMultiPrimePrivateCrtKeySpec.mark deleted file mode 100644 index 81c6d51..0000000 --- a/src/test/resources/Mark/bouncycastle/RSAMultiPrimePrivateCrtKeySpec.mark +++ /dev/null @@ -1,42 +0,0 @@ -package java.jca - -entity RSAMultiPrimePrivateCrtKeySpec { - - var modulus; - var publicExponent; - var privateExponent; - var primeP; - var primeQ; - var primeExponentP; - var primeExponentQ; - var crtCoefficient; - var otherPrimeInfo; - var keyParams; - - - op instantiate { - java.security.spec.RSAMultiPrimePrivateCrtKeySpec( - modulus : java.math.BigInteger, - publicExponent : java.math.BigInteger, - privateExponent : java.math.BigInteger, - primeP : java.math.BigInteger, - primeQ : java.math.BigInteger, - primeExponentP : java.math.BigInteger, - primeExponentQ : java.math.BigInteger, - crtCoefficient : java.math.BigInteger, - otherPrimeInfo : java.security.spec.RSAOtherPrimeInfo[] - ); - java.security.spec.RSAMultiPrimePrivateCrtKeySpec( - modulus : java.math.BigInteger, - publicExponent : java.math.BigInteger, - privateExponent : java.math.BigInteger, - primeP : java.math.BigInteger, - primeQ : java.math.BigInteger, - primeExponentP : java.math.BigInteger, - primeExponentQ : java.math.BigInteger, - crtCoefficient : java.math.BigInteger, - otherPrimeInfo : java.security.spec.RSAOtherPrimeInfo[], - keyParams : java.security.spec.AlgorithmParameterSpec - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/RSAPrivateCrtKeySpec.mark b/src/test/resources/Mark/bouncycastle/RSAPrivateCrtKeySpec.mark deleted file mode 100644 index 8c506c5..0000000 --- a/src/test/resources/Mark/bouncycastle/RSAPrivateCrtKeySpec.mark +++ /dev/null @@ -1,38 +0,0 @@ -package java.jca - -entity RSAPrivateCrtKeySpec { - - var modulus; - var publicExponent; - var privateExponent; - var primeP; - var primeQ; - var primeExponentP; - var primeExponentQ; - var crtCoefficient; - var keyParams; - - op instantiate { - java.security.spec.RSAPrivateCrtKeySpec( - modulus : java.math.BigInteger, - publicExponent : java.math.BigInteger, - privateExponent : java.math.BigInteger, - primeP : java.math.BigInteger, - primeQ : java.math.BigInteger, - primeExponentP : java.math.BigInteger, - primeExponentQ : java.math.BigInteger, - crtCoefficient : java.math.BigInteger - ); - java.security.spec.RSAPrivateCrtKeySpec( - modulus : java.math.BigInteger, - publicExponent : java.math.BigInteger, - privateExponent : java.math.BigInteger, - primeP : java.math.BigInteger, - primeQ : java.math.BigInteger, - primeExponentP : java.math.BigInteger, - primeExponentQ : java.math.BigInteger, - crtCoefficient : java.math.BigInteger, - keyParams : java.security.spec.AlgorithmParameterSpec - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/RSAPrivateKeySpec.mark b/src/test/resources/Mark/bouncycastle/RSAPrivateKeySpec.mark deleted file mode 100644 index 69a8732..0000000 --- a/src/test/resources/Mark/bouncycastle/RSAPrivateKeySpec.mark +++ /dev/null @@ -1,21 +0,0 @@ -package java.jca - -entity RSAPrivateKeySpec { - - var modulus; - var privateExponent; - var params; - - - op instantiate { - java.security.spec.RSAPrivateKeySpec( - modulus : java.math.BigInteger, - privateExponent : java.math.BigInteger - ); - java.security.spec.RSAPrivateKeySpec( - modulus : java.math.BigInteger, - privateExponent : java.math.BigInteger, - params : java.security.spec.AlgorithmParameterSpec - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/RSAPublicKeySpec.mark b/src/test/resources/Mark/bouncycastle/RSAPublicKeySpec.mark deleted file mode 100644 index 32f8764..0000000 --- a/src/test/resources/Mark/bouncycastle/RSAPublicKeySpec.mark +++ /dev/null @@ -1,21 +0,0 @@ -package java.jca - -entity RSAPublicKeySpec { - - var modulus; - var publicExponent; - var params; - - - op instantiate { - java.security.spec.RSAPublicKeySpec( - modulus : java.math.BigInteger, - publicExponent : java.math.BigInteger - ); - java.security.spec.RSAPublicKeySpec( - modulus : java.math.BigInteger, - publicExponent : java.math.BigInteger, - params : java.security.spec.AlgorithmParameterSpec - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/RulesBase_Cipher.mark b/src/test/resources/Mark/bouncycastle/RulesBase_Cipher.mark deleted file mode 100644 index 84327ff..0000000 --- a/src/test/resources/Mark/bouncycastle/RulesBase_Cipher.mark +++ /dev/null @@ -1,50 +0,0 @@ -package base.jca - -/** - * - */ -rule Crypt { - using - Cipher as c - when - _split(c.transform, "/", 1) in ["CBC", "CTR"] - && (c.opmode == "Cipher.ENCRYPT_MODE" - || c.opmode == "Cipher.DECRYPT_MODE" - || c.opmode == "1" /* ENCRYPT_MODE */ - || c.opmode == "2" /* DECRYPT_MODE */ - ) - ensure - order c.instantiate(), - c.init(), - c.update()*, - c.finalize() - onfail - InvalidOrderOfCipherOperations -} - -/** - * - */ -rule AEAD_Crypt { - using - Cipher as c - when - _split(c.transform, "/", 1) in ["CCM", "GCM"] - && (c.opmode == "Cipher.ENCRYPT_MODE" - || c.opmode == "Cipher.DECRYPT_MODE" - || c.opmode == "1" /* ENCRYPT_MODE */ - || c.opmode == "2" /* DECRYPT_MODE */ - ) - ensure - order c.instantiate(), - c.init(), - c.aad()*, /* optional because only called if actually supplying AAD */ - c.update()*, - c.finalize() - onfail - InvalidOrderforAEAD -} - - -//// TODO order rule for key wrap -//// TODO order rule for key unwrapping \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/RulesTR_Cipher.mark b/src/test/resources/Mark/bouncycastle/RulesTR_Cipher.mark deleted file mode 100644 index d371cb8..0000000 --- a/src/test/resources/Mark/bouncycastle/RulesTR_Cipher.mark +++ /dev/null @@ -1,371 +0,0 @@ -package rules.bsi.tr_02102_1.v2019_01 - -/** - * BSI TR-02102-1 (Version 2019-01), 2.1. Blockchiffren - * - block ciphers - */ -rule ID_2_01 { - using - Cipher as c - ensure - _split(c.transform, "/", 0) in ["AES"] /* BSI TR-02102-1, ID 2.01 */ - || _split(c.transform, "/", 0) in ["RSA"] - onfail - Invalid_TR21021_Cipher -} - -/** - * BSI TR-02102-1 (Version 2019-01), 2.1.1. Betriebsarten - * - block cipher modes - */ -rule ID_2_1_01 { - using - Cipher as c - when - _split(c.transform, "/", 0) in ["AES"] - ensure - /* */ - _split(c.transform, "/", 1) in ["CCM", "GCM", "CBC", "CTR"] - onfail - InvalidCipherModeforAESBlockCipher -} - -/** - * BSI TR-02102-1 (Version 2019-01), 2.1.2. Betriebsbedingungen - * - CCM non-repeated IV during key period - * - * Note: - * - seems to be not checkable. We cannot sufficiently reason about the dynamic behaviour of the program to check this rule. - */ -//rule ID_2_1_2_1_01 { -// using -// Cipher as c -// when -// _split(c.transform, "/", 0) in ["AES"] -// && _split(c.transform, "/", 1) in ["CCM"] -// ensure -// false -// onfail -// InsufficientCCMIVRenewal -//} - -/** - * BSI TR-02102-1 (Version 2019-01), 2.1.2. Betriebsbedingungen - * - CCM minimum length of authentication tag - * - * Note: - * Bouncy Castle uses GCMParameterSpec for AEAD cipher initialization. - */ -rule ID_2_1_2_1_02 { - using - Cipher as c, - GCMParameterSpec as gcmspec - when - _split(c.transform, "/", 0) in ["AES"] - && _split(c.transform, "/", 1) in ["CCM"] - ensure - _is(c.paramspec,gcmspec) - && gcmspec.tLen >= 64 - onfail - InsufficientCCMTagLength -} - - -/** - * BSI TR-02102-1 (Version 2019-01), 2.1.2. Betriebsbedingungen - * - GCM non-repeated IV during key period - * - * Note: - * - seems to be not checkable. We cannot sufficiently reason about the dynamic behaviour of the program to check this rule. - */ -//rule ID_2_1_2_2_01 { -// using -// Cipher as c, -// GCMParameterSpec as gcm -// when -// _split(c.transform, "/", 0) in ["AES"] -// && _split(c.transform, "/", 1) in ["GCM"] -// ensure -// false -// onfail -// InvalidGCMIV -//} - -/** - * BSI TR-02102-1 (Version 2019-01), 2.1.2. Betriebsbedingungen - * - GCM nonce length for authentication tag - */ -rule ID_2_1_2_2_02 { - using - Cipher as c, - GCMParameterSpec as gcm - when - _split(c.transform, "/", 0) in ["AES"] - && _split(c.transform, "/", 1) in ["GCM"] - && _is(c.paramspec, gcm) - ensure - (_has_value(gcm.src) && _length(gcm.src) == 12) /* in bytes */ - || (_has_value(gcm.len) && gcm.len == 12) - onfail - InvalidGCMAuthenticationNonceLength -} - -/** - * BSI TR-02102-1 (Version 2019-01), 2.1.2. Betriebsbedingungen - * - GCM minimum length of authentication tag - */ -rule ID_2_1_2_2_03 { - using - Cipher as c, - GCMParameterSpec as gcm - when - _split(c.transform, "/", 0) in ["AES"] - && _split(c.transform, "/", 1) in ["GCM"] - && _is(c.paramspec, gcm) - ensure - gcm.tLen >= 96 /* apparently, there are fixed sizes 96, 104, 112, 120 and 128 */ - onfail - InsufficientGCMTagLength -} - - -/** - * BSI TR-02102-1 (Version 2019-01), 2.1.2. Betriebsbedingungen - * - CBC unpredictable IV - */ -rule ID_2_1_2_3_01 { - using - Cipher as c, - IvParameterSpec as ivspec, - SecureRandom as sr - when - _split(c.transform, "/", 0) in ["AES"] - && _split(c.transform, "/", 1) in ["CBC"] - ensure - // IMPROV not just random IV - _is(c.paramspec, ivspec) - && _is(ivspec.iv, sr.randomBytes) - onfail - InvalidCBCIV -} - -/** - * BSI TR-02102-1 (Version 2019-01), 2.1.2. Betriebsbedingungen - * - CTR non-repeated counter during key period - * - * Note: - * - seems to be not checkable. We cannot sufficiently reason about the dynamic behaviour of the program to check this rule. - */ -//rule ID_2_1_2_4_01 { -// using -// Cipher as c -// when -// _split(c.transform, "/", 0) in ["AES"] -// && _split(c.transform, "/", 1) in ["CTR"] -// ensure -// false -// onfail -// InvalidCTRCounter -//} - -/** - * BSI TR-02102-1 (Version 2019-01), 2.1.3. Paddingverfahren - * - CBC padding - */ -rule ID_2_1_3_01 { - using - Cipher as c - when - _split(c.transform, "/", 0) in ["AES"] - && _split(c.transform, "/", 1) in ["CBC"] - ensure - _split(c.transform, "/", 2) in [ - "ISO7816-4Padding", // 1. ISO-Padding, siehe [57], padding method 2 und [73], Appendix A - "PKCS5Padding", "PKCS7Padding" // 2. Padding gemäß [87], Abschnitt 6.3 - ] - onfail - InvalidCBCPadding -} - - -/** - * BSI TR-02102-1 (Version 2019-01), 2.2. Stromchiffren - * - Integrity protection (e.g MAC) - * - * Note: - * - included in rule ID_2_2_02 - */ -//rule ID_2_2_01 { -// using -// Cipher as c -// ensure -// true -// onfail -// InsufficientStreamCipherIntegrityProtection -//} - -/** - * BSI TR-02102-1 (Version 2019-01), 2.2. Stromchiffren - * - AES/CTR with MAC - */ -rule ID_2_2_02 { - using - Cipher as c, - Mac as m - when - _split(c.transform, "/", 0) in ["AES"] - && _split(c.transform, "/", 1) in ["CTR"] - ensure - // IMPROV insufficient application of MAC; MAC over complete stream required - _is(c.output, m.input) - onfail - InsufficientAESCTRIntegrityProtection -} - - -/** - * BSI TR-02102-1 (Version 2019-01), 3.3. ECIES-Verschlüsselungsverfahren - * - ECIES order of decryption operations - * - * Note: - * - implementation not provided by Bouncy Castle. Implementation by user likely to be error prone and should not be encouraged. - */ -//rule ID_3_3_01 { -// using -// Cipher as c -// when -// false -// ensure -// false -// onfail -// InvalidECIESOperationOrder -//} - -/** - * BSI TR-02102-1 (Version 2019-01), 3.3. ECIES-Verschlüsselungsverfahren - * - ECIES curve parameters - * - * Note: - * - implementation not provided by Bouncy Castle. Implementation by user likely to be error prone and should not be encouraged. - */ -//rule ID_3_3_02 { -// using -// Cipher as c -// when -// false -// ensure -// false -// onfail -// InvalidECIESCurveParameter -//} - -/** - * BSI TR-02102-1 (Version 2019-01), 3.3. ECIES-Verschlüsselungsverfahren - * - ECIES key derivation - * - * Note: - * - implementation not provided by Bouncy Castle. Implementation by user likely to be error prone and should not be encouraged. - */ -//rule ID_3_3_03 { -// using -// Cipher as c -// when -// false -// ensure -// false -// onfail -// InvalidECIESSymmetricKeyDerivation -//} - -/** - * BSI TR-02102-1 (Version 2019-01), 3.3. ECIES-Verschlüsselungsverfahren - * - ECIES order of EC base point - * - * Note: - * - seems to be not checkable. We cannot sufficiently reason about the supplied order of an EC base point - */ -//rule ID_3_3_04 { -// using -// Cipher as c -// when -// false -// ensure -// false -// onfail -// InsufficientECIESBasePointOrder -//} - -/** - * BSI TR-02102-1 (Version 2019-01), 3.4. DLIES-Verschlüsselungsverfahren - * - * Note: - * - implementation not provided by Bouncy Castle. Implementation by user likely to be error prone and should not be encouraged. - * - applies to requirements ID 3.4.01, ID 3.4.02 and ID 3.4.03 - */ - - -/** - * BSI TR-02102-1 (Version 2019-01), 3.5. RSA - * - RSA EME-OAEP formatting scheme - */ -rule ID_3_5_01 { - using - Cipher as c - when - _split(c.transform, "/", 0) == "RSA" - //&& ( - // c.opmode == "javax.crypto.Cipher.ENCRYPT_MODE" - // || c.opmode == "javax.crypto.Cipher.DECRYPT_MODE" - // || c.opmode == "1" - // || c.opmode == "2" - // ) - ensure - _split(c.transform, "/", 2) in [ - // "OAEPWITHSHA1ANDMGF1PADDING", "OAEPWITHSHA-1ANDMGF1PADDING" // recommended by referenced RFC 8017, but not a recommended hash function by BSI - // "OAEPWITHSHA224ANDMGF1PADDING", "OAEPWITHSHA-224ANDMGF1PADDING", // recommended by referenced RFC 8017, but not a recommended hash function by BSI - "OAEPWITHSHA256ANDMGF1PADDING", "OAEPWITHSHA-256ANDMGF1PADDING", - "OAEPWITHSHA384ANDMGF1PADDING", "OAEPWITHSHA-384ANDMGF1PADDING", - "OAEPWITHSHA512ANDMGF1PADDING", "OAEPWITHSHA-512ANDMGF1PADDING"//, - // "OAEPWITHSHA3-256ANDMGF1PADDING", // not listed by referenced RFC 8017, but recommended hash function by BSI - // "OAEPWITHSHA3-384ANDMGF1PADDING", // not listed by referenced RFC 8017, but recommended hash function by BSI - // "OAEPWITHSHA3-512ANDMGF1PADDING" // not listed by referenced RFC 8017, but recommended hash function by BSI - ] - onfail - InvalidRSAPadding -} - -/** - * BSI TR-02102-1 (Version 2019-01), 3.5. RSA - * - RSA minimum length of modulus - * - * Note: - * - seems to be not checkable. We cannot sufficiently reason about the supplied RSA public key. - */ -//rule ID_3_5_02 { -// using -// Cipher as c, -// RSAPublicKeySpec as pubKey -// when -// _split(c.transform, "/", 0) == "RSA" -// && c.opmode == "javax.crypto.Cipher.ENCRYPT_MODE" -// ensure -// false -// onfail -// InsufficientRSAKeylength // FIXME valid until 2022 -//} - -/* - * Check RSA key length on key generation. - * - * Ensures that generated keys have sufficient length in compliance with BSI TR-02102-1 (Version 2019-01). - * - */ -rule ID_3_5_02_RSAKeyGenParameterSpec { - using - RSAKeyGenParameterSpec as rsaKeyGenSpec - ensure - rsaKeyGenSpec.keysize >= 2000 - onfail - InsufficientRSAKeylength // FIXME valid until 2022 -} - diff --git a/src/test/resources/Mark/bouncycastle/RulesTR_InstanceAuthentication.txt b/src/test/resources/Mark/bouncycastle/RulesTR_InstanceAuthentication.txt deleted file mode 100644 index 2fc11a0..0000000 --- a/src/test/resources/Mark/bouncycastle/RulesTR_InstanceAuthentication.txt +++ /dev/null @@ -1,8 +0,0 @@ -package rules.bsi.tr_02102_1.v2019_01 - -/** - * BSI TR-02102-1 (Version 2019-01), 6. Instanzauthentisierung - * - * Note: - * - requirements ID 6.1.01 and ID 6.1.02 already covered by rules for Cipher and MAC - */ diff --git a/src/test/resources/Mark/bouncycastle/RulesTR_KeyAgreement.txt b/src/test/resources/Mark/bouncycastle/RulesTR_KeyAgreement.txt deleted file mode 100644 index a5ea628..0000000 --- a/src/test/resources/Mark/bouncycastle/RulesTR_KeyAgreement.txt +++ /dev/null @@ -1,10 +0,0 @@ -package rules.bsi.tr_02102_1.v2019_01 - -/** - * BSI TR-02102-1 (Version 2019-01), 7. Schlüsseleinigungsverfahren, Schlüsseltransportverfahren und Key-Update - * - * Note: - * - requirements ID 7.1.1.01, ID 7.1.1.02 and ID 7.2.1.01 already covered by rules for Cipher and MAC - * - implementation for requirement ID 7.1.2.01 not provided by Bouncy Castle. Implementation by user likely to be error prone and should not be encouraged. - * - unable to check requirements ID 7.2.2.1.01 and ID 7.2.2.2.01. We cannot sufficiently reason about supplied key length or parameters - */ diff --git a/src/test/resources/Mark/bouncycastle/RulesTR_MAC.mark b/src/test/resources/Mark/bouncycastle/RulesTR_MAC.mark deleted file mode 100644 index c30e1ce..0000000 --- a/src/test/resources/Mark/bouncycastle/RulesTR_MAC.mark +++ /dev/null @@ -1,187 +0,0 @@ -package rules.bsi.tr_02102_1.v2019_01 - -/** - * BSI TR-02102-1 (Version 2019-01), 5.3. Message Authentication Code (MAC) - * - MAC algorithms - */ -rule ID_5_3_01 { - using - Mac as m - ensure - m.algorithm in [ - "AESCMAC", // CMACs - "HMACSHA256", "HMACSHA512/256", "HMACSHA384", "HMACSHA512", "HMACSHA3-256", "HMACSHA3-384", "HMACSHA3-512", // HMACs - "AES-GMAC" // GMACs - ] - onfail - InvalidMACAlgorithm -} - - -/** - * BSI TR-02102-1 (Version 2019-01), 5.3. Message Authentication Code (MAC) - * - CMAC minimum key length - */ -rule ID_5_3_02_CMAC_Keygen { - using - Mac as m, - KeyGenerator as kg - when - m.algorithm in ["AESCMAC"] - && _is(m.key, kg.key) - ensure - // find a keygenerator of sufficient size - _is(m.key, kg.key) - && kg.keysize >= 128 - onfail - InsufficientCMACKeyLength -} - -/** - * BSI TR-02102-1 (Version 2019-01), 5.3. Message Authentication Code (MAC) - * - CMAC minimum key length - */ -rule ID_5_3_02_CMAC_HMAC_SecretKeyFactory { - using - Mac as m, - SecretKeySpec as sks, - SecretKeyFactory as kf - when - m.algorithm in ["AESCMAC"] - && _is(m.key, kf.outkey) - ensure - // find a keygenerator of sufficient size - _is(m.key, kf.outkey) - && _is(kf.keyspec, sks) - && ( - (_has_value(sks.len) && sks.len >= 128) - || (!(_has_value(sks.len)) && _has_value(sks.key) && _length(sks.key) >= 16) - ) - onfail - InsufficientCMACKeyLength -} - -/** - * BSI TR-02102-1 (Version 2019-01), 5.3. Message Authentication Code (MAC) - * - HMAC minimum key length - */ -rule ID_5_3_02_HMAC_Keygen { - using - Mac as m, - KeyGenerator as kg - when - m.algorithm in ["HMACSHA256", "HMACSHA512/256", "HMACSHA384", "HMACSHA512", "HMACSHA3-256", "HMACSHA3-384", "HMACSHA3-512"] - && _is(m.key, kg.key) - ensure - // find a keygenerator of sufficient size - ( - _is(m.key, kg.key) - && kg.keysize >= 128 - ) - || ( - _is(m.key, kg.key) - && kg.algorithm == m.algorithm - ) - onfail - InsufficientHMACKeyLength -} - -/** - * BSI TR-02102-1 (Version 2019-01), 5.3. Message Authentication Code (MAC) - * - HMAC minimum key length - */ -rule ID_5_3_02_HMAC_SecretKeyFactory { - using - Mac as m, - SecretKeySpec as sks, - SecretKeyFactory as kf - when - m.algorithm in ["HMACSHA256", "HMACSHA512/256", "HMACSHA384", "HMACSHA512", "HMACSHA3-256", "HMACSHA3-384", "HMACSHA3-512"] - && _is(m.key, kf.outkey) - && _is(kf.keyspec, sks) - ensure - ( - _is(m.key, kf.outkey) - && _is(kf.keyspec, sks) - && ( - (_has_value(sks.len) && sks.len >= 128) - || (!(_has_value(sks.len)) && _has_value(sks.key) && _length(sks.key) >= 16) - ) - ) - onfail - InsufficientHMACKeyLength -} - -/** - * BSI TR-02102-1 (Version 2019-01), 5.3. Message Authentication Code (MAC) - * - GMAC minimum key length - */ -rule ID_5_3_02_GMAC { - using - Mac as m, - KeyGenerator as kg, - SecretKeySpec as sks, - SecretKeyFactory as kf - when - m.algorithm in ["AES-GMAC"] && ( _is(m.key, kg.key) - || ( _is(m.key, kf.outkey) && _is(kf.keyspec, sks) ) ) - ensure - // find a keygenerator of sufficient size - kg.keysize >= 128 - || (_has_value(sks.len) && sks.len >= 128) - || (!(_has_value(sks.len)) && _has_value(sks.key) && _length(sks.key) >= 16) - onfail - InsufficientGMACKeyLength -} - - -/** - * BSI TR-02102-1 (Version 2019-01), 5.3. Message Authentication Code (MAC) - * - MAC minimum length of authentication tag - */ -rule ID_5_3_03_CMAC { - using - Mac as m - when - m.algorithm in ["AESCMAC"] - ensure - // TODO check in future releases of Bouncy Castle - true // Bouncy Castle implementation uses block size of AES in bits (128) by default - onfail - InsufficientCMACTagLength -} - -/** -* BSI TR-02102-1 (Version 2019-01), 5.3. Message Authentication Code (MAC) - * - HMAC minimum length of authentication tag - */ -rule ID_5_3_03_HMAC { - using - Mac as m, - HMACParameterSpec as spec - when - m.algorithm in ["HMACSHA256", "HMACSHA512/256", "HMACSHA384", "HMACSHA512", "HMACSHA3-256", "HMACSHA3-384", "HMACSHA3-512"] - && _is(m.params, spec) - ensure - _is(m.params, spec) - && spec.outputLength >= 96 - onfail - InsufficientHMACTagLength -} - -/** -* BSI TR-02102-1 (Version 2019-01), 5.3. Message Authentication Code (MAC) - * - GMAC minimum length of authentication tag - */ -rule ID_5_3_03_GMAC { - using - Mac as m - when - m.algorithm in ["AES-GMAC"] - ensure - // TODO check in future releases of Bouncy Castle - true // Bouncy Castle uses 128 bits by default - onfail - InsufficientGMACTagLength -} - diff --git a/src/test/resources/Mark/bouncycastle/RulesTR_MessageDigest.mark b/src/test/resources/Mark/bouncycastle/RulesTR_MessageDigest.mark deleted file mode 100644 index 86d407b..0000000 --- a/src/test/resources/Mark/bouncycastle/RulesTR_MessageDigest.mark +++ /dev/null @@ -1,17 +0,0 @@ -package rules.bsi.tr_02102_1.v2019_01 - -/** - * BSI TR-02102-1 (Version 2019-01), 4. Hashfunktionen - * - hash functions - */ -rule ID_4_01 { - using - MessageDigest as md - ensure - md.algorithm in [ - "SHA-256", "SHA-512/256", "SHA-384", "SHA-512", // SHA-2 - "SHA3-256", "SHA3-384", "SHA3-512" // SHA-3 - ] - onfail - InvalidHashFunction -} diff --git a/src/test/resources/Mark/bouncycastle/RulesTR_PRNG.txt b/src/test/resources/Mark/bouncycastle/RulesTR_PRNG.txt deleted file mode 100644 index bd975b5..0000000 --- a/src/test/resources/Mark/bouncycastle/RulesTR_PRNG.txt +++ /dev/null @@ -1,10 +0,0 @@ -package rules.bsi.tr_02102_1.v2019_01 - -/** - * BSI TR-02102-1 (Version 2019-01), 9. Zufallszahlengeneratoren - * - * Note: - * - requirement ID 9.2.1.01 fulfiled by Bouncy Castle implementation - * - requirement ID 9.2.2.01 does not apply to Bouncy Castle - * - requirement ID 9.5.1.01 and ID 9.5.2.1 not checked because it is internal to the implementation of Bouncy Castle - */ \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/RulesTR_Signature.mark b/src/test/resources/Mark/bouncycastle/RulesTR_Signature.mark deleted file mode 100644 index 3de1756..0000000 --- a/src/test/resources/Mark/bouncycastle/RulesTR_Signature.mark +++ /dev/null @@ -1,81 +0,0 @@ -package rules.bsi.tr_02102_1.v2019_01 - -/** - * BSI TR-02102-1 (Version 2019-01), 5.4.1. RSA und 5.4.2. Digital Signature Algorithm (DSA) und 5.4.3 DSA-Varianten basierend auf elliptischen Kurven - * - signature algorithms - * - * Note: - * - includes requirements ID 5.4.1.01 and ID 5.4.3.01 - */ -rule ID_5_4 { - using - Signature as s - ensure - s.algorithm in [ - "SHA256WITHRSAANDMGF1", "SHA512(256)WITHRSAANDMGF1", "SHA384WITHRSAANDMGF1", "SHA512WITHRSAANDMGF1", "SHA3-256WITHRSAANDMGF1", "SHA3-384WITHRSAANDMGF1", "SHA3-512WITHRSAANDMGF1", // RSA, EMSA-PSS - "SHA256WITHRSA/ISO9796-2", "SHA512(256)WITHRSA/ISO9796-2", "SHA384WITHRSA/ISO9796-2", "SHA512WITHRSA/ISO9796-2", // RSA, Digital Signature Scheme (DS) 2 und 3 - "SHA256WITHDSA", "SHA384WITHDSA", "SHA512WITHDSA", "SHA3-256WITHDSA", "SHA3-384WITHDSA", "SHA3-512WITHDSA", // DSA - "SHA256WITHECDSA", "SHA384WITHECDSA", "SHA512WITHECDSA", "SHA3-256WITHECDSA", "SHA3-384WITHECDSA", "SHA3-512WITHECDSA" // ECDSA - ] - onfail - InvalidSignatureAlgorithm -} - -/** - * BSI TR-02102-1 (Version 2019-01), 5.4.1. RSA - * - RSA modulus length of key in bits - * - * Note: - * - seems to be not checkable. We cannot sufficiently reason about the supplied RSA private key - */ -//rule ID_5_4_1_02 { -// using -// Signature as s, -// KeyFactory as kf, -// RSAPrivateKeySpec as rsaprivkey -// ensure -// // TODO valid until 2022 -// _is(s.privateKey, kf.prikey) -// && _is(kf.keyspec, rsaprivkey) -// && _bit_length(rsaprivkey.modulus) >= 2000 -// onfail -// InsufficientRSAKeyLength -//} - -/** - * BSI TR-02102-1 (Version 2019-01), 5.4.2. Digital Signature Algorithm (DSA) - * - DSA modulus length of key in bits - * - * Note: - * - seems to be not checkable. We cannot sufficiently reason about the supplied DSA private key - */ -//rule ID_5_4_2_01 { -// using -// Signature as s, -// KeyFactory as kf, -// DSAPrivateKeySpec as dsaprivkey -// ensure -// // TODO valid until 2022 -// _is(s.privateKey, kf.prikey) -// && _is(kf.keyspec, dsaprivkey) -// && _bit_length(dsaprivkey.modulus) >= 2000 -// onfail -// InsufficientDSAKeyLength -//} - -/** - * BSI TR-02102-1 (Version 2019-01), 5.4.3. DSA-Varianten basierend auf elliptischen Kurven - * - EC[K/G]DSA size of order q - * - * Note: - * - seems to be not checkable. We cannot sufficiently reason about the supplied EC private key - */ -//rule ID_5_4_3_02 { -// using -// Signature as s -// ensure -// // TODO valid until 2022 -// false -// onfail -// InsufficientECOrderQSize -//} diff --git a/src/test/resources/Mark/bouncycastle/Rules_BouncyCastleJCA.mark b/src/test/resources/Mark/bouncycastle/Rules_BouncyCastleJCA.mark deleted file mode 100644 index 607197c..0000000 --- a/src/test/resources/Mark/bouncycastle/Rules_BouncyCastleJCA.mark +++ /dev/null @@ -1,223 +0,0 @@ -package java.bcjca - -rule BouncyCastleProvider_AlgorithmParameterGenerator { - using - AlgorithmParameterGenerator as apg - ensure - _has_value(apg.provider) - && ( - apg.provider == "BC" - || _is_instance(apg.provider, "org.bouncycastle.jce.provider.BouncyCastleProvider") - ) - onfail - InvalidProvider_AlgorithmParameterGenerator -} - -rule BouncyCastleProvider_AlgorithmParameters { - using - AlgorithmParameters as ap - ensure - _has_value(ap.provider) - && ( - ap.provider == "BC" - || _is_instance(ap.provider, "org.bouncycastle.jce.provider.BouncyCastleProvider") - ) - onfail - InvalidProvider_AlgorithmParameters -} - -rule BouncyCastleProvider_CertificateFactory { - using - CertificateFactory as cf - ensure - _has_value(cf.provider) - && ( - cf.provider == "BC" - || _is_instance(cf.provider, "org.bouncycastle.jce.provider.BouncyCastleProvider") - ) - onfail - InvalidProvider_CertificateFactory -} - -rule BouncyCastleProvider_CertPathBuilder { - using - CertPathBuilder as cpb - ensure - _has_value(cpb.provider) - && ( - cpb.provider == "BC" - || _is_instance(cpb.provider, "org.bouncycastle.jce.provider.BouncyCastleProvider") - ) - onfail - InvalidProvider_CertPathBuilder -} - -rule BouncyCastleProvider_CertPathValidator { - using - CertPathValidator as cpv - ensure - _has_value(cpv.provider) - && ( - cpv.provider == "BC" - || _is_instance(cpv.provider, "org.bouncycastle.jce.provider.BouncyCastleProvider") - ) - onfail - InvalidProvider_CertPathValidator -} - -rule BouncyCastleProvider_CertStore { - using - CertStore as cs - ensure - _has_value(cs.provider) - && ( - cs.provider == "BC" - || _is_instance(cs.provider, "org.bouncycastle.jce.provider.BouncyCastleProvider") - ) - onfail - InvalidProvider_CertStore -} - -rule BouncyCastleProvider_Cipher { - using - Cipher as c - ensure - _has_value(c.provider) - && ( - c.provider == "BC" - || _is_instance(c.provider, "org.bouncycastle.jce.provider.BouncyCastleProvider") - ) - onfail - InvalidProvider_Cipher -} - -rule BouncyCastleProvider_KeyAgreement { - using - KeyAgreement as ka - ensure - _has_value(ka.provider) - && ( - ka.provider == "BC" - || _is_instance(ka.provider, "org.bouncycastle.jce.provider.BouncyCastleProvider") - ) - onfail - InvalidProvider_KeyAgreement -} - - -rule BouncyCastleProvider_KeyFactory { - using - KeyFactory as kf - ensure - _has_value(kf.provider) - && ( - kf.provider == "BC" - || _is_instance(kf.provider, "org.bouncycastle.jce.provider.BouncyCastleProvider") - ) - onfail - InvalidProvider_KeyFactory -} - -rule BouncyCastleProvider_KeyGenerator { - using - KeyGenerator as kg - ensure - _has_value(kg.provider) - && ( - kg.provider == "BC" - || _is_instance(kg.provider, "org.bouncycastle.jce.provider.BouncyCastleProvider") - ) - onfail - InvalidProvider_KeyGenerator -} - -rule BouncyCastleProvider_KeyPairGenerator { - using - KeyPairGenerator as kpg - ensure - _has_value(kpg.provider) - && ( - kpg.provider == "BC" - || _is_instance(kpg.provider, "org.bouncycastle.jce.provider.BouncyCastleProvider") - ) - onfail - InvalidProvider_KeyPairGenerator -} - -rule BouncyCastleProvider_KeyStore { - using - KeyStore as ks - ensure - _has_value(ks.provider) - && ( - ks.provider == "BC" - || _is_instance(ks.provider, "org.bouncycastle.jce.provider.BouncyCastleProvider") - ) - onfail - InvalidProvider_KeyStore -} - -rule BouncyCastleProvider_Mac { - using - Mac as m - ensure - _has_value(m.provider) - && ( - m.provider == "BC" - || _is_instance(m.provider, "org.bouncycastle.jce.provider.BouncyCastleProvider") - ) - onfail - InvalidProvider_Mac -} - -rule BouncyCastleProvider_MessageDigest { - using - MessageDigest as md - ensure - _has_value(md.provider) - && ( - md.provider == "BC" - || _is_instance(md.provider, "org.bouncycastle.jce.provider.BouncyCastleProvider") - ) - onfail - InvalidProvider_MessageDigest -} - -rule BouncyCastleProvider_SecretKeyFactory { - using - SecretKeyFactory as skf - ensure - _has_value(skf.provider) - && ( - skf.provider == "BC" - || _is_instance(skf.provider, "org.bouncycastle.jce.provider.BouncyCastleProvider") - ) - onfail - InvalidProvider_SecretKeyFactory -} - -rule BouncyCastleProvider_SecureRandom { - using - SecureRandom as sr - ensure - _has_value(sr.provider) - && ( - sr.provider == "BC" - || _is_instance(sr.provider, "org.bouncycastle.jce.provider.BouncyCastleProvider") - ) - onfail - InvalidProvider_SecureRandom -} - -rule BouncyCastleProvider_Signature { - using - Signature as s - ensure - _has_value(s.provider) - && ( - s.provider == "BC" - || _is_instance(s.provider, "org.bouncycastle.jce.provider.BouncyCastleProvider") - ) - onfail - InvalidProvider_Signature -} diff --git a/src/test/resources/Mark/bouncycastle/SecretKey.mark b/src/test/resources/Mark/bouncycastle/SecretKey.mark deleted file mode 100644 index 2a42d04..0000000 --- a/src/test/resources/Mark/bouncycastle/SecretKey.mark +++ /dev/null @@ -1,5 +0,0 @@ -package java.jca - -entity SecretKey { - -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/SecretKeyFactory.mark b/src/test/resources/Mark/bouncycastle/SecretKeyFactory.mark deleted file mode 100644 index 8703ea3..0000000 --- a/src/test/resources/Mark/bouncycastle/SecretKeyFactory.mark +++ /dev/null @@ -1,35 +0,0 @@ -package java.jca - -entity SecretKeyFactory { - - var algorithm; - var provider; - - var keyspec; - - var inkey; - var outkey; - - - op instantiate { - javax.crypto.SecretKeyFactory.getInstance( - algorithm : java.lang.String - ); - javax.crypto.SecretKeyFactory.getInstance( - algorithm : java.lang.String, - provider : java.lang.String | java.security.Provider - ); - } - - op generate { - outkey = javax.crypto.SecretKeyFactory.generateSecret( - keyspec: java.security.spec.KeySpec - ); - } - - op translate { - outkey = javax.crypto.SecretKeyFactory.translateKey( - inkey : javax.crypto.SecretKey - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/SecretKeySpec.mark b/src/test/resources/Mark/bouncycastle/SecretKeySpec.mark deleted file mode 100644 index fbcb922..0000000 --- a/src/test/resources/Mark/bouncycastle/SecretKeySpec.mark +++ /dev/null @@ -1,23 +0,0 @@ -package java.jca - -entity SecretKeySpec { - - var key; - var offset; - var len; - var algorithm; - - op instantiate { - javax.crypto.spec.SecretKeySpec( - key : byte[], - offset : int, - len : int, - algorithm : java.lang.String - ); - javax.crypto.spec.SecretKeySpec( - key : byte[], - algorithm : java.lang.String - ); - } - -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/Signature.mark b/src/test/resources/Mark/bouncycastle/Signature.mark deleted file mode 100644 index effa321..0000000 --- a/src/test/resources/Mark/bouncycastle/Signature.mark +++ /dev/null @@ -1,89 +0,0 @@ -package java.jca - -entity Signature { - - var algorithm; - var provider; - - var privateKey; - var random; - - var certificate; - var publicKey; - - var b; - var data; - var off; - var len; - - var outbuf; - var offset; - var len; - - var signature; - var offset; - var length; - - op instantiate { - java.security.Signature.getInstance( - algorithm : java.lang.String - ); - java.security.Signature.getInstance( - algorithm : java.lang.String, - provider : java.lang.String | java.security.Provider - ); - } - - op initsign { - java.security.Signature.initSign( - privateKey : java.security.PrivateKey - ); - java.security.Signature.initSign( - privateKey : java.security.PrivateKey, - random : java.security.SecureRandom - ); - } - - op initverify { - java.security.Signature.initVerify( - certificate : java.security.cert.Certificate - ); - java.security.Signature.initVerify( - publicKey : java.security.PublicKey - ); - } - - op update { - java.security.Signature.update( - b : byte - ); - java.security.Signature.update( - data : byte[] | java.nio.ByteBuffer - ); - java.security.Signature.update( - data : byte[], - off : int, - len : int - ); - } - - op sign { - signature = java.security.Signature.sign(); - java.security.Signature.sign( - outbuf : byte[], - offseet : int, - len : int - ); - } - - op verify { - java.security.Signature.verify( - signature : byte[] - ); - java.security.Signature.verify( - signature : byte[], - offset : int, - length : int - ); - } -} diff --git a/src/test/resources/Mark/bouncycastle/X509EncodedKeySpec.mark b/src/test/resources/Mark/bouncycastle/X509EncodedKeySpec.mark deleted file mode 100644 index 298ef70..0000000 --- a/src/test/resources/Mark/bouncycastle/X509EncodedKeySpec.mark +++ /dev/null @@ -1,18 +0,0 @@ -package java.jca - -entity X509EncodedKeySpec { - - var encodedKey; - var algorithm; - - - op instantiate { - java.security.spec.X509EncodedKeySpec( - encodedKey : byte[] - ); - java.security.spec.X509EncodedKeySpec( - encodedKey : byte[], - algorithm : java.lang.String - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/XECPrivateKeySpec.mark b/src/test/resources/Mark/bouncycastle/XECPrivateKeySpec.mark deleted file mode 100644 index d093427..0000000 --- a/src/test/resources/Mark/bouncycastle/XECPrivateKeySpec.mark +++ /dev/null @@ -1,14 +0,0 @@ -package java.jca - -entity XECPrivateKeySpec { - - var params; - var scalar; - - op instantiate { - java.security.spec.XECPrivateKeySpec( - params : java.security.spec.AlgorithmParameterSpec, - scalar : byte[] - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/XECPublicKeySpec.mark b/src/test/resources/Mark/bouncycastle/XECPublicKeySpec.mark deleted file mode 100644 index e228310..0000000 --- a/src/test/resources/Mark/bouncycastle/XECPublicKeySpec.mark +++ /dev/null @@ -1,14 +0,0 @@ -package java.jca - -entity XECPublicKeySpec { - - var params; - var u; - - op instantiate { - java.security.spec.XECPublicKeySpec( - params : java.security.spec.AlgorithmParameterSpec, - u : java.math.BigInteger - ); - } -} \ No newline at end of file diff --git a/src/test/resources/Mark/exampleMapping.yaml b/src/test/resources/Mark/exampleMapping.yaml new file mode 100644 index 0000000..7358b83 --- /dev/null +++ b/src/test/resources/Mark/exampleMapping.yaml @@ -0,0 +1,20 @@ +metrics: + - name: "TestMetric1" + rules: + - "WrongUseOfBotan_CipherMode" + configuration: + default: false + operator: "=" + type: NUMBER + target: + - "1.23" + - "3.14" + - name: "TestMetric2" + rules: + - "VariableNotInitialized" + configuration: + default: false + operator: "=" + type: BOOLEAN + target: + - "true" \ No newline at end of file diff --git a/src/test/resources/Mark/jackson/ObjectMapper.mark b/src/test/resources/Mark/jackson/ObjectMapper.mark deleted file mode 100644 index dad5ede..0000000 --- a/src/test/resources/Mark/jackson/ObjectMapper.mark +++ /dev/null @@ -1,12 +0,0 @@ -entity ObjectMapper { - op instantiate { - com.fasterxml.jackson.databind.ObjectMapper(); - } - - /** - * Default typing has lead to various problems with Jackson in the past and should be avoided. - */ - op enableDefaultTyping { - forbidden com.fasterxml.jackson.databind.ObjectMapper.enableDefaultTyping(); - } -} \ No newline at end of file diff --git a/src/test/resources/codyze.yaml b/src/test/resources/codyze.yaml index 66a79ac..2a2abb4 100644 --- a/src/test/resources/codyze.yaml +++ b/src/test/resources/codyze.yaml @@ -7,12 +7,16 @@ orchestrator: username: "clouditor" password: "clouditor" -source: "src/test/resources/exampleFiles/TLSServer/" +id: "default" +source: + - exampleFiles/2_1_2_1_02.cpp output: "codyze.sarif" -sarif: true +rules: mark/medina.yaml + +mark: + builtin: + - bc-jsse/ codyze: - mark: - - "src/test/resources/mark/demo/" no-good-findings: false pedantic: false diff --git a/src/test/resources/demos/bcjsse/TlsServer.java b/src/test/resources/demos/bcjsse/TlsServer.java deleted file mode 100644 index f71cad4..0000000 --- a/src/test/resources/demos/bcjsse/TlsServer.java +++ /dev/null @@ -1,146 +0,0 @@ -package de.fraunhofer.aisec.codyze.medina.demo.jsse; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; - -import javax.net.ssl.*; -import java.io.*; -import java.net.Socket; -import java.nio.file.Paths; -import java.security.*; -import java.security.cert.CertificateException; -import java.util.Arrays; - -public class TlsServer { - - private static final boolean DEBUG = true; - - private SSLServerSocket socket; - - private void configure(int port, String keystore, String keystorePwd) throws IOException, NoSuchAlgorithmException, KeyStoreException, CertificateException, UnrecoverableKeyException, KeyManagementException, NoSuchProviderException { - // overview of functionality provided by BCJSSE - if (DEBUG) { - System.out.println("Services provided by BCJSSE security Provider"); - - for (Provider.Service s : Security.getProvider("BCJSSE").getServices()) { - System.out.println(s); - } - System.out.println(); - } - - // get default from most prioritized securtiy provider -> BCJSSE - SSLContext sslCtx = SSLContext.getInstance("TLS", "BCJSSE"); - - // initialize sslContext with a KeyManager and no TrustManager - KeyStore ks = KeyStore.getInstance("PKCS12"); - ks.load(new FileInputStream(keystore), keystorePwd.toCharArray()); - KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); - kmf.init(ks, keystorePwd.toCharArray()); - sslCtx.init(kmf.getKeyManagers(), null, null); - - // verify correct selection - if(DEBUG) { - System.out.println("Current provider:"); - System.out.println(sslCtx.getProvider()); - System.out.println(); - - System.out.println("Selected protocol:"); - System.out.println(sslCtx.getProtocol()); - System.out.println(); - - SSLParameters sslParams = sslCtx.getSupportedSSLParameters(); - - System.out.println("Protocols:"); - Arrays.stream(sslParams.getProtocols()).forEach(System.out::println); - System.out.println(); - - System.out.println("Use cipher suites order: "); - System.out.println(sslParams.getUseCipherSuitesOrder()); - System.out.println(); - - System.out.println("Cipher suites:"); - Arrays.stream(sslParams.getCipherSuites()).forEach(System.out::println); - System.out.println(); - } - - // create the SSLServerSocket - SSLServerSocketFactory socketFactory = sslCtx.getServerSocketFactory(); - socket = (SSLServerSocket) socketFactory.createServerSocket(port); - - // set protocol versions and cipher suites - socket.setEnabledProtocols(new String[]{"TLSv1.1", "TLSv1.2"}); - socket.setEnabledCipherSuites(new String[]{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_AES_128_CCM_SHA256", "TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384"}); - - if (DEBUG) { - System.out.println("Enabled by the Socket:"); - - System.out.println("Protocols:"); - Arrays.stream(socket.getEnabledProtocols()).forEach(System.out::println); - System.out.println(); - - System.out.println("Cipher suites:"); - Arrays.stream(socket.getEnabledCipherSuites()).forEach(System.out::println); - System.out.println(); - } - } - - private void start() { - while(true) { - try (Socket sock = socket.accept()) { - BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream())); - BufferedWriter out = new BufferedWriter(new OutputStreamWriter(sock.getOutputStream())); - while (in.readLine() != null) { - out.write("ack"); - out.flush(); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - public static void main(String[] args) { - // ensure Bouncy Castle is first provider queried when retrieving implementations - Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); - Security.insertProviderAt(new BouncyCastleProvider(), 2); - - if (DEBUG) { - System.out.println("Registered security providers:"); - - Provider[] ps = Security.getProviders(); - for (int i = 0; i < ps.length; i++) { - System.out.println(i + " : " + ps[i]); - } - System.out.println(); - } - - // try to get the path of the needed Keystore: - File jks = null; - // 1: absolute path via program argument - if (args.length != 0) { - jks = new File(args[0]); - } - // 2: absolute path via environment variable - if (args.length == 0 || !jks.exists()) { - String env = System.getenv("KEYSTORE_PATH"); - if (env != null) - jks = new File(env); - //3: load via class loader - if (env == null || !jks.exists()) { - // Throws a NPE if Keystore could not be found up until this point - jks = new File(TlsServer.class.getClassLoader().getResource("keystore.jks").getPath()); - } - } - - // create TLS server - TlsServer server = new TlsServer(); - try { - // the keystore is expected to be generated with the script in the resource folder - server.configure(2200, jks.getAbsolutePath(), "demo-password"); - } catch (Exception e) { - e.getLocalizedMessage(); - System.exit(1); - } - server.start(); - } -} diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml deleted file mode 100644 index a7748a1..0000000 --- a/src/test/resources/log4j2.xml +++ /dev/null @@ -1,19 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<configuration> - <appenders> - <console name="stdout" target="SYSTEM_OUT"> - <patternLayout pattern="%d{ABSOLUTE} %5p %c{1}:%L - %m%n"/> - </console> - - <file name="fileout" fileName="medina-codyze.log"> - <patternLayout pattern="%d{ABSOLUTE} %5p %c{1}:%L - %m%n"/> - </file> - </appenders> - - <loggers> - <Logger level="ALL" name="de.fraunhofer.aisec.codyze"/> - <root level="TRACE"> - <appenderRef ref="stdout"/> - </root> - </loggers> -</configuration> \ No newline at end of file diff --git a/src/test/resources/mappingTreeTestStructure/botan/bt1/block_ciphers.mark b/src/test/resources/mappingTreeTestStructure/botan/bt1/block_ciphers.mark new file mode 100644 index 0000000..117cc71 --- /dev/null +++ b/src/test/resources/mappingTreeTestStructure/botan/bt1/block_ciphers.mark @@ -0,0 +1,20 @@ +package botan + +/* + * From Botan Handbook: + * "In general a bare block cipher is not what you should be using. You probably want a cipher mode instead (see Cipher Modes)" + */ + +entity Botan.Forbidden.BlockCipher { + op forbid { + forbidden Botan::get_block_cipher(); + forbidden Botan::get_block_cipher(...); + forbidden Botan::get_block_cipher_providers(); + forbidden Botan::get_block_cipher_providers(...); + + forbidden Botan::BlockCipher::create(); + forbidden Botan::BlockCipher::create(...); + forbidden Botan::BlockCipher::create_or_throw(); + forbidden Botan::BlockCipher::create_or_throw(...); + } +} \ No newline at end of file diff --git a/src/test/resources/mappingTreeTestStructure/botan/bt1/mapping.yaml b/src/test/resources/mappingTreeTestStructure/botan/bt1/mapping.yaml new file mode 100644 index 0000000..de12ff7 --- /dev/null +++ b/src/test/resources/mappingTreeTestStructure/botan/bt1/mapping.yaml @@ -0,0 +1,11 @@ +metrics: + - name: "bt1" + rules: + - "block_ciphers" + configuration: + default: false + operator: "=" + type: NUMBER + target: + - "1.23" + - "3.14" \ No newline at end of file diff --git a/src/test/resources/mappingTreeTestStructure/botan/bt2/hash.mark b/src/test/resources/mappingTreeTestStructure/botan/bt2/hash.mark new file mode 100644 index 0000000..f987874 --- /dev/null +++ b/src/test/resources/mappingTreeTestStructure/botan/bt2/hash.mark @@ -0,0 +1,69 @@ +package botan + +entity Botan.HashFunction { + var alg; + var data; + var len; + var hash_output; + + op create { + Botan::HashFunction::create(alg); + Botan::HashFunction::create(alg, _); + Botan::HashFunction::create_or_throw(alg); + Botan::HashFunction::create_or_throw(alg, _); + } + + op update { + Botan::HashFunction::update(data); + Botan::HashFunction::update(data: uint8_t[], length); + } + + op finalize { + hash_output = Botan::HashFunction::final(); + hash_output = Botan::HashFunction::final_stdvec(); + Botan::HashFunction::final(hash_output); + } + + op process { + hash_output = Botan::HashFunction::process(data); + hash_output = Botan::HashFunction::process(data: uint8_t[], length); + } +} + +rule HashOrder { + using Botan.HashFunction as hf + ensure order + hf.create(), + ( + (hf.update()*, hf.finalize()) + | hf.process() + ) + onfail HashOrder + +} + + + + /* + * For use of hashes in Filter/Pipes + * correct order is defined in pipe.mark + */ + entity Botan.Hash_Filter { + var hash_function: Botan.HashFunction; + var request; + + op create { + Botan::Hash_Filter(request: std::string, len); + Botan::Hash_Filter(request: std::string, len); + Botan::Hash_Filter(hash_function: Botan::HashFunction); + Botan::Hash_Filter(hash_function: Botan::HashFunction, len); + } + } + + rule _4_01_HashFilter { + using Botan.Hash_Filter as hf + when _has_value(hf.request) + ensure hf.request in ["SHA-256", "SHA-512-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384", "SHA3-512"] + onfail _01_HashFilter +} + \ No newline at end of file diff --git a/src/test/resources/mappingTreeTestStructure/botan/bt2/mapping.yaml b/src/test/resources/mappingTreeTestStructure/botan/bt2/mapping.yaml new file mode 100644 index 0000000..bda4ddf --- /dev/null +++ b/src/test/resources/mappingTreeTestStructure/botan/bt2/mapping.yaml @@ -0,0 +1,11 @@ +metrics: + - name: "bt2" + rules: + - "hash" + configuration: + default: false + operator: "=" + type: NUMBER + target: + - "1.23" + - "3.14" \ No newline at end of file diff --git a/src/test/resources/mappingTreeTestStructure/botan/cipher_mode.mark b/src/test/resources/mappingTreeTestStructure/botan/cipher_mode.mark new file mode 100644 index 0000000..b1b29a1 --- /dev/null +++ b/src/test/resources/mappingTreeTestStructure/botan/cipher_mode.mark @@ -0,0 +1,107 @@ +package botan + +entity Botan.Cipher_Mode { + + var algorithm; + var symkey : Botan.SymmetricKey; + var iv : Botan.InitializationVector; + var iv_length; + var direction; + + var input; + var input_length; + + var inout; + + var aead_data; + var aead_data_len; + + /* + Note: allows creating objects of Type Botan::Keyed_Filter and Botan::Cipher_Mode, therefore all other ops should consider member functions of these two classes + + Botan::OctetString might be allowed for key and IV, but we will not accept it because Botan::SymmetricKey and Botan::InitializationVector carry more semantics, which should increase safety and maintainability. + We allow secure vector because real world examples seem to use it + */ + + /* Note: there is also the possibility to create a cipher from ECIES_System_Params. Maybe forbid that? */ + + op create_uninit { + Botan::get_cipher_mode(algorithm, direction); + Botan::get_cipher_mode(algorithm, direction, ...); + Botan::get_cipher(algorithm, direction); + } + + op create_key_init { + Botan::get_cipher( + algorithm, + iv: Botan::InitializationVector, + direction + ); + } + + op create_key_iv_init { + Botan::get_cipher( + algorithm, + symkey: Botan::SymmetricKey, + iv: Botan::InitializationVector, + direction + ); + } + + op set_key { + Botan::Cipher_Mode::set_key(symkey: Botan::SymmetricKey | Botan::secure_vector<uint8_t>); + forbidden Botan::Cipher_Mode::set_key(_, _); + Botan::Keyed_Filter::set_key(symkey: Botan::SymmetricKey | Botan::secure_vector<uint8_t>); + } + + op set_iv { + Botan::Keyed_Filter::set_iv(iv: Botan::InitializationVector); + } + + op start_no_iv { + Botan::Cipher_Mode::start(); + Botan::Keyed_Filter::start_msg(); + } + + op start_iv { + Botan::Cipher_Mode::start(iv); + Botan::Cipher_Mode::start(iv, iv_length); + Botan::Cipher_Mode::start_msg(iv, iv_length); + } + + op process { + Botan::Cipher_Mode::process(input, input_length); + Botan::Cipher_Mode::update(inout); + Botan::Cipher_Mode::update(inout, _); + + Botan::Keyed_Filter::write(input, input_length); + } + + op finish { + Botan::Cipher_Mode::finish(inout); + Botan::Cipher_Mode::finish(inout, _); + + Botan::Keyed_Filter::end_msg(); + } + + op reset { + Botan::Cipher_Mode::reset(); + } + + op assoc_data { + Botan::AEAD_Filter::set_associated_data(aead_data, aead_data_len); + } + +} + + +rule Cipher_Mode_Order { + using Botan.Cipher_Mode as cm + ensure order + ((cm.create_uninit(), cm.set_key()) | cm.create_key_init()), // key is set here + ((cm.set_iv(), cm.start_no_iv()) | cm.start_iv()), + cm.assoc_data()*, + cm.process()*, + cm.finish() + onfail Cipher_Mode_Order +} \ No newline at end of file diff --git a/src/test/resources/mappingTreeTestStructure/botan/mapping.yaml b/src/test/resources/mappingTreeTestStructure/botan/mapping.yaml new file mode 100644 index 0000000..d86e377 --- /dev/null +++ b/src/test/resources/mappingTreeTestStructure/botan/mapping.yaml @@ -0,0 +1,13 @@ +metrics: + - name: "botan" + rules: + - "hash" + - "block_ciphers" + - "cipher_mode" + configuration: + default: false + operator: "=" + type: NUMBER + target: + - "1.23" + - "3.14" \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/BouncyCastleProvider.mark b/src/test/resources/mappingTreeTestStructure/bouncycastle/bc1/BouncyCastleProvider.mark similarity index 100% rename from src/test/resources/Mark/bouncycastle/BouncyCastleProvider.mark rename to src/test/resources/mappingTreeTestStructure/bouncycastle/bc1/BouncyCastleProvider.mark diff --git a/src/test/resources/mappingTreeTestStructure/bouncycastle/bc1/mapping.yaml b/src/test/resources/mappingTreeTestStructure/bouncycastle/bc1/mapping.yaml new file mode 100644 index 0000000..d54470d --- /dev/null +++ b/src/test/resources/mappingTreeTestStructure/bouncycastle/bc1/mapping.yaml @@ -0,0 +1,11 @@ +metrics: + - name: "bc1" + rules: + - "BouncyCastleProvider" + configuration: + default: false + operator: "=" + type: NUMBER + target: + - "1.23" + - "3.14" \ No newline at end of file diff --git a/src/test/resources/Mark/bouncycastle/Cipher.mark b/src/test/resources/mappingTreeTestStructure/bouncycastle/bc2/Cipher.mark similarity index 100% rename from src/test/resources/Mark/bouncycastle/Cipher.mark rename to src/test/resources/mappingTreeTestStructure/bouncycastle/bc2/Cipher.mark diff --git a/src/test/resources/mappingTreeTestStructure/bouncycastle/bc2/mapping.yaml b/src/test/resources/mappingTreeTestStructure/bouncycastle/bc2/mapping.yaml new file mode 100644 index 0000000..da14598 --- /dev/null +++ b/src/test/resources/mappingTreeTestStructure/bouncycastle/bc2/mapping.yaml @@ -0,0 +1,11 @@ +metrics: + - name: "bc2" + rules: + - "Cipher" + configuration: + default: false + operator: "=" + type: NUMBER + target: + - "1.23" + - "3.14" \ No newline at end of file diff --git a/templates/codyze-medina-metrics.yaml b/templates/codyze-medina-metrics.yaml new file mode 100644 index 0000000..cd2d6cd --- /dev/null +++ b/templates/codyze-medina-metrics.yaml @@ -0,0 +1,16 @@ +metrics: + - name: "CodeSignoff" + target: + - "<Name>" + - "<Name>" + - name: "SignedCommits" + target: + - "<16-Digit Key-Id>" + - name: "SignedSignoff" + target: + - name: "<Name>" + email: "<E-Mail>" + pub-key-id: "<16-Digit Base-64 Signing-Key-Id>" + - name: "ApprovedCommitAuthor" + target: + - "<Name>" diff --git a/templates/codyze-medina.yaml b/templates/codyze-medina.yaml new file mode 100644 index 0000000..3051555 --- /dev/null +++ b/templates/codyze-medina.yaml @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=schema/codyze-config-schema.json +# For more explanation regarding the parameters, please refer to the README. + +orchestrator: + required: <BOOLEAN> # Optional value, defaults to true + endpoint: "<URL>" # Required value + auth: + oauth-endpoint: "<URL>" # Required value + username: "<STRING>" # Required value + password: "<STRING>" # Required value, can also be injected via program arguments or environment variables + +id: "<UUID>" # Required value +source: # Required value + - <FILEPATH> + - ... + +ci: "<NONE | GITLAB | GITHUB | JENKINS>" # Optional value, automatically inferred by default +rules: <FILEPATH> # Optional value, defaults to codyze-medina-metrics.yaml +medina-output: <FILEPATH> # Optional value, defaults to codyze-medina.sarif +combined-output: <BOOLEAN> # Optional value, defaults to true +key-location: <PATH> # Optional value, defaults to public-keys/ + +mark: # While technically optional, MARK rules are required to generate analysis results + builtin: # Refers to MARK modules included in the "mark/" directory of the release package + - <MODULE_NAME> + - ... + project: # Refers to additional MARK modules in other places + - <PATH> + - ... + +# All additional parameters will be directly passed to the underlying Codyze implementation. +# Core Codyze parameters are documented at https://www.codyze.io/ -- GitLab