diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..13566b81b --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..6ed36dd36 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..35eb1ddfb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.kubernetes/create_aks.ps1 b/.kubernetes/create_aks.ps1 new file mode 100644 index 000000000..a3b3f8e8e --- /dev/null +++ b/.kubernetes/create_aks.ps1 @@ -0,0 +1,33 @@ +# Login to Azure (Uncomment the next line if you need to login or switch accounts) +# az login + +# Set Variables +$resourceGroupName = "devsecops" +$location = "northeurope" +$aksClusterName = "Devsecops-aks" +$aksVersion = "1.29.0" + +# Create Resource Group if it doesn't exist +$resourceGroupExists = az group exists --name $resourceGroupName +if (-not $resourceGroupExists) { + Write-Output "Creating resource group '$resourceGroupName' in location '$location'." + az group create --name $resourceGroupName --location $location +} else { + Write-Output "Resource group '$resourceGroupName' already exists." +} + +# Create AKS Cluster +Write-Output "Creating AKS cluster named '$aksClusterName' in resource group '$resourceGroupName'." +az aks create ` + --resource-group $resourceGroupName ` + --name $aksClusterName ` + --node-count 1 ` + --enable-addons monitoring ` + --kubernetes-version $aksVersion ` + --generate-ssh-keys ` + --location $location ` + --node-vm-size Standard_B2s ` + --load-balancer-sku basic ` + --no-wait + +Write-Output "AKS cluster creation command has been executed. It may take a few minutes for the cluster to be fully operational." diff --git a/.pydevproject b/.pydevproject new file mode 100644 index 000000000..97606ffb5 --- /dev/null +++ b/.pydevproject @@ -0,0 +1,8 @@ + + + Default + python interpreter + + /${PROJECT_DIR_NAME}/devops-spring-project + + diff --git a/.talismanrc b/.talismanrc new file mode 100644 index 000000000..2fda1b123 --- /dev/null +++ b/.talismanrc @@ -0,0 +1,4 @@ +fileignoreconfig: +- filename: Jenkinsfile + checksum: a4c9cd41b0402afd7b2324cc30a6628cbd0aade31a0d852ef2664b297e2bc117 +version: "" \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..e01206508 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "java.compile.nullAnalysis.mode": "automatic", + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 6eea1d93c..f0551cc89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ FROM openjdk:8-jdk-alpine EXPOSE 8080 ARG JAR_FILE=target/*.jar -ADD ${JAR_FILE} app.jar -ENTRYPOINT ["java","-jar","/app.jar"] \ No newline at end of file +RUN addgroup -S pipeline && adduser -S k8s-pipeline -G pipeline +COPY ${JAR_FILE} /home/k8s-pipeline/app.jar +USER k8s-pipeline +ENTRYPOINT ["java","-jar","/home/k8s-pipeline/app.jar"] \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index 56745c21a..ecf314a41 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,12 +1,194 @@ pipeline { - agent any + agent any - stages { - stage('Build Artifact') { + environment { + // Set JVM options for Maven + deploymentName = "devsecops" + containerName = "devsecops-container" + serviceName = "devsecops-svc" + imageName = "manlikeabz/numeric-app:${GIT_COMMIT}" + applicationURL = "http://devsecops-abzconsultancies.eastus.cloudapp.azure.com/" + applicationURI = "/increment/99" + MAVEN_OPTS = "--add-opens java.base/java.lang=ALL-UNNAMED" + AKS_CLUSTER_NAME = 'Devsecops-aks' + NAMESPACE = 'default' + } + + stages { + stage('Build Artifact') { + steps { + // Use environment variable for Maven options + sh 'mvn clean package -DskipTests=true' + archiveArtifacts artifacts: 'target/*.jar', onlyIfSuccessful: true + } + } + + stage('Unit Tests - JUnit and Jacoco') { + steps { + sh "mvn test" + } + post { + always { + junit 'target/surefire-reports/*.xml' + jacoco execPattern: 'target/jacoco.exec' + } + } + } + + stage('Mutation Tests - PIT') { + steps { + sh 'mvn org.pitest:pitest-maven:mutationCoverage' + } + post { + always { + pitmutation mutationStatsFile: '**/pit-reports/**/mutations.xml' + } + } + } + + stage('SonarQube - SAST') { + steps { + withSonarQubeEnv('SonarQube') { + sh "mvn sonar:sonar -Dsonar.projectKey=numeric-application -Dsonar.host.url=http://devsecops-abzconsultancies.eastus.cloudapp.azure.com:9000 -Dsonar.token=squ_b0ffa602f401442384e12c06cbd73a66b51a7d2a" + // Make sure the SonarQube scanner has finished before proceeding + } + script { + // It will wait indefinitely for the SonarQube analysis to complete + def qg = waitForQualityGate() + if (qg.status != 'OK') { + error "Quality gate not passed: ${qg.status}" + } + } + } + } + + stage('Vulnerability Scan - Docker') { + parallel { + stage('Maven Dependency Check') { + steps { + script { + sh "mvn dependency-check:check" + } + } + post { + always { + dependencyCheckPublisher pattern: 'target/dependency-check-report.xml' + } + } + } + stage('Trivy Scan') { + steps { + script { + sh "bash trivy-docker-image-scan.sh" + //trivy image --exit-code 0 --severity HIGH,CRITICAL manlikeabz/numeric-app:${GIT_COMMIT} + } + } + } + stage('OPA Conftest') { + steps { + script { + // Run OPA Conftest against the Dockerfile + sh 'docker run --rm -v $(pwd):/project openpolicyagent/conftest test --policy opa-docker-security.rego Dockerfile' + } + } + } + } + } + + stage('Docker Build and Push') { + steps { + script { + // Ensure GIT_COMMIT is populated + GIT_COMMIT = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() + echo "Building and pushing Docker image for commit: ${GIT_COMMIT}" + + // Docker login using credentials securely + withDockerRegistry([credentialsId: "90cf476e-ad01-40fe-86fa-4b0599ac41ff", url: ""]) { + sh "printenv" + + // Docker build and push commands + sh "docker build -t manlikeabz/numeric-app:${GIT_COMMIT} ." + sh "docker push manlikeabz/numeric-app:${GIT_COMMIT}" + } + } + } + } + + // stage('Vulneability Scan - Kubernetes') { + // steps { + // script { + // // Run OPA Conftest scan against the Kubernetes deployment file + // sh "docker run --rm -v $pwd:/project openpolicyagent/conftest test --policy opa-k8s-security.rego k8s_deployment_service.yaml" + // } + // } + // } + + stage('Vulnerability Scan - Kubernetes') { steps { - sh "mvn clean package -DskipTests=true" - archive 'target/*.jar' //so that they can be downloaded later + parallel( + "OPA Scan": { + script { + // Run OPA Conftest scan against the Kubernetes deployment file + sh "docker run --rm -v $(pwd):/project openpolicyagent/conftest test --policy opa-k8s-security.rego k8s_deployment_service.yaml" + } + }, + "Kubesec Scan": { + sh "bsh kubesec-scan.sh" + } + } + ) } - } + } + + // stage('Kubernetes Deployment - DEV') { + // steps { + // withKubeConfig([credentialsId: 'kubeconfig']) { + // sh "kubectl config use-context ${AKS_CLUSTER_NAME}" + // sh "sed -i 's#replace#manlikeabz/numeric-app:${GIT_COMMIT}#g' k8s_deployment_service.yaml" + // sh "kubectl apply -f k8s_deployment_service.yaml" + // } + // } + // } + + stage('Integration Tests - DEV') { + steps { + script { + try { + withKubeConfig([credentialsId: 'kubeconfig']) { + sh "bash integration-tests.sh" + } + } catch (Exception e) { + withKubeConfig([credentialsId: 'kubeconfig']) { + sh "kubectl rollout undo deployment ${deploymentName}" + } + throw e + } + } + } + + stage('K8s Deployment - Dev'){ + steps{ + parallel( + "Deploynment": { + withKubeConfig([credentialsId: 'kubeconfig']) { + sh "bash k8s-deployment.sh" + } + }, + "Rollout Status": { + withKubeConfig([credentialsId: 'kubeconfig']) { + sh "bash k8s-deployment-rollout-status.sh" + } + } + ) + } + } + } + + post { + always { + // Cleanup after Docker to avoid logged in credentials hanging around + sh "docker logout" + echo 'Pipeline execution complete.' + } } -} \ No newline at end of file +} diff --git a/clusterrolebinding.yaml b/clusterrolebinding.yaml new file mode 100644 index 000000000..700906d77 --- /dev/null +++ b/clusterrolebinding.yaml @@ -0,0 +1,14 @@ +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: deployment-manager-binding + namespace: default +subjects: +- kind: ServiceAccount + name: jenkins-deployer + namespace: default +roleRef: + kind: Role + name: deployment-manager + apiGroup: rbac.authorization.k8s.io + diff --git a/integration-test.sh b/integration-test.sh new file mode 100644 index 000000000..4278b8819 --- /dev/null +++ b/integration-test.sh @@ -0,0 +1,33 @@ +#!/bin/bash +#integration-test.sh + +sleep 5s + +PORT=$(kubectl -n default get svc ${serviceName} -o json | jq .spec.ports[].nodePort) +echo $PORT +echo $applicationURL:$PORT/$applicationURI + +if [[ ! -z "$PORT" ]] +then + response=$(curl -s $applicationURL:$PORT$applicationURI) + http_code=$(curl -s -o /dev/null -w "%{http_code}" $applicationURL:$PORT$applicationURI) + + if [[ "$response" == 100 ]]; + then + echo "Increment Test Passed" + else + echo "Increment Test Failed" + exit 1; + fi; + + if [[ "$http_code" == 200 ]]; + then + echo "HTTP Status Code Test Passed" + else + echo "HTTP Status code is not 200" + exit 1; + fi; +else + echo "The Service does not have a NodePort" + exit 1; +fi; diff --git a/k8s-deployment-rollout-status.sh b/k8s-deployment-rollout-status.sh new file mode 100644 index 000000000..f3fa22843 --- /dev/null +++ b/k8s-deployment-rollout-status.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +sleep 60s + +if [[ $(kubectl -n default rollout status deployment/${deploymentName} --timeout 5s) != *"successfully rolled out"* ]]; +then + echo "Deployment ${deploymentName} rolled out has failed" + kubectl rollout undo deployment/${deploymentName} -n default +else + echo "Deployment ${deploymentName} rollout success" +fi \ No newline at end of file diff --git a/k8s-deployment.sh b/k8s-deployment.sh new file mode 100644 index 000000000..88a62e0ef --- /dev/null +++ b/k8s-deployment.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +sed -i "s#replace#${imageName}#g" k8s_deployment_service.yaml +# kubectl -n default get deployment ${deploymentName} &> /dev/null + +# if [[ $? -ne 0]]; then +# echo "deployment ${deploymentName} not found, creating new deployment" +# kubectl apply -f k8s_deployment_service.yaml -n default +# else +# echo "deployment ${deploymentName} found, updating deployment" +# echo "image name - ${imageName}" +# kubectl set image deployment/${deploymentName} ${containerName}=${imageName} -n default +# fi + +kubectl apply -f k8s_deployment_service.yaml -n default \ No newline at end of file diff --git a/k8s_deployment_service.yaml b/k8s_deployment_service.yaml index 574742d32..ee1b717fd 100644 --- a/k8s_deployment_service.yaml +++ b/k8s_deployment_service.yaml @@ -15,9 +15,27 @@ spec: labels: app: devsecops spec: + volumes: + - name: vol + emptyDir: {} + serviceAccount: default containers: - image: replace name: devsecops-container + volumeMounts: + - name: vol + mountPath: /tmp + securityContext: + runAsNonRoot: true + runAsUser: 100 + readOnlyRootFilesystem: true + resources: + limits: + cpu: "1" + memory: "1Gi" + requests: + cpu: "100m" + memory: "256Mi" --- apiVersion: v1 kind: Service diff --git a/kubesec-scan.sh b/kubesec-scan.sh new file mode 100644 index 000000000..5d78d777a --- /dev/null +++ b/kubesec-scan.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# using kubesec v2 api +scan_result=$(curl -sSX POST --data-binary @k8s_deployment_service.yaml https://kubesec.io/api/v1/scan) +scan_message=$(curl -sSX POST --data-binary @k8s_deployment_service.yaml https://kubesec.io/api/v1/scan | jq -r '.data[].message') +scan_score=$(curl -sSX POST --data-binary @k8s_deployment_service.yaml https://kubesec.io/api/v1/scan | jq -r '.data[].score') + +if [[ $scan_score -ge 9 ]]; then + echo "Score is $scan_score" + echo "Kubesec scan message: $scan_message" + exit 1 +else + echo "Score is $scan_score, which is less than or equal to 5" + echo "Scanning Kubernetes Resource has failed" + exit 1; +fi \ No newline at end of file diff --git a/opa-docker-security.rego b/opa-docker-security.rego new file mode 100644 index 000000000..aa34c2c8e --- /dev/null +++ b/opa-docker-security.rego @@ -0,0 +1,107 @@ +package main + +# Do Not store secrets in ENV variables +secrets_env = [ + "passwd", + "password", + "pass", + "secret", + "key", + "access", + "api_key", + "apikey", + "token", + "tkn" +] + +deny[msg] { + input[i].Cmd == "env" + val := input[i].Value + contains(lower(val[_]), secrets_env[_]) + msg = sprintf("Line %d: Potential secret in ENV key found: %s", [i, val]) +} + +# Only use trusted base images +deny[msg] { + input[i].Cmd == "from" + val := split(input[i].Value[0], "/") + count(val) > 1 + msg = sprintf("Line %d: use a trusted base image", [i]) +} + +# Do not use 'latest' tag for base imagedeny[msg] { +deny[msg] { + input[i].Cmd == "from" + val := split(input[i].Value[0], ":") + contains(lower(val[1]), "latest") + msg = sprintf("Line %d: do not use 'latest' tag for base images", [i]) +} + +# Avoid curl bashing +deny[msg] { + input[i].Cmd == "run" + val := concat(" ", input[i].Value) + matches := regex.find_n("(curl|wget)[^|^>]*[|>]", lower(val), -1) + count(matches) > 0 + msg = sprintf("Line %d: Avoid curl bashing", [i]) +} + +# Do not upgrade your system packages +warn[msg] { + input[i].Cmd == "run" + val := concat(" ", input[i].Value) + matches := regex.match(".*?(apk|yum|dnf|apt|pip).+?(install|[dist-|check-|group]?up[grade|date]).*", lower(val)) + matches == true + msg = sprintf("Line: %d: Do not upgrade your system packages: %s", [i, val]) +} + +# Do not use ADD if possible +deny[msg] { + input[i].Cmd == "add" + msg = sprintf("Line %d: Use COPY instead of ADD", [i]) +} + +# Any user... +any_user { + input[i].Cmd == "user" + } + +deny[msg] { + not any_user + msg = "Do not run as root, use USER instead" +} + +# ... but do not root +forbidden_users = [ + "root", + "toor", + "0" +] + +deny[msg] { + command := "user" + users := [name | input[i].Cmd == "user"; name := input[i].Value] + lastuser := users[count(users)-1] + contains(lower(lastuser[_]), forbidden_users[_]) + msg = sprintf("Line %d: Last USER directive (USER %s) is forbidden", [i, lastuser]) +} + +# Do not sudo +deny[msg] { + input[i].Cmd == "run" + val := concat(" ", input[i].Value) + contains(lower(val), "sudo") + msg = sprintf("Line %d: Do not use 'sudo' command", [i]) +} + +# Use multi-stage builds +default multi_stage = false +multi_stage = true { + input[i].Cmd == "copy" + val := concat(" ", input[i].Flags) + contains(lower(val), "--from=") +} +deny[msg] { + multi_stage == false + msg = sprintf("You COPY, but do not appear to use multi-stage builds...", []) +} \ No newline at end of file diff --git a/opa-k8s-security.rego b/opa-k8s-security.rego new file mode 100644 index 000000000..a2d631e45 --- /dev/null +++ b/opa-k8s-security.rego @@ -0,0 +1,13 @@ +package main + +deny[msg] { + input.kind = "Service" + not input.spec.type == "NodePort" + msg = "Service type should be NodePort" +} + +deny[msg] { + input.kind = "Deployment" + not input.spec.template.spec.containers[0].securityContext.runAsNonRoot == true + msg = "Containers must not run as root - use runAsNonRoot within the container security context" +} \ No newline at end of file diff --git a/packages-microsoft-prod.deb b/packages-microsoft-prod.deb new file mode 100644 index 000000000..14e3c6285 Binary files /dev/null and b/packages-microsoft-prod.deb differ diff --git a/pom.xml b/pom.xml index 8e71c7ac2..75ecb15c2 100644 --- a/pom.xml +++ b/pom.xml @@ -3,10 +3,9 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - org.springframework.boot - spring-boot-starter-parent - 2.2.1.RELEASE - + org.springframework.boot + spring-boot-starter-parent + 3.1.3 com.devsecops @@ -18,7 +17,7 @@ UTF-8 UTF-8 - 1.8 + 17 @@ -32,6 +31,12 @@ spring-boot-starter + + com.fasterxml.jackson.core + jackson-core + 2.15.2 + + org.springframework.boot spring-boot-starter-test @@ -45,8 +50,54 @@ org.springframework.boot spring-boot-maven-plugin - + + org.jacoco + jacoco-maven-plugin + 0.8.7 + + + prepare-agent + + prepare-agent + + + + report + test + + report + + + + + + org.pitest + pitest-maven + 1.7.0 + + + org.pitest + pitest-junit5-plugin + 0.14 + + + + 70 + + HTML + XML + + + + + org.owasp + dependency-check-maven + 6.5.0 + + 20 + ALL + + - - + \ No newline at end of file diff --git a/role.yaml b/role.yaml new file mode 100644 index 000000000..4a2c04e4b --- /dev/null +++ b/role.yaml @@ -0,0 +1,10 @@ +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + namespace: default + name: deployment-manager +rules: +- apiGroups: ["", "apps", "extensions"] + resources: ["deployments", "replicasets", "pods"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + diff --git a/secret.yaml b/secret.yaml new file mode 100644 index 000000000..a41492622 --- /dev/null +++ b/secret.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +type: kubernetes.io/service-account-token +metadata: + name: token-secret + namespace: default # Replace with the actual namespace name + annotations: + kubernetes.io/service-account.name: "jenkins-deployer" diff --git a/setup/vm-install-script/install-script.sh b/setup/vm-install-script/install-script.sh index 1ccbf3bff..20aa6f1a3 100644 --- a/setup/vm-install-script/install-script.sh +++ b/setup/vm-install-script/install-script.sh @@ -15,9 +15,9 @@ cat < /etc/apt/sources.list.d/kubernetes.list deb http://apt.kubernetes.io/ kubernetes-xenial main EOF -KUBE_VERSION=1.20.0 +KUBE_VERSION=1.28.0 apt-get update -apt-get install -y docker.io vim build-essential jq python3-pip kubelet=${KUBE_VERSION}-00 kubectl=${KUBE_VERSION}-00 kubernetes-cni=0.8.7-00 kubeadm=${KUBE_VERSION}-00 +apt-get install -y docker.io vim build-essential jq python3-pip kubelet=${KUBE_VERSION}-00 kubectl=${KUBE_VERSION}-00 kubernetes-cni=1.2.0-00 kubeadm=${KUBE_VERSION}-00 pip3 install jc ### UUID of VM @@ -62,7 +62,7 @@ kubectl get node -o wide echo ".........----------------#################._.-.-Java and MAVEN-.-._.#################----------------........." -sudo apt install openjdk-11-jdk -y +sudo apt install openjdk-17-jdk -y java -version sudo apt install -y maven mvn -v @@ -70,10 +70,13 @@ mvn -v echo ".........----------------#################._.-.-JENKINS-.-._.#################----------------........." -wget -q -O - https://pkg.jenkins.io/debian-stable/jenkins.io.key | sudo apt-key add - -sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list' -sudo apt update -sudo apt install -y jenkins +sudo wget -O /usr/share/keyrings/jenkins-keyring.asc \ + https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key +echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] \ + https://pkg.jenkins.io/debian-stable binary/ | sudo tee \ + /etc/apt/sources.list.d/jenkins.list > /dev/null +sudo apt-get update +sudo apt-get install jenkins systemctl daemon-reload systemctl enable jenkins sudo systemctl start jenkins diff --git a/src/main/java/com/devsecops/NumericController.java b/src/main/java/com/devsecops/NumericController.java index b0aeeeeed..7b31dd0ee 100644 --- a/src/main/java/com/devsecops/NumericController.java +++ b/src/main/java/com/devsecops/NumericController.java @@ -2,53 +2,51 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.context.annotation.Bean; import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; -import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @RestController -public class NumericController { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - private static final String baseURL = "http://node-service:5000/plusone"; - - RestTemplate restTemplate = new RestTemplate(); - - @RestController - public class compare { - - @GetMapping("/") - public String welcome() { - return "Kubernetes DevSecOps"; - } - - @GetMapping("/compare/{value}") - public String compareToFifty(@PathVariable int value) { - String message = "Could not determine comparison"; - if (value > 50) { - message = "Greater than 50"; - } else { - message = "Smaller than or equal to 50"; - } - return message; - } - - @GetMapping("/increment/{value}") - public int increment(@PathVariable int value) { - ResponseEntity responseEntity = restTemplate.getForEntity(baseURL + '/' + value, String.class); - String response = responseEntity.getBody(); - logger.info("Value Received in Request - " + value); - logger.info("Node Service Response - " + response); - return Integer.parseInt(response); - } - } - -} \ No newline at end of file +class NumericController { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private static final String BASE_URL = "http://node-service:5000/plusone"; + private final RestTemplate restTemplate = new RestTemplate(); + + @RestController + class Compare { + + @GetMapping("/") + String welcome() { + return "Kubernetes DevSecOps"; + } + + @GetMapping("/compare/{value}") + String compareToFifty(@PathVariable int value) { + return value > 50 ? "Greater than 50" : "Smaller than or equal to 50"; + } + + @GetMapping("/increment/{value}") + int increment(@PathVariable int value) { + try { + ResponseEntity responseEntity = restTemplate.getForEntity(BASE_URL + '/' + value, String.class); + String response = responseEntity.getBody(); + logger.info("Value Received in Request - {}", value); + logger.info("Node Service Response - {}", response); + return Integer.parseInt(response); + } catch (NumberFormatException e) { + logger.error("Error parsing response to integer. Value received: {}, Error: {}", value, e.getMessage()); + throw new ResponseParseException("Failed to parse the response from Node Service for value: " + value, e); + } + } + } + + // Custom exception class for handling response parse errors + static class ResponseParseException extends RuntimeException { + ResponseParseException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/src/main/java/com/devsecops/ResponseParseException.java b/src/main/java/com/devsecops/ResponseParseException.java new file mode 100644 index 000000000..13aafff56 --- /dev/null +++ b/src/main/java/com/devsecops/ResponseParseException.java @@ -0,0 +1,11 @@ +package com.devsecops; + +public class ResponseParseException extends RuntimeException { + public ResponseParseException(String message) { + super(message); + } + + public ResponseParseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/test/java/com/devsecops/NumericApplicationTests.java b/src/test/java/com/devsecops/NumericApplicationTests.java index 3e0ae20a4..619d6b98c 100644 --- a/src/test/java/com/devsecops/NumericApplicationTests.java +++ b/src/test/java/com/devsecops/NumericApplicationTests.java @@ -1,55 +1,40 @@ package com.devsecops; - -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.web.servlet.MockMvc; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -@RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc -public class NumericApplicationTests { +class NumericApplicationTests { @Autowired private MockMvc mockMvc; @Test - public void smallerThanOrEqualToFiftyMessage() throws Exception { - this.mockMvc.perform(get("/compare/49")).andDo(print()).andExpect(status().isOk()) + void smallerThanOrEqualToFiftyMessage() throws Exception { + this.mockMvc.perform(MockMvcRequestBuilders.get("/compare/50")) + .andExpect(status().isOk()) .andExpect(content().string("Smaller than or equal to 50")); } @Test - public void greaterThanFiftyMessage() throws Exception { - this.mockMvc.perform(get("/compare/51")).andDo(print()).andExpect(status().isOk()) + void greaterThanFiftyMessage() throws Exception { + this.mockMvc.perform(MockMvcRequestBuilders.get("/compare/51")) + .andExpect(status().isOk()) .andExpect(content().string("Greater than 50")); } - + @Test - public void welcomeMessage() throws Exception { - this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk()); + void welcomeMessage() throws Exception { + this.mockMvc.perform(MockMvcRequestBuilders.get("/")) + .andExpect(status().isOk()) + .andExpect(content().string("Kubernetes DevSecOps")); } - - -} \ No newline at end of file +} diff --git a/trivy-docker-image-scan.sh b/trivy-docker-image-scan.sh new file mode 100644 index 000000000..ff7b48212 --- /dev/null +++ b/trivy-docker-image-scan.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +dockerImageName=$(awk 'NR==1 {print $2}' Dockerfile) +echo $dockerImageName + +# Ensure Docker is logged in if the image is private +echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin + +# Run Trivy scan +docker run --rm -v $WORKSPACE:/root/.cache/aquasec/trivy:0.51.1 aquasec/trivy:0.51.1 image --exit-code 0 --severity HIGH --light $dockerImageName +docker run --rm -v $WORKSPACE:/root/.cache/aquasec/trivy:0.51.1 aquasec/trivy:0.51.1 image --exit-code 1 --severity CRITICAL --light $dockerImageName + +# Trivy scan result processing +exit_code=$? + +echo "Exit Code : $exit_code" + +# Check scan results +if [[ "${exit_code}" -eq 1 ]]; then + echo "Image scanning failed. Vulnerabilities found" + exit 1 +else + echo "Image scanning passed. No CRITICAL vulnerabilities found" +fi diff --git a/trivy-k8s-scan.sh b/trivy-k8s-scan.sh new file mode 100644 index 000000000..3e6be0ea5 --- /dev/null +++ b/trivy-k8s-scan.sh @@ -0,0 +1,19 @@ +#!/bin/bash +#trivy-k8s-scan + +echo $imageName #getting Image name from env variable + +docker run --rm -v $WORKSPACE:/root/.cache/ aquasec/trivy:0.17.2 -q image --exit-code 0 --severity LOW,MEDIUM,HIGH --light $imageName +docker run --rm -v $WORKSPACE:/root/.cache/ aquasec/trivy:0.17.2 -q image --exit-code 1 --severity CRITICAL --light $imageName + +# Trivy scan result processing +exit_code=$? +echo "Exit Code : $exit_code" + +# Check scan results +if [[ ${exit_code} == 1 ]]; then + echo "Image scanning failed. Vulnerabilities found" + exit 1; +else + echo "Image scanning passed. No vulnerabilities found" +fi; diff --git a/untitled/.gitignore b/untitled/.gitignore new file mode 100644 index 000000000..5ff6309b7 --- /dev/null +++ b/untitled/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/untitled/.idea/.gitignore b/untitled/.idea/.gitignore new file mode 100644 index 000000000..13566b81b --- /dev/null +++ b/untitled/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/untitled/.idea/misc.xml b/untitled/.idea/misc.xml new file mode 100644 index 000000000..690fc288b --- /dev/null +++ b/untitled/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/untitled/.idea/vcs.xml b/untitled/.idea/vcs.xml new file mode 100644 index 000000000..6c0b86358 --- /dev/null +++ b/untitled/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/untitled/pom.xml b/untitled/pom.xml new file mode 100644 index 000000000..2f4e6dd6c --- /dev/null +++ b/untitled/pom.xml @@ -0,0 +1,9 @@ + + 4.0.0 + org.example + untitled + 1.0-SNAPSHOT + Archetype - untitled + http://maven.apache.org + diff --git a/untitled/src/main/resources/META-INF/maven/archetype.xml b/untitled/src/main/resources/META-INF/maven/archetype.xml new file mode 100644 index 000000000..507c1c0d2 --- /dev/null +++ b/untitled/src/main/resources/META-INF/maven/archetype.xml @@ -0,0 +1,9 @@ + + untitled + + src/main/java/App.java + + + src/test/java/AppTest.java + + diff --git a/untitled/src/main/resources/archetype-resources/pom.xml b/untitled/src/main/resources/archetype-resources/pom.xml new file mode 100644 index 000000000..9c5e7e740 --- /dev/null +++ b/untitled/src/main/resources/archetype-resources/pom.xml @@ -0,0 +1,15 @@ + + 4.0.0 + $org.example + $untitled + $1.0-SNAPSHOT + + + junit + junit + 3.8.1 + test + + + diff --git a/untitled/src/main/resources/archetype-resources/src/main/java/App.java b/untitled/src/main/resources/archetype-resources/src/main/java/App.java new file mode 100644 index 000000000..1fa6a9565 --- /dev/null +++ b/untitled/src/main/resources/archetype-resources/src/main/java/App.java @@ -0,0 +1,13 @@ +package $org.example; + +/** + * Hello world! + * + */ +public class App +{ + public static void main( String[] args ) + { + System.out.println( "Hello World!" ); + } +} diff --git a/untitled/src/main/resources/archetype-resources/src/test/java/AppTest.java b/untitled/src/main/resources/archetype-resources/src/test/java/AppTest.java new file mode 100644 index 000000000..65be417e7 --- /dev/null +++ b/untitled/src/main/resources/archetype-resources/src/test/java/AppTest.java @@ -0,0 +1,38 @@ +package $org.example; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Unit test for simple App. + */ +public class AppTest + extends TestCase +{ + /** + * Create the test case + * + * @param testName name of the test case + */ + public AppTest( String testName ) + { + super( testName ); + } + + /** + * @return the suite of tests being tested + */ + public static Test suite() + { + return new TestSuite( AppTest.class ); + } + + /** + * Rigourous Test :-) + */ + public void testApp() + { + assertTrue( true ); + } +}