From f76b0c3124cf13ca6b8a6299d0058afbd84c413a Mon Sep 17 00:00:00 2001 From: oliviasiow Date: Sun, 21 Jun 2026 17:59:11 +0800 Subject: [PATCH 01/31] Update CI/CD workflows for result, vote, and worker services --- .../workflows/call-docker-build-result.yaml | 116 ++++++++---------- .github/workflows/call-docker-build-vote.yaml | 116 ++++++++---------- .../workflows/call-docker-build-worker.yaml | 116 ++++++++---------- 3 files changed, 144 insertions(+), 204 deletions(-) diff --git a/.github/workflows/call-docker-build-result.yaml b/.github/workflows/call-docker-build-result.yaml index a946a87b03..5856440b34 100644 --- a/.github/workflows/call-docker-build-result.yaml +++ b/.github/workflows/call-docker-build-result.yaml @@ -1,82 +1,62 @@ -name: Build Result -# template source: https://github.com/dockersamples/.github/blob/main/templates/call-docker-build.yaml +name: Build Result Service on: - # we want pull requests so we can build(test) but not push to image registry push: branches: - - 'main' - # only build when important files change + - main + - development paths: - 'result/**' - '.github/workflows/call-docker-build-result.yaml' pull_request: branches: - - 'main' - # only build when important files change + - main + - development paths: - 'result/**' - '.github/workflows/call-docker-build-result.yaml' jobs: - call-docker-build: - - name: Result Call Docker Build - - uses: dockersamples/.github/.github/workflows/reusable-docker-build.yaml@main - - permissions: - contents: read - packages: write # needed to push docker image to ghcr.io - pull-requests: write # needed to create and update comments in PRs - - secrets: - - # Only needed if with:dockerhub-enable is true below - dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} - - # Only needed if with:dockerhub-enable is true below - dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} - - with: - - ### REQUIRED - ### ENABLE ONE OR BOTH REGISTRIES - ### tell docker where to push. - ### NOTE if Docker Hub is set to true, you must set secrets above and also add account/repo/tags below - dockerhub-enable: true - ghcr-enable: true - - ### REQUIRED - ### A list of the account/repo names for docker build. List should match what's enabled above - ### defaults to: - image-names: | - ghcr.io/dockersamples/example-voting-app-result - dockersamples/examplevotingapp_result - - ### REQUIRED set rules for tagging images, based on special action syntax: - ### https://github.com/docker/metadata-action#tags-input - ### defaults to: - tag-rules: | - type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=raw,value=before,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=raw,value=after,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=ref,event=pr - - ### path to where docker should copy files into image - ### defaults to root of repository (.) - context: result - - ### Dockerfile alternate name. Default is Dockerfile (relative to context path) - # file: Containerfile - - ### build stage to target, defaults to empty, which builds to last stage in Dockerfile - # target: - - ### platforms to build for, defaults to linux/amd64 - ### other options: linux/amd64,linux/arm64,linux/arm/v7 - platforms: linux/amd64,linux/arm64,linux/arm/v7 - - ### Create a PR comment with image tags and labels - ### defaults to false - # comment-enable: false + build-scan-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-southeast-1 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Set version tag + run: | + if [[ "${GITHUB_REF##*/}" == "main" ]]; then + echo "TAG=v2" >> $GITHUB_ENV + else + echo "TAG=v1" >> $GITHUB_ENV + fi + + - name: Build Docker image + run: | + REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting + docker build -t $REGISTRY:result-${TAG} ./result + + - name: Security scan with Trivy + uses: aquasecurity/trivy-action@v0.20.0 + with: + scan-type: image + image-ref: ${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting:result-${{ env.TAG }} + severity: HIGH,CRITICAL + ignore-unfixed: true + + - name: Push image to ECR + run: | + REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting + docker push $REGISTRY:result-${TAG} diff --git a/.github/workflows/call-docker-build-vote.yaml b/.github/workflows/call-docker-build-vote.yaml index cb4a484a2a..3e5ead2871 100644 --- a/.github/workflows/call-docker-build-vote.yaml +++ b/.github/workflows/call-docker-build-vote.yaml @@ -1,82 +1,62 @@ -name: Build Vote -# template source: https://github.com/dockersamples/.github/blob/main/templates/call-docker-build.yaml +name: Build Vote Service on: - # we want pull requests so we can build(test) but not push to image registry push: branches: - - 'main' - # only build when important files change + - main + - development paths: - 'vote/**' - '.github/workflows/call-docker-build-vote.yaml' pull_request: branches: - - 'main' - # only build when important files change + - main + - development paths: - 'vote/**' - '.github/workflows/call-docker-build-vote.yaml' jobs: - call-docker-build: - - name: Vote Call Docker Build - - uses: dockersamples/.github/.github/workflows/reusable-docker-build.yaml@main - - permissions: - contents: read - packages: write # needed to push docker image to ghcr.io - pull-requests: write # needed to create and update comments in PRs - - secrets: - - # Only needed if with:dockerhub-enable is true below - dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} - - # Only needed if with:dockerhub-enable is true below - dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} - - with: - - ### REQUIRED - ### ENABLE ONE OR BOTH REGISTRIES - ### tell docker where to push. - ### NOTE if Docker Hub is set to true, you must set secrets above and also add account/repo/tags below - dockerhub-enable: true - ghcr-enable: true - - ### REQUIRED - ### A list of the account/repo names for docker build. List should match what's enabled above - ### defaults to: - image-names: | - ghcr.io/dockersamples/example-voting-app-vote - dockersamples/examplevotingapp_vote - - ### REQUIRED set rules for tagging images, based on special action syntax: - ### https://github.com/docker/metadata-action#tags-input - ### defaults to: - tag-rules: | - type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=raw,value=before,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=raw,value=after,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=ref,event=pr - - ### path to where docker should copy files into image - ### defaults to root of repository (.) - context: vote - - ### Dockerfile alternate name. Default is Dockerfile (relative to context path) - # file: Containerfile - - ### build stage to target, defaults to empty, which builds to last stage in Dockerfile - # target: - - ### platforms to build for, defaults to linux/amd64 - ### other options: linux/amd64,linux/arm64,linux/arm/v7 - platforms: linux/amd64,linux/arm64,linux/arm/v7 - - ### Create a PR comment with image tags and labels - ### defaults to false - # comment-enable: false + build-scan-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-southeast-1 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Set version tag + run: | + if [[ "${GITHUB_REF##*/}" == "main" ]]; then + echo "TAG=v2" >> $GITHUB_ENV + else + echo "TAG=v1" >> $GITHUB_ENV + fi + + - name: Build Docker image + run: | + REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting + docker build -t $REGISTRY:vote-${TAG} ./vote + + - name: Security scan with Trivy + uses: aquasecurity/trivy-action@v0.20.0 + with: + scan-type: image + image-ref: ${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting:vote-${{ env.TAG }} + severity: HIGH,CRITICAL + ignore-unfixed: true + + - name: Push image to ECR + run: | + REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting + docker push $REGISTRY:vote-${TAG} diff --git a/.github/workflows/call-docker-build-worker.yaml b/.github/workflows/call-docker-build-worker.yaml index 5abfb6bc9c..0302da0558 100644 --- a/.github/workflows/call-docker-build-worker.yaml +++ b/.github/workflows/call-docker-build-worker.yaml @@ -1,82 +1,62 @@ -name: Build Worker -# template source: https://github.com/dockersamples/.github/blob/main/templates/call-docker-build.yaml +name: Build Worker Service on: - # we want pull requests so we can build(test) but not push to image registry push: branches: - - 'main' - # only build when important files change + - main + - development paths: - 'worker/**' - '.github/workflows/call-docker-build-worker.yaml' pull_request: branches: - - 'main' - # only build when important files change + - main + - development paths: - 'worker/**' - '.github/workflows/call-docker-build-worker.yaml' jobs: - call-docker-build: - - name: Worker Call Docker Build - - uses: dockersamples/.github/.github/workflows/reusable-docker-build.yaml@main - - permissions: - contents: read - packages: write # needed to push docker image to ghcr.io - pull-requests: write # needed to create and update comments in PRs - - secrets: - - # Only needed if with:dockerhub-enable is true below - dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} - - # Only needed if with:dockerhub-enable is true below - dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} - - with: - - ### REQUIRED - ### ENABLE ONE OR BOTH REGISTRIES - ### tell docker where to push. - ### NOTE if Docker Hub is set to true, you must set secrets above and also add account/repo/tags below - dockerhub-enable: true - ghcr-enable: true - - ### REQUIRED - ### A list of the account/repo names for docker build. List should match what's enabled above - ### defaults to: - image-names: | - ghcr.io/dockersamples/example-voting-app-worker - dockersamples/examplevotingapp_worker - - ### REQUIRED set rules for tagging images, based on special action syntax: - ### https://github.com/docker/metadata-action#tags-input - ### defaults to: - tag-rules: | - type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=ref,event=pr - - ### path to where docker should copy files into image - ### defaults to root of repository (.) - context: worker - - ### Dockerfile alternate name. Default is Dockerfile (relative to context path) - # file: Containerfile - - ### build stage to target, defaults to empty, which builds to last stage in Dockerfile - # target: - - ### platforms to build for, defaults to linux/amd64 - ### other options: linux/amd64,linux/arm64,linux/arm/v7 - # FIXME worker arm/v7 support doesn't build in .net core 3.1 with QEMU - # a fix would likely run the .net build on amd64 but with a target of arm/v7 - platforms: linux/amd64,linux/arm64,linux/arm/v7 - - ### Create a PR comment with image tags and labels - ### defaults to false - # comment-enable: false + build-scan-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-southeast-1 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Set version tag + run: | + if [[ "${GITHUB_REF##*/}" == "main" ]]; then + echo "TAG=v2" >> $GITHUB_ENV + else + echo "TAG=v1" >> $GITHUB_ENV + fi + + - name: Build Docker image + run: | + REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting + docker build -t $REGISTRY:worker-${TAG} ./worker + + - name: Security scan with Trivy + uses: aquasecurity/trivy-action@v0.20.0 + with: + scan-type: image + image-ref: ${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting:worker-${{ env.TAG }} + severity: HIGH,CRITICAL + ignore-unfixed: true + + - name: Push image to ECR + run: | + REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting + docker push $REGISTRY:worker-${TAG} From e4e122d343efc0b881a0c8957dd0d8b040b7e66f Mon Sep 17 00:00:00 2001 From: oliviasiow Date: Sun, 21 Jun 2026 19:18:02 +0800 Subject: [PATCH 02/31] Enable manual trigger for Build Worker workflow --- .github/workflows/call-docker-build-worker.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/call-docker-build-worker.yaml b/.github/workflows/call-docker-build-worker.yaml index 0302da0558..463ccb0960 100644 --- a/.github/workflows/call-docker-build-worker.yaml +++ b/.github/workflows/call-docker-build-worker.yaml @@ -1,6 +1,7 @@ name: Build Worker Service on: + workflow_dispatch: push: branches: - main From 3908ad575a54a8fbcc617182828fa61333254101 Mon Sep 17 00:00:00 2001 From: oliviasiow Date: Sun, 21 Jun 2026 19:25:32 +0800 Subject: [PATCH 03/31] Enable manual trigger for Build Vote workflow --- .github/workflows/call-docker-build-vote.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/call-docker-build-vote.yaml b/.github/workflows/call-docker-build-vote.yaml index 3e5ead2871..5c95f9de4a 100644 --- a/.github/workflows/call-docker-build-vote.yaml +++ b/.github/workflows/call-docker-build-vote.yaml @@ -1,6 +1,7 @@ name: Build Vote Service on: + workflow_dispatch: push: branches: - main From dd7b22345dd0b095d60404baf1d0b79403ad1001 Mon Sep 17 00:00:00 2001 From: oliviasiow Date: Sun, 21 Jun 2026 19:26:25 +0800 Subject: [PATCH 04/31] Enable manual trigger for Build Result workflow --- .github/workflows/call-docker-build-result.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/call-docker-build-result.yaml b/.github/workflows/call-docker-build-result.yaml index 5856440b34..ccff5f2009 100644 --- a/.github/workflows/call-docker-build-result.yaml +++ b/.github/workflows/call-docker-build-result.yaml @@ -1,6 +1,7 @@ name: Build Result Service on: + workflow_dispatch: push: branches: - main From d19f08393112a241275fee0c0f1fdf51deb42bd3 Mon Sep 17 00:00:00 2001 From: oliviasiow Date: Mon, 22 Jun 2026 05:27:57 +0800 Subject: [PATCH 05/31] Update Kubernetes YAML manifests --- k8s-specifications/db-deployment.yaml | 25 ++++++------ k8s-specifications/db-service.yaml | 13 ++++--- k8s-specifications/redis-deployment.yaml | 9 +++-- k8s-specifications/redis-service.yaml | 11 +++--- k8s-specifications/result-v1-deployment.yaml | 40 ++++++++++++++++++++ k8s-specifications/result-v1-service.yaml | 16 ++++++++ k8s-specifications/result-v2-deployment.yaml | 40 ++++++++++++++++++++ k8s-specifications/vote-deployment.yaml | 16 +++++--- k8s-specifications/vote-service.yaml | 16 ++++---- k8s-specifications/worker-deployment.yaml | 16 ++++++-- 10 files changed, 159 insertions(+), 43 deletions(-) create mode 100644 k8s-specifications/result-v1-deployment.yaml create mode 100644 k8s-specifications/result-v1-service.yaml create mode 100644 k8s-specifications/result-v2-deployment.yaml diff --git a/k8s-specifications/db-deployment.yaml b/k8s-specifications/db-deployment.yaml index bc94ca7368..cf2eadd0d1 100644 --- a/k8s-specifications/db-deployment.yaml +++ b/k8s-specifications/db-deployment.yaml @@ -1,9 +1,10 @@ apiVersion: apps/v1 kind: Deployment metadata: + name: db-deployment + namespace: mightycapstone labels: app: db - name: db spec: replicas: 1 selector: @@ -15,19 +16,21 @@ spec: app: db spec: containers: - - image: postgres:15-alpine - name: postgres - env: - - name: POSTGRES_USER - value: postgres - - name: POSTGRES_PASSWORD - value: postgres + - name: postgres + image: postgres:15-alpine ports: - containerPort: 5432 name: postgres + env: + - name: POSTGRES_USER + value: myuser + - name: POSTGRES_PASSWORD + value: mypassword + - name: POSTGRES_DB + value: mydatabase volumeMounts: - mountPath: /var/lib/postgresql/data - name: db-data + name: postgres-data volumes: - - name: db-data - emptyDir: {} + - name: postgres-data + emptyDir: {} # ephemeral storage; replace with PVC for persistence diff --git a/k8s-specifications/db-service.yaml b/k8s-specifications/db-service.yaml index 104f1e8268..ce895c1ba7 100644 --- a/k8s-specifications/db-service.yaml +++ b/k8s-specifications/db-service.yaml @@ -1,15 +1,16 @@ apiVersion: v1 kind: Service metadata: + name: db-service + namespace: mightycapstone # 👈 ensure it matches your namespace labels: app: db - name: db spec: type: ClusterIP - ports: - - name: "db-service" - port: 5432 - targetPort: 5432 selector: app: db - + ports: + - name: postgres + port: 5432 # service port inside cluster + targetPort: 5432 # container port from db-deployment + diff --git a/k8s-specifications/redis-deployment.yaml b/k8s-specifications/redis-deployment.yaml index 24aa52135f..c58fb0204c 100644 --- a/k8s-specifications/redis-deployment.yaml +++ b/k8s-specifications/redis-deployment.yaml @@ -1,9 +1,10 @@ apiVersion: apps/v1 kind: Deployment metadata: + name: redis-deployment + namespace: mightycapstone labels: app: redis - name: redis spec: replicas: 1 selector: @@ -15,8 +16,8 @@ spec: app: redis spec: containers: - - image: redis:alpine - name: redis + - name: redis + image: redis:alpine ports: - containerPort: 6379 name: redis @@ -25,4 +26,4 @@ spec: name: redis-data volumes: - name: redis-data - emptyDir: {} + emptyDir: {} # ephemeral storage; replace with PVC for persistence diff --git a/k8s-specifications/redis-service.yaml b/k8s-specifications/redis-service.yaml index 809d31d875..0c0831b36f 100644 --- a/k8s-specifications/redis-service.yaml +++ b/k8s-specifications/redis-service.yaml @@ -1,15 +1,16 @@ apiVersion: v1 kind: Service metadata: + name: redis-service + namespace: mightycapstone labels: app: redis - name: redis spec: type: ClusterIP + selector: + app: redis ports: - - name: "redis-service" + - name: redis port: 6379 targetPort: 6379 - selector: - app: redis - + diff --git a/k8s-specifications/result-v1-deployment.yaml b/k8s-specifications/result-v1-deployment.yaml new file mode 100644 index 0000000000..05a574966d --- /dev/null +++ b/k8s-specifications/result-v1-deployment.yaml @@ -0,0 +1,40 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: result-v1-deployment + namespace: mightycapstone + labels: + app: result-v1 +spec: + replicas: 1 + selector: + matchLabels: + app: result-v1 + template: + metadata: + labels: + app: result-v1 + spec: + containers: + - name: result + image: 255945442255.dkr.ecr.ap-southeast-1.amazonaws.com/mightycapstonevoting:result-v1 + ports: + - containerPort: 80 + name: http + env: + - name: REDIS_HOST + value: redis-service + - name: DB_HOST + value: db-service + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 20 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 30 + periodSeconds: 10 diff --git a/k8s-specifications/result-v1-service.yaml b/k8s-specifications/result-v1-service.yaml new file mode 100644 index 0000000000..ee13b2516b --- /dev/null +++ b/k8s-specifications/result-v1-service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: result-v1-service + namespace: mightycapstone + labels: + app: result-v1 +spec: + type: LoadBalancer # 👈 AWS ELB will be provisioned + selector: + app: result-v1 + ports: + - name: http + port: 8081 # external port exposed by ELB + targetPort: 80 # container port inside result pod + diff --git a/k8s-specifications/result-v2-deployment.yaml b/k8s-specifications/result-v2-deployment.yaml new file mode 100644 index 0000000000..e8783907f0 --- /dev/null +++ b/k8s-specifications/result-v2-deployment.yaml @@ -0,0 +1,40 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: result-v2-deployment + namespace: mightycapstone + labels: + app: result-v2 +spec: + replicas: 1 + selector: + matchLabels: + app: result-v2 + template: + metadata: + labels: + app: result-v2 + spec: + containers: + - name: result + image: 255945442255.dkr.ecr.ap-southeast-1.amazonaws.com/mightycapstonevoting:result-v2 + ports: + - containerPort: 80 + name: http + env: + - name: REDIS_HOST + value: redis-service + - name: DB_HOST + value: db-service + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 20 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 30 + periodSeconds: 10 diff --git a/k8s-specifications/vote-deployment.yaml b/k8s-specifications/vote-deployment.yaml index 165a9478f8..e9973e3572 100644 --- a/k8s-specifications/vote-deployment.yaml +++ b/k8s-specifications/vote-deployment.yaml @@ -1,11 +1,12 @@ apiVersion: apps/v1 kind: Deployment metadata: + name: vote-deployment + namespace: mightycapstone labels: app: vote - name: vote spec: - replicas: 1 + replicas: 2 selector: matchLabels: app: vote @@ -15,8 +16,13 @@ spec: app: vote spec: containers: - - image: dockersamples/examplevotingapp_vote - name: vote + - name: vote + image: 255945442255.dkr.ecr.ap-southeast-1.amazonaws.com/mightycapstonevoting:vote-v2 ports: - containerPort: 80 - name: vote + name: http + env: + - name: REDIS_HOST + value: redis-service + - name: DB_HOST + value: db-service diff --git a/k8s-specifications/vote-service.yaml b/k8s-specifications/vote-service.yaml index d7a05b5513..176b2a2157 100644 --- a/k8s-specifications/vote-service.yaml +++ b/k8s-specifications/vote-service.yaml @@ -1,16 +1,16 @@ apiVersion: v1 kind: Service metadata: + name: vote-service + namespace: mightycapstone labels: app: vote - name: vote spec: - type: NodePort - ports: - - name: "vote-service" - port: 8080 - targetPort: 80 - nodePort: 31000 + type: LoadBalancer # 👈 no nodePort here selector: app: vote - + ports: + - name: http + port: 8080 # external port exposed by ELB + targetPort: 80 # container port + diff --git a/k8s-specifications/worker-deployment.yaml b/k8s-specifications/worker-deployment.yaml index 9e35450aec..17c4b9fc84 100644 --- a/k8s-specifications/worker-deployment.yaml +++ b/k8s-specifications/worker-deployment.yaml @@ -1,11 +1,12 @@ apiVersion: apps/v1 kind: Deployment metadata: + name: worker-deployment + namespace: mightycapstone labels: app: worker - name: worker spec: - replicas: 1 + replicas: 2 selector: matchLabels: app: worker @@ -15,5 +16,12 @@ spec: app: worker spec: containers: - - image: dockersamples/examplevotingapp_worker - name: worker + - name: worker + image: 255945442255.dkr.ecr.ap-southeast-1.amazonaws.com/mightycapstonevoting:worker-v2 + env: + - name: REDIS_HOST + value: redis-service + - name: DB_HOST + value: db-service + + From 121048d4dec0674fa33d87e1ef8a9444135ae84a Mon Sep 17 00:00:00 2001 From: oliviasiow Date: Mon, 22 Jun 2026 05:30:24 +0800 Subject: [PATCH 06/31] Update Worker Docker --- worker/Dockerfile | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/worker/Dockerfile b/worker/Dockerfile index a3f92d7e94..7779344f51 100644 --- a/worker/Dockerfile +++ b/worker/Dockerfile @@ -8,20 +8,20 @@ # docker buildx build --platform "linux/arm64/v8" . # build compiles the program for the builder's local platform -FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/dotnet/sdk:7.0 AS build -ARG TARGETPLATFORM +# Build stage +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build ARG TARGETARCH -ARG BUILDPLATFORM -RUN echo "I am running on $BUILDPLATFORM, building for $TARGETPLATFORM" - WORKDIR /source + +# Copy project file and restore dependencies COPY *.csproj . RUN dotnet restore -a $TARGETARCH +# Copy source and publish COPY . . -RUN dotnet publish -c release -o /app -a $TARGETARCH --self-contained false --no-restore +RUN dotnet publish -c Release -o /app -a $TARGETARCH --self-contained false --no-restore -# app image +# Runtime stage FROM mcr.microsoft.com/dotnet/runtime:7.0 WORKDIR /app COPY --from=build /app . From 688497a9e426466a43755da4679119f8d7646884 Mon Sep 17 00:00:00 2001 From: oliviasiow Date: Mon, 22 Jun 2026 05:31:38 +0800 Subject: [PATCH 07/31] Update result Dockerfile --- result/Dockerfile | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/result/Dockerfile b/result/Dockerfile index 33f0ba86fb..db836f2512 100644 --- a/result/Dockerfile +++ b/result/Dockerfile @@ -1,25 +1,14 @@ FROM node:18-slim -# add curl for healthcheck -RUN apt-get update && \ - apt-get install -y --no-install-recommends curl tini && \ - rm -rf /var/lib/apt/lists/* - WORKDIR /usr/local/app -# have nodemon available for local dev use (file watching) -RUN npm install -g nodemon - COPY package*.json ./ - -RUN npm ci && \ - npm cache clean --force && \ - mv /usr/local/app/node_modules /node_modules +RUN npm ci && npm cache clean --force COPY . . ENV PORT=80 EXPOSE 80 -ENTRYPOINT ["/usr/bin/tini", "--"] CMD ["node", "server.js"] + From 034359c7ed0a6647eb6a4144ba6785d2493d7014 Mon Sep 17 00:00:00 2001 From: oliviasiow Date: Mon, 22 Jun 2026 05:35:58 +0800 Subject: [PATCH 08/31] Update build and push file --- build_and_push.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100755 build_and_push.sh diff --git a/build_and_push.sh b/build_and_push.sh new file mode 100755 index 0000000000..6bc90ffdfe --- /dev/null +++ b/build_and_push.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# ECR registry URI +REGISTRY=255945442255.dkr.ecr.ap-southeast-1.amazonaws.com/mightycapstonevoting + +# Authenticate Docker to ECR +aws ecr get-login-password --region ap-southeast-1 \ + | docker login --username AWS --password-stdin 255945442255.dkr.ecr.ap-southeast-1.amazonaws.com + +# Build & push Vote service +docker build -t $REGISTRY:vote-v1 ./vote +docker build -t $REGISTRY:vote-v2 ./vote +docker push $REGISTRY:vote-v1 +docker push $REGISTRY:vote-v2 + +# Build & push Result service +docker build -t $REGISTRY:result-v1 ./result +docker build -t $REGISTRY:result-v2 ./result +docker push $REGISTRY:result-v1 +docker push $REGISTRY:result-v2 + +# Build & push Worker service +docker build -t $REGISTRY:worker-v1 ./worker +docker build -t $REGISTRY:worker-v2 ./worker +docker push $REGISTRY:worker-v1 +docker push $REGISTRY:worker-v2 + +echo "✅ All images built and pushed to ECR" From a3056ebf94b08c4da4437961596b2e9bdb6aa5c1 Mon Sep 17 00:00:00 2001 From: oliviasiow Date: Mon, 22 Jun 2026 05:44:19 +0800 Subject: [PATCH 09/31] Update server.js --- result/server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/result/server.js b/result/server.js index 1c8593e7ee..ad3ce8c9ae 100644 --- a/result/server.js +++ b/result/server.js @@ -6,7 +6,7 @@ var express = require('express'), server = require('http').Server(app), io = require('socket.io')(server); -var port = process.env.PORT || 4000; +var port = process.env.PORT || 80; io.on('connection', function (socket) { @@ -18,7 +18,7 @@ io.on('connection', function (socket) { }); var pool = new Pool({ - connectionString: 'postgres://postgres:postgres@db/postgres' + connectionString: 'postgres://postgres:postgres@db-service/postgres' }); async.retry( From f93bc2014a543e132cafbc5f1e1cde9273e66c89 Mon Sep 17 00:00:00 2001 From: leizichao Date: Tue, 23 Jun 2026 22:41:26 +0800 Subject: [PATCH 10/31] Add DevSecOps security scans to application workflows --- .../workflows/call-docker-build-result.yaml | 95 ++++++++++++++++--- .github/workflows/call-docker-build-vote.yaml | 91 +++++++++++++++--- .../workflows/call-docker-build-worker.yaml | 75 ++++++++++++--- 3 files changed, 222 insertions(+), 39 deletions(-) diff --git a/.github/workflows/call-docker-build-result.yaml b/.github/workflows/call-docker-build-result.yaml index ccff5f2009..ed83e31069 100644 --- a/.github/workflows/call-docker-build-result.yaml +++ b/.github/workflows/call-docker-build-result.yaml @@ -21,20 +21,44 @@ jobs: build-scan-push: runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read + steps: - name: Checkout code uses: actions/checkout@v4 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + - name: Run SAST scan with Semgrep + run: | + python -m pip install --upgrade pip + python -m pip install semgrep + semgrep scan \ + --config p/ci \ + --severity ERROR \ + --error \ + --exclude result/views/angular.min.js \ + --exclude result/views/socket.io.js \ + . + + - name: Run dependency vulnerability scan with Trivy + uses: aquasecurity/trivy-action@v0.20.0 with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ap-southeast-1 + scan-type: fs + scan-ref: ./result + vuln-type: library + severity: HIGH,CRITICAL + ignore-unfixed: true + exit-code: '1' - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 + - name: Run secret scan with Gitleaks + run: | + GITLEAKS_VERSION=8.24.2 + curl -sSL -o gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + tar -xzf gitleaks.tar.gz gitleaks + sudo mv gitleaks /usr/local/bin/gitleaks + gitleaks detect --source . --redact --verbose --exit-code 1 - name: Set version tag run: | @@ -46,18 +70,63 @@ jobs: - name: Build Docker image run: | - REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting - docker build -t $REGISTRY:result-${TAG} ./result + docker build -t mightycapstonevoting:result-${TAG} ./result - - name: Security scan with Trivy + - name: Run container image scan with Trivy uses: aquasecurity/trivy-action@v0.20.0 with: scan-type: image - image-ref: ${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting:result-${{ env.TAG }} + image-ref: mightycapstonevoting:result-${{ env.TAG }} severity: HIGH,CRITICAL ignore-unfixed: true + exit-code: '1' + + - name: Run DAST scan with OWASP ZAP + run: | + docker network create result-dast-net + docker run -d --name db-service --network result-dast-net \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_DB=postgres \ + postgres:15-alpine + docker run -d --name result-dast --network result-dast-net -p 8081:80 mightycapstonevoting:result-${TAG} + sleep 30 + curl --fail --retry 10 --retry-delay 5 http://localhost:8081/ + mkdir -p zap-reports + docker run --rm --network host \ + -v "${PWD}/zap-reports:/zap/wrk/:rw" \ + ghcr.io/zaproxy/zaproxy:stable \ + zap-baseline.py -t http://localhost:8081 -I -r result-zap-report.html + + - name: Upload DAST report + uses: actions/upload-artifact@v4 + if: always() + with: + name: result-zap-report + path: zap-reports/result-zap-report.html + if-no-files-found: ignore + + - name: Configure AWS credentials + if: github.event_name != 'pull_request' + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${ secrets.AWS_ACCESS_KEY_ID } + aws-secret-access-key: ${ secrets.AWS_SECRET_ACCESS_KEY } + aws-region: ap-southeast-1 + + - name: Login to Amazon ECR + if: github.event_name != 'pull_request' + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Tag image for ECR + if: github.event_name != 'pull_request' + run: | + REGISTRY=${ steps.login-ecr.outputs.registry }/mightycapstonevoting + docker tag mightycapstonevoting:result-${TAG} $REGISTRY:result-${TAG} - name: Push image to ECR + if: github.event_name != 'pull_request' run: | - REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting + REGISTRY=${ steps.login-ecr.outputs.registry }/mightycapstonevoting docker push $REGISTRY:result-${TAG} diff --git a/.github/workflows/call-docker-build-vote.yaml b/.github/workflows/call-docker-build-vote.yaml index 5c95f9de4a..e02ac45046 100644 --- a/.github/workflows/call-docker-build-vote.yaml +++ b/.github/workflows/call-docker-build-vote.yaml @@ -21,20 +21,44 @@ jobs: build-scan-push: runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read + steps: - name: Checkout code uses: actions/checkout@v4 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + - name: Run SAST scan with Semgrep + run: | + python -m pip install --upgrade pip + python -m pip install semgrep + semgrep scan \ + --config p/ci \ + --severity ERROR \ + --error \ + --exclude result/views/angular.min.js \ + --exclude result/views/socket.io.js \ + . + + - name: Run dependency vulnerability scan with Trivy + uses: aquasecurity/trivy-action@v0.20.0 with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ap-southeast-1 + scan-type: fs + scan-ref: ./vote + vuln-type: library + severity: HIGH,CRITICAL + ignore-unfixed: true + exit-code: '1' - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 + - name: Run secret scan with Gitleaks + run: | + GITLEAKS_VERSION=8.24.2 + curl -sSL -o gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + tar -xzf gitleaks.tar.gz gitleaks + sudo mv gitleaks /usr/local/bin/gitleaks + gitleaks detect --source . --redact --verbose --exit-code 1 - name: Set version tag run: | @@ -46,18 +70,59 @@ jobs: - name: Build Docker image run: | - REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting - docker build -t $REGISTRY:vote-${TAG} ./vote + docker build -t mightycapstonevoting:vote-${TAG} ./vote - - name: Security scan with Trivy + - name: Run container image scan with Trivy uses: aquasecurity/trivy-action@v0.20.0 with: scan-type: image - image-ref: ${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting:vote-${{ env.TAG }} + image-ref: mightycapstonevoting:vote-${{ env.TAG }} severity: HIGH,CRITICAL ignore-unfixed: true + exit-code: '1' + + - name: Run DAST scan with OWASP ZAP + run: | + docker network create vote-dast-net + docker run -d --name redis --network vote-dast-net redis:7-alpine + docker run -d --name vote-dast --network vote-dast-net -p 8080:80 mightycapstonevoting:vote-${TAG} + sleep 20 + curl --fail --retry 10 --retry-delay 5 http://localhost:8080/ + mkdir -p zap-reports + docker run --rm --network host \ + -v "${PWD}/zap-reports:/zap/wrk/:rw" \ + ghcr.io/zaproxy/zaproxy:stable \ + zap-baseline.py -t http://localhost:8080 -I -r vote-zap-report.html + + - name: Upload DAST report + uses: actions/upload-artifact@v4 + if: always() + with: + name: vote-zap-report + path: zap-reports/vote-zap-report.html + if-no-files-found: ignore + + - name: Configure AWS credentials + if: github.event_name != 'pull_request' + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${ secrets.AWS_ACCESS_KEY_ID } + aws-secret-access-key: ${ secrets.AWS_SECRET_ACCESS_KEY } + aws-region: ap-southeast-1 + + - name: Login to Amazon ECR + if: github.event_name != 'pull_request' + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Tag image for ECR + if: github.event_name != 'pull_request' + run: | + REGISTRY=${ steps.login-ecr.outputs.registry }/mightycapstonevoting + docker tag mightycapstonevoting:vote-${TAG} $REGISTRY:vote-${TAG} - name: Push image to ECR + if: github.event_name != 'pull_request' run: | - REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting + REGISTRY=${ steps.login-ecr.outputs.registry }/mightycapstonevoting docker push $REGISTRY:vote-${TAG} diff --git a/.github/workflows/call-docker-build-worker.yaml b/.github/workflows/call-docker-build-worker.yaml index 463ccb0960..54135949d3 100644 --- a/.github/workflows/call-docker-build-worker.yaml +++ b/.github/workflows/call-docker-build-worker.yaml @@ -21,20 +21,44 @@ jobs: build-scan-push: runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read + steps: - name: Checkout code uses: actions/checkout@v4 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + - name: Run SAST scan with Semgrep + run: | + python -m pip install --upgrade pip + python -m pip install semgrep + semgrep scan \ + --config p/ci \ + --severity ERROR \ + --error \ + --exclude result/views/angular.min.js \ + --exclude result/views/socket.io.js \ + . + + - name: Run dependency vulnerability scan with Trivy + uses: aquasecurity/trivy-action@v0.20.0 with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ap-southeast-1 + scan-type: fs + scan-ref: ./worker + vuln-type: library + severity: HIGH,CRITICAL + ignore-unfixed: true + exit-code: '1' - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 + - name: Run secret scan with Gitleaks + run: | + GITLEAKS_VERSION=8.24.2 + curl -sSL -o gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + tar -xzf gitleaks.tar.gz gitleaks + sudo mv gitleaks /usr/local/bin/gitleaks + gitleaks detect --source . --redact --verbose --exit-code 1 - name: Set version tag run: | @@ -46,18 +70,43 @@ jobs: - name: Build Docker image run: | - REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting - docker build -t $REGISTRY:worker-${TAG} ./worker + docker build -t mightycapstonevoting:worker-${TAG} ./worker - - name: Security scan with Trivy + - name: Run container image scan with Trivy uses: aquasecurity/trivy-action@v0.20.0 with: scan-type: image - image-ref: ${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting:worker-${{ env.TAG }} + image-ref: mightycapstonevoting:worker-${{ env.TAG }} severity: HIGH,CRITICAL ignore-unfixed: true + exit-code: '1' + + - name: DAST note for worker service + run: | + echo "Worker is a background service without an HTTP endpoint, so OWASP ZAP DAST is not applicable to this service." + echo "DAST is implemented for the vote and result web services." + + - name: Configure AWS credentials + if: github.event_name != 'pull_request' + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${ secrets.AWS_ACCESS_KEY_ID } + aws-secret-access-key: ${ secrets.AWS_SECRET_ACCESS_KEY } + aws-region: ap-southeast-1 + + - name: Login to Amazon ECR + if: github.event_name != 'pull_request' + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Tag image for ECR + if: github.event_name != 'pull_request' + run: | + REGISTRY=${ steps.login-ecr.outputs.registry }/mightycapstonevoting + docker tag mightycapstonevoting:worker-${TAG} $REGISTRY:worker-${TAG} - name: Push image to ECR + if: github.event_name != 'pull_request' run: | - REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting + REGISTRY=${ steps.login-ecr.outputs.registry }/mightycapstonevoting docker push $REGISTRY:worker-${TAG} From 2257de67e9818783ba7310ef8a9dc2e0de5c8be5 Mon Sep 17 00:00:00 2001 From: leizichao Date: Tue, 23 Jun 2026 23:19:32 +0800 Subject: [PATCH 11/31] Allow Trivy vulnerability reporting without blocking development pipeline --- .github/workflows/call-docker-build-result.yaml | 4 ++-- .github/workflows/call-docker-build-vote.yaml | 4 ++-- .github/workflows/call-docker-build-worker.yaml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/call-docker-build-result.yaml b/.github/workflows/call-docker-build-result.yaml index ed83e31069..04ec62be25 100644 --- a/.github/workflows/call-docker-build-result.yaml +++ b/.github/workflows/call-docker-build-result.yaml @@ -50,7 +50,7 @@ jobs: vuln-type: library severity: HIGH,CRITICAL ignore-unfixed: true - exit-code: '1' + exit-code: '0' - name: Run secret scan with Gitleaks run: | @@ -79,7 +79,7 @@ jobs: image-ref: mightycapstonevoting:result-${{ env.TAG }} severity: HIGH,CRITICAL ignore-unfixed: true - exit-code: '1' + exit-code: '0' - name: Run DAST scan with OWASP ZAP run: | diff --git a/.github/workflows/call-docker-build-vote.yaml b/.github/workflows/call-docker-build-vote.yaml index e02ac45046..cb7dbbfe05 100644 --- a/.github/workflows/call-docker-build-vote.yaml +++ b/.github/workflows/call-docker-build-vote.yaml @@ -50,7 +50,7 @@ jobs: vuln-type: library severity: HIGH,CRITICAL ignore-unfixed: true - exit-code: '1' + exit-code: '0' - name: Run secret scan with Gitleaks run: | @@ -79,7 +79,7 @@ jobs: image-ref: mightycapstonevoting:vote-${{ env.TAG }} severity: HIGH,CRITICAL ignore-unfixed: true - exit-code: '1' + exit-code: '0' - name: Run DAST scan with OWASP ZAP run: | diff --git a/.github/workflows/call-docker-build-worker.yaml b/.github/workflows/call-docker-build-worker.yaml index 54135949d3..a107613dd5 100644 --- a/.github/workflows/call-docker-build-worker.yaml +++ b/.github/workflows/call-docker-build-worker.yaml @@ -50,7 +50,7 @@ jobs: vuln-type: library severity: HIGH,CRITICAL ignore-unfixed: true - exit-code: '1' + exit-code: '0' - name: Run secret scan with Gitleaks run: | @@ -79,7 +79,7 @@ jobs: image-ref: mightycapstonevoting:worker-${{ env.TAG }} severity: HIGH,CRITICAL ignore-unfixed: true - exit-code: '1' + exit-code: '0' - name: DAST note for worker service run: | From 29569b11c7445e8e53f074c99f645ffb73ecc252 Mon Sep 17 00:00:00 2001 From: oliviasyhspm Date: Thu, 25 Jun 2026 14:26:47 +0800 Subject: [PATCH 12/31] Fix formatting in AWS credentials configuration step --- .github/workflows/call-docker-build-worker.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/call-docker-build-worker.yaml b/.github/workflows/call-docker-build-worker.yaml index a107613dd5..9dff2ba9b2 100644 --- a/.github/workflows/call-docker-build-worker.yaml +++ b/.github/workflows/call-docker-build-worker.yaml @@ -86,16 +86,14 @@ jobs: echo "Worker is a background service without an HTTP endpoint, so OWASP ZAP DAST is not applicable to this service." echo "DAST is implemented for the vote and result web services." - - name: Configure AWS credentials - if: github.event_name != 'pull_request' + - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${ secrets.AWS_ACCESS_KEY_ID } aws-secret-access-key: ${ secrets.AWS_SECRET_ACCESS_KEY } aws-region: ap-southeast-1 - + - name: Login to Amazon ECR - if: github.event_name != 'pull_request' id: login-ecr uses: aws-actions/amazon-ecr-login@v2 From 1fbd4520db7e681b1b84ade4186ff5b5a4273efc Mon Sep 17 00:00:00 2001 From: oliviasyhspm Date: Thu, 25 Jun 2026 15:09:19 +0800 Subject: [PATCH 13/31] Add AWS credentials verification step --- .github/workflows/call-docker-build-worker.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/call-docker-build-worker.yaml b/.github/workflows/call-docker-build-worker.yaml index 9dff2ba9b2..a9f1c12346 100644 --- a/.github/workflows/call-docker-build-worker.yaml +++ b/.github/workflows/call-docker-build-worker.yaml @@ -85,6 +85,9 @@ jobs: run: | echo "Worker is a background service without an HTTP endpoint, so OWASP ZAP DAST is not applicable to this service." echo "DAST is implemented for the vote and result web services." + + - name: Verify AWS credentials + run: aws sts get-caller-identity - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 From d3199fbb8edceee920b97990f1c59f40ca4508de Mon Sep 17 00:00:00 2001 From: oliviasyhspm Date: Thu, 25 Jun 2026 15:23:56 +0800 Subject: [PATCH 14/31] Refactor AWS credentials configuration in workflow syntax error double {{ }} for AWS secrets --- .github/workflows/call-docker-build-worker.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/call-docker-build-worker.yaml b/.github/workflows/call-docker-build-worker.yaml index a9f1c12346..1951bf183a 100644 --- a/.github/workflows/call-docker-build-worker.yaml +++ b/.github/workflows/call-docker-build-worker.yaml @@ -86,16 +86,16 @@ jobs: echo "Worker is a background service without an HTTP endpoint, so OWASP ZAP DAST is not applicable to this service." echo "DAST is implemented for the vote and result web services." - - name: Verify AWS credentials - run: aws sts get-caller-identity - - - name: Configure AWS credentials + - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: - aws-access-key-id: ${ secrets.AWS_ACCESS_KEY_ID } - aws-secret-access-key: ${ secrets.AWS_SECRET_ACCESS_KEY } + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-southeast-1 - + + - name: Verify AWS credentials + run: aws sts get-caller-identity + - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v2 From ad6757cad9113c32f22040936b149175a2b69e7f Mon Sep 17 00:00:00 2001 From: leizichao Date: Thu, 25 Jun 2026 21:34:16 +0800 Subject: [PATCH 15/31] Fix AWS credentials issue in workflow --- .../workflows/call-docker-build-result.yaml | 45 +++++++------------ .github/workflows/call-docker-build-vote.yaml | 45 +++++++------------ .../workflows/call-docker-build-worker.yaml | 22 +-------- 3 files changed, 33 insertions(+), 79 deletions(-) diff --git a/.github/workflows/call-docker-build-result.yaml b/.github/workflows/call-docker-build-result.yaml index 04ec62be25..50d43692e7 100644 --- a/.github/workflows/call-docker-build-result.yaml +++ b/.github/workflows/call-docker-build-result.yaml @@ -21,15 +21,17 @@ jobs: build-scan-push: runs-on: ubuntu-latest - permissions: - contents: read - security-events: write - actions: read - steps: - name: Checkout code uses: actions/checkout@v4 + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-southeast-1 + - name: Run SAST scan with Semgrep run: | python -m pip install --upgrade pip @@ -60,6 +62,10 @@ jobs: sudo mv gitleaks /usr/local/bin/gitleaks gitleaks detect --source . --redact --verbose --exit-code 1 + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + - name: Set version tag run: | if [[ "${GITHUB_REF##*/}" == "main" ]]; then @@ -70,13 +76,14 @@ jobs: - name: Build Docker image run: | - docker build -t mightycapstonevoting:result-${TAG} ./result + REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting + docker build -t $REGISTRY:result-${TAG} ./result - - name: Run container image scan with Trivy + - name: Security scan with Trivy uses: aquasecurity/trivy-action@v0.20.0 with: scan-type: image - image-ref: mightycapstonevoting:result-${{ env.TAG }} + image-ref: ${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting:result-${{ env.TAG }} severity: HIGH,CRITICAL ignore-unfixed: true exit-code: '0' @@ -106,27 +113,7 @@ jobs: path: zap-reports/result-zap-report.html if-no-files-found: ignore - - name: Configure AWS credentials - if: github.event_name != 'pull_request' - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${ secrets.AWS_ACCESS_KEY_ID } - aws-secret-access-key: ${ secrets.AWS_SECRET_ACCESS_KEY } - aws-region: ap-southeast-1 - - - name: Login to Amazon ECR - if: github.event_name != 'pull_request' - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Tag image for ECR - if: github.event_name != 'pull_request' - run: | - REGISTRY=${ steps.login-ecr.outputs.registry }/mightycapstonevoting - docker tag mightycapstonevoting:result-${TAG} $REGISTRY:result-${TAG} - - name: Push image to ECR - if: github.event_name != 'pull_request' run: | - REGISTRY=${ steps.login-ecr.outputs.registry }/mightycapstonevoting + REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting docker push $REGISTRY:result-${TAG} diff --git a/.github/workflows/call-docker-build-vote.yaml b/.github/workflows/call-docker-build-vote.yaml index cb7dbbfe05..3109ab1393 100644 --- a/.github/workflows/call-docker-build-vote.yaml +++ b/.github/workflows/call-docker-build-vote.yaml @@ -21,15 +21,17 @@ jobs: build-scan-push: runs-on: ubuntu-latest - permissions: - contents: read - security-events: write - actions: read - steps: - name: Checkout code uses: actions/checkout@v4 + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-southeast-1 + - name: Run SAST scan with Semgrep run: | python -m pip install --upgrade pip @@ -60,6 +62,10 @@ jobs: sudo mv gitleaks /usr/local/bin/gitleaks gitleaks detect --source . --redact --verbose --exit-code 1 + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + - name: Set version tag run: | if [[ "${GITHUB_REF##*/}" == "main" ]]; then @@ -70,13 +76,14 @@ jobs: - name: Build Docker image run: | - docker build -t mightycapstonevoting:vote-${TAG} ./vote + REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting + docker build -t $REGISTRY:vote-${TAG} ./vote - - name: Run container image scan with Trivy + - name: Security scan with Trivy uses: aquasecurity/trivy-action@v0.20.0 with: scan-type: image - image-ref: mightycapstonevoting:vote-${{ env.TAG }} + image-ref: ${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting:vote-${{ env.TAG }} severity: HIGH,CRITICAL ignore-unfixed: true exit-code: '0' @@ -102,27 +109,7 @@ jobs: path: zap-reports/vote-zap-report.html if-no-files-found: ignore - - name: Configure AWS credentials - if: github.event_name != 'pull_request' - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${ secrets.AWS_ACCESS_KEY_ID } - aws-secret-access-key: ${ secrets.AWS_SECRET_ACCESS_KEY } - aws-region: ap-southeast-1 - - - name: Login to Amazon ECR - if: github.event_name != 'pull_request' - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Tag image for ECR - if: github.event_name != 'pull_request' - run: | - REGISTRY=${ steps.login-ecr.outputs.registry }/mightycapstonevoting - docker tag mightycapstonevoting:vote-${TAG} $REGISTRY:vote-${TAG} - - name: Push image to ECR - if: github.event_name != 'pull_request' run: | - REGISTRY=${ steps.login-ecr.outputs.registry }/mightycapstonevoting + REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting docker push $REGISTRY:vote-${TAG} diff --git a/.github/workflows/call-docker-build-worker.yaml b/.github/workflows/call-docker-build-worker.yaml index 1951bf183a..79260ef8dd 100644 --- a/.github/workflows/call-docker-build-worker.yaml +++ b/.github/workflows/call-docker-build-worker.yaml @@ -86,28 +86,8 @@ jobs: echo "Worker is a background service without an HTTP endpoint, so OWASP ZAP DAST is not applicable to this service." echo "DAST is implemented for the vote and result web services." - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ap-southeast-1 - - - name: Verify AWS credentials - run: aws sts get-caller-identity - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Tag image for ECR - if: github.event_name != 'pull_request' - run: | - REGISTRY=${ steps.login-ecr.outputs.registry }/mightycapstonevoting - docker tag mightycapstonevoting:worker-${TAG} $REGISTRY:worker-${TAG} - - name: Push image to ECR if: github.event_name != 'pull_request' run: | REGISTRY=${ steps.login-ecr.outputs.registry }/mightycapstonevoting - docker push $REGISTRY:worker-${TAG} + docker push $REGISTRY:worker-${TAG} \ No newline at end of file From 934f9797bcfc221da53bb658b9501c2585ada197 Mon Sep 17 00:00:00 2001 From: leizichao Date: Thu, 25 Jun 2026 22:31:43 +0800 Subject: [PATCH 16/31] Fix AWS credentials issue in workflow for Devlopment --- ...l-docker-build-result.yaml:Zone.Identifier | Bin 0 -> 25 bytes ...all-docker-build-vote.yaml:Zone.Identifier | Bin 0 -> 25 bytes .../workflows/call-docker-build-worker.yaml | 30 +++++++++++------- ...l-docker-build-worker.yaml:Zone.Identifier | Bin 0 -> 25 bytes 4 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/call-docker-build-result.yaml:Zone.Identifier create mode 100644 .github/workflows/call-docker-build-vote.yaml:Zone.Identifier create mode 100644 .github/workflows/call-docker-build-worker.yaml:Zone.Identifier diff --git a/.github/workflows/call-docker-build-result.yaml:Zone.Identifier b/.github/workflows/call-docker-build-result.yaml:Zone.Identifier new file mode 100644 index 0000000000000000000000000000000000000000..d6c1ec682968c796b9f5e9e080cc6f674b57c766 GIT binary patch literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2xdl#JyUFr831@K2xdl#JyUFr831@K2x Date: Thu, 25 Jun 2026 22:55:06 +0800 Subject: [PATCH 17/31] Fix Docker tag issue in workflow for Devlopment --- .github/workflows/call-docker-build-result.yaml | 4 ++-- .github/workflows/call-docker-build-vote.yaml | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/call-docker-build-result.yaml b/.github/workflows/call-docker-build-result.yaml index 50d43692e7..3223793e20 100644 --- a/.github/workflows/call-docker-build-result.yaml +++ b/.github/workflows/call-docker-build-result.yaml @@ -77,7 +77,7 @@ jobs: - name: Build Docker image run: | REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting - docker build -t $REGISTRY:result-${TAG} ./result + docker build -t result-service:${TAG} -t $REGISTRY:result-${TAG} ./result - name: Security scan with Trivy uses: aquasecurity/trivy-action@v0.20.0 @@ -96,7 +96,7 @@ jobs: -e POSTGRES_PASSWORD=postgres \ -e POSTGRES_DB=postgres \ postgres:15-alpine - docker run -d --name result-dast --network result-dast-net -p 8081:80 mightycapstonevoting:result-${TAG} + docker run -d --name result-dast --network result-dast-net -p 8081:80 result-service:${TAG} sleep 30 curl --fail --retry 10 --retry-delay 5 http://localhost:8081/ mkdir -p zap-reports diff --git a/.github/workflows/call-docker-build-vote.yaml b/.github/workflows/call-docker-build-vote.yaml index 3109ab1393..bb78474931 100644 --- a/.github/workflows/call-docker-build-vote.yaml +++ b/.github/workflows/call-docker-build-vote.yaml @@ -77,13 +77,14 @@ jobs: - name: Build Docker image run: | REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting - docker build -t $REGISTRY:vote-${TAG} ./vote + docker build -t vote-service:${TAG} ./vote + docker tag vote-service:${TAG} $REGISTRY:vote-${TAG} - name: Security scan with Trivy uses: aquasecurity/trivy-action@v0.20.0 with: scan-type: image - image-ref: ${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting:vote-${{ env.TAG }} + image-ref: vote-service:${{ env.TAG }} severity: HIGH,CRITICAL ignore-unfixed: true exit-code: '0' @@ -92,7 +93,7 @@ jobs: run: | docker network create vote-dast-net docker run -d --name redis --network vote-dast-net redis:7-alpine - docker run -d --name vote-dast --network vote-dast-net -p 8080:80 mightycapstonevoting:vote-${TAG} + docker run -d --name vote-dast --network vote-dast-net -p 8080:80 vote-service:${TAG} sleep 20 curl --fail --retry 10 --retry-delay 5 http://localhost:8080/ mkdir -p zap-reports From 8a3cb2f75a99b32505773311a6f4e3d8e34e7d4e Mon Sep 17 00:00:00 2001 From: leizichao Date: Sat, 27 Jun 2026 18:59:30 +0800 Subject: [PATCH 18/31] Update Security Report Generation and Download --- .../workflows/call-docker-build-result.yaml | 390 +++++++++++++++++- .github/workflows/call-docker-build-vote.yaml | 388 ++++++++++++++++- .../workflows/call-docker-build-worker.yaml | 353 +++++++++++++++- 3 files changed, 1105 insertions(+), 26 deletions(-) diff --git a/.github/workflows/call-docker-build-result.yaml b/.github/workflows/call-docker-build-result.yaml index 3223793e20..efdfe6460a 100644 --- a/.github/workflows/call-docker-build-result.yaml +++ b/.github/workflows/call-docker-build-result.yaml @@ -25,6 +25,11 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Prepare security report directory + run: | + mkdir -p security-reports + chmod 777 security-reports + - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: @@ -39,10 +44,85 @@ jobs: semgrep scan \ --config p/ci \ --severity ERROR \ - --error \ --exclude result/views/angular.min.js \ --exclude result/views/socket.io.js \ - . + --sarif \ + --output security-reports/semgrep-result.sarif \ + . || true + + - name: Generate SAST HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/semgrep-result.sarif") + html_path = pathlib.Path("security-reports/semgrep-result.html") + title = "SAST (Semgrep) - Result Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish SAST Summary + if: always() + run: | + { + echo "## SAST (Semgrep) - Result Service" + echo "" + if [ -f security-reports/semgrep-result.sarif ]; then + echo "✅ SAST scan completed." + echo "" + echo "**Report artifact files:** semgrep-result.sarif, semgrep-result.html" + else + echo "❌ SAST report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" - name: Run dependency vulnerability scan with Trivy uses: aquasecurity/trivy-action@v0.20.0 @@ -53,6 +133,82 @@ jobs: severity: HIGH,CRITICAL ignore-unfixed: true exit-code: '0' + format: sarif + output: security-reports/trivy-fs-result.sarif + + - name: Generate Dependency Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/trivy-fs-result.sarif") + html_path = pathlib.Path("security-reports/trivy-fs-result.html") + title = "SCA / Dependency Scan (Trivy FS) - Result Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Dependency Scan Summary + if: always() + run: | + { + echo "## SCA / Dependency Vulnerability Scan (Trivy FS) - Result Service" + echo "" + if [ -f security-reports/trivy-fs-result.sarif ]; then + echo "✅ Dependency vulnerability scan completed." + echo "" + echo "**Report artifact files:** trivy-fs-result.sarif, trivy-fs-result.html" + else + echo "❌ Dependency scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" - name: Run secret scan with Gitleaks run: | @@ -60,7 +216,77 @@ jobs: curl -sSL -o gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" tar -xzf gitleaks.tar.gz gitleaks sudo mv gitleaks /usr/local/bin/gitleaks - gitleaks detect --source . --redact --verbose --exit-code 1 + gitleaks detect \ + --source . \ + --redact \ + --verbose \ + --report-format json \ + --report-path security-reports/gitleaks-result.json \ + --exit-code 0 + + - name: Generate Secret Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + json_path = pathlib.Path("security-reports/gitleaks-result.json") + html_path = pathlib.Path("security-reports/gitleaks-result.html") + title = "Secret Scan (Gitleaks) - Result Service" + findings = [] + error = None + try: + text = json_path.read_text(encoding="utf-8").strip() + data = json.loads(text) if text else [] + if isinstance(data, list): + findings = data + elif isinstance(data, dict): + findings = data.get("findings") or data.get("results") or [] + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source JSON file: {html.escape(str(json_path))}

" + ] + if error: + parts.append(f"

Could not parse Gitleaks report: {html.escape(error)}

") + elif not findings: + parts.append("

No secrets were reported.

") + else: + parts.append(f"

Total findings: {len(findings)}

") + parts.append("") + for item in findings: + rule = item.get("RuleID") or item.get("rule") or "" + desc = item.get("Description") or item.get("description") or "" + file = item.get("File") or item.get("file") or "" + line = item.get("StartLine") or item.get("line") or "" + commit = item.get("Commit") or item.get("commit") or "" + parts.append("" + "".join(f"" for x in (rule, desc, file, line, commit)) + "") + parts.append("
RuleDescriptionFileLineCommit
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Secret Scan Summary + if: always() + run: | + { + echo "## Secret Scan (Gitleaks) - Result Service" + echo "" + if [ -f security-reports/gitleaks-result.json ]; then + echo "✅ Secret scan completed." + echo "" + echo "**Report artifact files:** gitleaks-result.json, gitleaks-result.html" + else + echo "❌ Secret scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" - name: Login to Amazon ECR id: login-ecr @@ -83,14 +309,92 @@ jobs: uses: aquasecurity/trivy-action@v0.20.0 with: scan-type: image - image-ref: ${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting:result-${{ env.TAG }} + image-ref: result-service:${{ env.TAG }} severity: HIGH,CRITICAL ignore-unfixed: true exit-code: '0' + format: sarif + output: security-reports/trivy-image-result.sarif + + - name: Generate Container Image Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/trivy-image-result.sarif") + html_path = pathlib.Path("security-reports/trivy-image-result.html") + title = "Container Image Scan (Trivy Image) - Result Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Container Image Scan Summary + if: always() + run: | + { + echo "## Container Image Scan (Trivy Image) - Result Service" + echo "" + if [ -f security-reports/trivy-image-result.sarif ]; then + echo "✅ Container image scan completed." + echo "" + echo "**Report artifact files:** trivy-image-result.sarif, trivy-image-result.html" + else + echo "❌ Container image scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" - name: Run DAST scan with OWASP ZAP + continue-on-error: true run: | - docker network create result-dast-net + docker network create result-dast-net || true + docker rm -f db-service result-dast || true docker run -d --name db-service --network result-dast-net \ -e POSTGRES_USER=postgres \ -e POSTGRES_PASSWORD=postgres \ @@ -99,19 +403,83 @@ jobs: docker run -d --name result-dast --network result-dast-net -p 8081:80 result-service:${TAG} sleep 30 curl --fail --retry 10 --retry-delay 5 http://localhost:8081/ + mkdir -p zap-reports - docker run --rm --network host \ + chmod 777 zap-reports + + docker run --rm \ + --user root \ + --network host \ -v "${PWD}/zap-reports:/zap/wrk/:rw" \ ghcr.io/zaproxy/zaproxy:stable \ - zap-baseline.py -t http://localhost:8081 -I -r result-zap-report.html + zap-baseline.py \ + -t http://localhost:8081 \ + -I \ + -r result-zap-report.html + + mkdir -p security-reports + cp zap-reports/result-zap-report.html security-reports/result-zap-report.html || true + echo "=== zap-reports ===" + ls -lah zap-reports || true + echo "=== zap-reports ===" + ls -lah zap-reports || true + echo "=== security-reports ===" + ls -lah security-reports || true || true + + - name: Verify ZAP Report + if: always() + run: | + echo "=== zap-reports ===" + ls -lah zap-reports || true + echo "=== security-reports ===" + ls -lah security-reports || true + + - name: Publish DAST Summary + if: always() + run: | + { + echo "## DAST (OWASP ZAP) - Result Service" + echo "" + if [ -f security-reports/result-zap-report.html ]; then + echo "✅ DAST scan completed and HTML report was generated." + echo "" + echo "**Report artifact file:** result-zap-report.html" + else + echo "❌ DAST report was not generated." + echo "" + echo "Please check the OWASP ZAP logs above." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Publish Consolidated Security Summary + if: always() + run: | + { + echo "# DevSecOps Security Reports - Result Service" + echo "" + echo "| Security Control | Report File |" + echo "|---|---|" + echo "| SAST (Semgrep) | semgrep-result.sarif, semgrep-result.html |" + echo "| SCA / Dependency Scan (Trivy FS) | trivy-fs-result.sarif, trivy-fs-result.html |" + echo "| Secret Scan (Gitleaks) | gitleaks-result.json, gitleaks-result.html |" + echo "| Container Image Scan (Trivy Image) | trivy-image-result.sarif, trivy-image-result.html |" + echo "| DAST (OWASP ZAP) | result-zap-report.html |" + echo "" + echo "Human-readable HTML reports and machine-readable SARIF/JSON reports are uploaded as the **security-reports-result** workflow artifact." + echo "" + echo "## Files generated" + echo "" + ls -lah security-reports || true + echo "" + } >> "$GITHUB_STEP_SUMMARY" - - name: Upload DAST report + - name: Upload Security Reports uses: actions/upload-artifact@v4 if: always() with: - name: result-zap-report - path: zap-reports/result-zap-report.html - if-no-files-found: ignore + name: security-reports-result + path: security-reports/ + if-no-files-found: warn - name: Push image to ECR run: | diff --git a/.github/workflows/call-docker-build-vote.yaml b/.github/workflows/call-docker-build-vote.yaml index bb78474931..8724f6e455 100644 --- a/.github/workflows/call-docker-build-vote.yaml +++ b/.github/workflows/call-docker-build-vote.yaml @@ -25,6 +25,11 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Prepare security report directory + run: | + mkdir -p security-reports + chmod 777 security-reports + - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: @@ -39,10 +44,85 @@ jobs: semgrep scan \ --config p/ci \ --severity ERROR \ - --error \ --exclude result/views/angular.min.js \ --exclude result/views/socket.io.js \ - . + --sarif \ + --output security-reports/semgrep-vote.sarif \ + . || true + + - name: Generate SAST HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/semgrep-vote.sarif") + html_path = pathlib.Path("security-reports/semgrep-vote.html") + title = "SAST (Semgrep) - Vote Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish SAST Summary + if: always() + run: | + { + echo "## SAST (Semgrep) - Vote Service" + echo "" + if [ -f security-reports/semgrep-vote.sarif ]; then + echo "✅ SAST scan completed." + echo "" + echo "**Report artifact files:** semgrep-vote.sarif, semgrep-vote.html" + else + echo "❌ SAST report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" - name: Run dependency vulnerability scan with Trivy uses: aquasecurity/trivy-action@v0.20.0 @@ -53,6 +133,82 @@ jobs: severity: HIGH,CRITICAL ignore-unfixed: true exit-code: '0' + format: sarif + output: security-reports/trivy-fs-vote.sarif + + - name: Generate Dependency Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/trivy-fs-vote.sarif") + html_path = pathlib.Path("security-reports/trivy-fs-vote.html") + title = "SCA / Dependency Scan (Trivy FS) - Vote Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Dependency Scan Summary + if: always() + run: | + { + echo "## SCA / Dependency Vulnerability Scan (Trivy FS) - Vote Service" + echo "" + if [ -f security-reports/trivy-fs-vote.sarif ]; then + echo "✅ Dependency vulnerability scan completed." + echo "" + echo "**Report artifact files:** trivy-fs-vote.sarif, trivy-fs-vote.html" + else + echo "❌ Dependency scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" - name: Run secret scan with Gitleaks run: | @@ -60,7 +216,77 @@ jobs: curl -sSL -o gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" tar -xzf gitleaks.tar.gz gitleaks sudo mv gitleaks /usr/local/bin/gitleaks - gitleaks detect --source . --redact --verbose --exit-code 1 + gitleaks detect \ + --source . \ + --redact \ + --verbose \ + --report-format json \ + --report-path security-reports/gitleaks-vote.json \ + --exit-code 0 + + - name: Generate Secret Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + json_path = pathlib.Path("security-reports/gitleaks-vote.json") + html_path = pathlib.Path("security-reports/gitleaks-vote.html") + title = "Secret Scan (Gitleaks) - Vote Service" + findings = [] + error = None + try: + text = json_path.read_text(encoding="utf-8").strip() + data = json.loads(text) if text else [] + if isinstance(data, list): + findings = data + elif isinstance(data, dict): + findings = data.get("findings") or data.get("results") or [] + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source JSON file: {html.escape(str(json_path))}

" + ] + if error: + parts.append(f"

Could not parse Gitleaks report: {html.escape(error)}

") + elif not findings: + parts.append("

No secrets were reported.

") + else: + parts.append(f"

Total findings: {len(findings)}

") + parts.append("") + for item in findings: + rule = item.get("RuleID") or item.get("rule") or "" + desc = item.get("Description") or item.get("description") or "" + file = item.get("File") or item.get("file") or "" + line = item.get("StartLine") or item.get("line") or "" + commit = item.get("Commit") or item.get("commit") or "" + parts.append("" + "".join(f"" for x in (rule, desc, file, line, commit)) + "") + parts.append("
RuleDescriptionFileLineCommit
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Secret Scan Summary + if: always() + run: | + { + echo "## Secret Scan (Gitleaks) - Vote Service" + echo "" + if [ -f security-reports/gitleaks-vote.json ]; then + echo "✅ Secret scan completed." + echo "" + echo "**Report artifact files:** gitleaks-vote.json, gitleaks-vote.html" + else + echo "❌ Secret scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" - name: Login to Amazon ECR id: login-ecr @@ -88,27 +314,169 @@ jobs: severity: HIGH,CRITICAL ignore-unfixed: true exit-code: '0' + format: sarif + output: security-reports/trivy-image-vote.sarif + + - name: Generate Container Image Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/trivy-image-vote.sarif") + html_path = pathlib.Path("security-reports/trivy-image-vote.html") + title = "Container Image Scan (Trivy Image) - Vote Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Container Image Scan Summary + if: always() + run: | + { + echo "## Container Image Scan (Trivy Image) - Vote Service" + echo "" + if [ -f security-reports/trivy-image-vote.sarif ]; then + echo "✅ Container image scan completed." + echo "" + echo "**Report artifact files:** trivy-image-vote.sarif, trivy-image-vote.html" + else + echo "❌ Container image scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" - name: Run DAST scan with OWASP ZAP + continue-on-error: true run: | - docker network create vote-dast-net + docker network create vote-dast-net || true + docker rm -f redis vote-dast || true docker run -d --name redis --network vote-dast-net redis:7-alpine docker run -d --name vote-dast --network vote-dast-net -p 8080:80 vote-service:${TAG} sleep 20 curl --fail --retry 10 --retry-delay 5 http://localhost:8080/ + mkdir -p zap-reports - docker run --rm --network host \ + chmod 777 zap-reports + + docker run --rm \ + --user root \ + --network host \ -v "${PWD}/zap-reports:/zap/wrk/:rw" \ ghcr.io/zaproxy/zaproxy:stable \ - zap-baseline.py -t http://localhost:8080 -I -r vote-zap-report.html + zap-baseline.py \ + -t http://localhost:8080 \ + -I \ + -r vote-zap-report.html + + mkdir -p security-reports + cp zap-reports/vote-zap-report.html security-reports/vote-zap-report.html || true + echo "=== zap-reports ===" + ls -lah zap-reports || true + echo "=== zap-reports ===" + ls -lah zap-reports || true + echo "=== security-reports ===" + ls -lah security-reports || true || true + + - name: Verify ZAP Report + if: always() + run: | + echo "=== zap-reports ===" + ls -lah zap-reports || true + echo "=== security-reports ===" + ls -lah security-reports || true + + - name: Publish DAST Summary + if: always() + run: | + { + echo "## DAST (OWASP ZAP) - Vote Service" + echo "" + if [ -f security-reports/vote-zap-report.html ]; then + echo "✅ DAST scan completed and HTML report was generated." + echo "" + echo "**Report artifact file:** vote-zap-report.html" + else + echo "❌ DAST report was not generated." + echo "" + echo "Please check the OWASP ZAP logs above." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Publish Consolidated Security Summary + if: always() + run: | + { + echo "# DevSecOps Security Reports - Vote Service" + echo "" + echo "| Security Control | Report File |" + echo "|---|---|" + echo "| SAST (Semgrep) | semgrep-vote.sarif, semgrep-vote.html |" + echo "| SCA / Dependency Scan (Trivy FS) | trivy-fs-vote.sarif, trivy-fs-vote.html |" + echo "| Secret Scan (Gitleaks) | gitleaks-vote.json, gitleaks-vote.html |" + echo "| Container Image Scan (Trivy Image) | trivy-image-vote.sarif, trivy-image-vote.html |" + echo "| DAST (OWASP ZAP) | vote-zap-report.html |" + echo "" + echo "Human-readable HTML reports and machine-readable SARIF/JSON reports are uploaded as the **security-reports-vote** workflow artifact." + echo "" + echo "## Files generated" + echo "" + ls -lah security-reports || true + echo "" + } >> "$GITHUB_STEP_SUMMARY" - - name: Upload DAST report + - name: Upload Security Reports uses: actions/upload-artifact@v4 if: always() with: - name: vote-zap-report - path: zap-reports/vote-zap-report.html - if-no-files-found: ignore + name: security-reports-vote + path: security-reports/ + if-no-files-found: warn - name: Push image to ECR run: | diff --git a/.github/workflows/call-docker-build-worker.yaml b/.github/workflows/call-docker-build-worker.yaml index 53ccd4a3ad..7d52dab957 100644 --- a/.github/workflows/call-docker-build-worker.yaml +++ b/.github/workflows/call-docker-build-worker.yaml @@ -25,6 +25,11 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Prepare security report directory + run: | + mkdir -p security-reports + chmod 777 security-reports + - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: @@ -39,10 +44,85 @@ jobs: semgrep scan \ --config p/ci \ --severity ERROR \ - --error \ --exclude result/views/angular.min.js \ --exclude result/views/socket.io.js \ - . + --sarif \ + --output security-reports/semgrep-worker.sarif \ + . || true + + - name: Generate SAST HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/semgrep-worker.sarif") + html_path = pathlib.Path("security-reports/semgrep-worker.html") + title = "SAST (Semgrep) - Worker Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish SAST Summary + if: always() + run: | + { + echo "## SAST (Semgrep) - Worker Service" + echo "" + if [ -f security-reports/semgrep-worker.sarif ]; then + echo "✅ SAST scan completed." + echo "" + echo "**Report artifact files:** semgrep-worker.sarif, semgrep-worker.html" + else + echo "❌ SAST report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" - name: Run dependency vulnerability scan with Trivy uses: aquasecurity/trivy-action@v0.20.0 @@ -53,6 +133,82 @@ jobs: severity: HIGH,CRITICAL ignore-unfixed: true exit-code: '0' + format: sarif + output: security-reports/trivy-fs-worker.sarif + + - name: Generate Dependency Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/trivy-fs-worker.sarif") + html_path = pathlib.Path("security-reports/trivy-fs-worker.html") + title = "SCA / Dependency Scan (Trivy FS) - Worker Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Dependency Scan Summary + if: always() + run: | + { + echo "## SCA / Dependency Vulnerability Scan (Trivy FS) - Worker Service" + echo "" + if [ -f security-reports/trivy-fs-worker.sarif ]; then + echo "✅ Dependency vulnerability scan completed." + echo "" + echo "**Report artifact files:** trivy-fs-worker.sarif, trivy-fs-worker.html" + else + echo "❌ Dependency scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" - name: Run secret scan with Gitleaks run: | @@ -60,7 +216,77 @@ jobs: curl -sSL -o gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" tar -xzf gitleaks.tar.gz gitleaks sudo mv gitleaks /usr/local/bin/gitleaks - gitleaks detect --source . --redact --verbose --exit-code 1 + gitleaks detect \ + --source . \ + --redact \ + --verbose \ + --report-format json \ + --report-path security-reports/gitleaks-worker.json \ + --exit-code 0 + + - name: Generate Secret Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + json_path = pathlib.Path("security-reports/gitleaks-worker.json") + html_path = pathlib.Path("security-reports/gitleaks-worker.html") + title = "Secret Scan (Gitleaks) - Worker Service" + findings = [] + error = None + try: + text = json_path.read_text(encoding="utf-8").strip() + data = json.loads(text) if text else [] + if isinstance(data, list): + findings = data + elif isinstance(data, dict): + findings = data.get("findings") or data.get("results") or [] + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source JSON file: {html.escape(str(json_path))}

" + ] + if error: + parts.append(f"

Could not parse Gitleaks report: {html.escape(error)}

") + elif not findings: + parts.append("

No secrets were reported.

") + else: + parts.append(f"

Total findings: {len(findings)}

") + parts.append("") + for item in findings: + rule = item.get("RuleID") or item.get("rule") or "" + desc = item.get("Description") or item.get("description") or "" + file = item.get("File") or item.get("file") or "" + line = item.get("StartLine") or item.get("line") or "" + commit = item.get("Commit") or item.get("commit") or "" + parts.append("" + "".join(f"" for x in (rule, desc, file, line, commit)) + "") + parts.append("
RuleDescriptionFileLineCommit
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Secret Scan Summary + if: always() + run: | + { + echo "## Secret Scan (Gitleaks) - Worker Service" + echo "" + if [ -f security-reports/gitleaks-worker.json ]; then + echo "✅ Secret scan completed." + echo "" + echo "**Report artifact files:** gitleaks-worker.json, gitleaks-worker.html" + else + echo "❌ Secret scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" - name: Login to Amazon ECR id: login-ecr @@ -77,22 +303,139 @@ jobs: - name: Build Docker image run: | REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting - docker build -t $REGISTRY:worker-${TAG} ./worker + docker build -t worker-service:${TAG} -t $REGISTRY:worker-${TAG} ./worker - name: Security scan with Trivy uses: aquasecurity/trivy-action@v0.20.0 with: scan-type: image - image-ref: ${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting:worker-${{ env.TAG }} + image-ref: worker-service:${{ env.TAG }} severity: HIGH,CRITICAL ignore-unfixed: true exit-code: '0' + format: sarif + output: security-reports/trivy-image-worker.sarif + + - name: Generate Container Image Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/trivy-image-worker.sarif") + html_path = pathlib.Path("security-reports/trivy-image-worker.html") + title = "Container Image Scan (Trivy Image) - Worker Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Container Image Scan Summary + if: always() + run: | + { + echo "## Container Image Scan (Trivy Image) - Worker Service" + echo "" + if [ -f security-reports/trivy-image-worker.sarif ]; then + echo "✅ Container image scan completed." + echo "" + echo "**Report artifact files:** trivy-image-worker.sarif, trivy-image-worker.html" + else + echo "❌ Container image scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" - name: DAST note for worker service run: | echo "Worker is a background service without an HTTP endpoint, so OWASP ZAP DAST is not applicable to this service." echo "DAST is implemented for the vote and result web services." + - name: Publish DAST Summary + if: always() + run: | + { + echo "## DAST (OWASP ZAP) - Worker Service" + echo "" + echo "ℹ️ DAST is not applicable because the worker is a background service without an HTTP endpoint." + echo "" + echo "DAST is implemented for the vote and result web services." + } >> "$GITHUB_STEP_SUMMARY" + + - name: Publish Consolidated Security Summary + if: always() + run: | + { + echo "# DevSecOps Security Reports - Worker Service" + echo "" + echo "| Security Control | Report File |" + echo "|---|---|" + echo "| SAST (Semgrep) | semgrep-worker.sarif, semgrep-worker.html |" + echo "| SCA / Dependency Scan (Trivy FS) | trivy-fs-worker.sarif, trivy-fs-worker.html |" + echo "| Secret Scan (Gitleaks) | gitleaks-worker.json, gitleaks-worker.html |" + echo "| Container Image Scan (Trivy Image) | trivy-image-worker.sarif, trivy-image-worker.html |" + echo "| DAST (OWASP ZAP) | Not applicable - background service |" + echo "" + echo "Human-readable HTML reports and machine-readable SARIF/JSON reports are uploaded as the **security-reports-worker** workflow artifact." + echo "" + echo "## Files generated" + echo "" + ls -lah security-reports || true + echo "" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload Security Reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: security-reports-worker + path: security-reports/ + if-no-files-found: warn + - name: Push image to ECR run: | REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting From 850038fb20eb39682df4c115c793dedc2453d477 Mon Sep 17 00:00:00 2001 From: leizichao Date: Mon, 29 Jun 2026 22:57:14 +0800 Subject: [PATCH 19/31] Use GitHub OIDC IAM role for AWS deployment --- .github/workflows/call-docker-build-result.yaml | 12 +++++++++--- .github/workflows/call-docker-build-vote.yaml | 12 +++++++++--- .github/workflows/call-docker-build-worker.yaml | 12 +++++++++--- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/.github/workflows/call-docker-build-result.yaml b/.github/workflows/call-docker-build-result.yaml index efdfe6460a..b945a4bc3f 100644 --- a/.github/workflows/call-docker-build-result.yaml +++ b/.github/workflows/call-docker-build-result.yaml @@ -17,6 +17,10 @@ on: - 'result/**' - '.github/workflows/call-docker-build-result.yaml' +permissions: + id-token: write + contents: read + jobs: build-scan-push: runs-on: ubuntu-latest @@ -30,13 +34,15 @@ jobs: mkdir -p security-reports chmod 777 security-reports - - name: Configure AWS credentials + - name: Configure AWS credentials using OIDC uses: aws-actions/configure-aws-credentials@v4 with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} aws-region: ap-southeast-1 + - name: Verify AWS identity + run: aws sts get-caller-identity + - name: Run SAST scan with Semgrep run: | python -m pip install --upgrade pip diff --git a/.github/workflows/call-docker-build-vote.yaml b/.github/workflows/call-docker-build-vote.yaml index 8724f6e455..dfb954e435 100644 --- a/.github/workflows/call-docker-build-vote.yaml +++ b/.github/workflows/call-docker-build-vote.yaml @@ -17,6 +17,10 @@ on: - 'vote/**' - '.github/workflows/call-docker-build-vote.yaml' +permissions: + id-token: write + contents: read + jobs: build-scan-push: runs-on: ubuntu-latest @@ -30,13 +34,15 @@ jobs: mkdir -p security-reports chmod 777 security-reports - - name: Configure AWS credentials + - name: Configure AWS credentials using OIDC uses: aws-actions/configure-aws-credentials@v4 with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} aws-region: ap-southeast-1 + - name: Verify AWS identity + run: aws sts get-caller-identity + - name: Run SAST scan with Semgrep run: | python -m pip install --upgrade pip diff --git a/.github/workflows/call-docker-build-worker.yaml b/.github/workflows/call-docker-build-worker.yaml index 7d52dab957..272bd952a6 100644 --- a/.github/workflows/call-docker-build-worker.yaml +++ b/.github/workflows/call-docker-build-worker.yaml @@ -17,6 +17,10 @@ on: - 'worker/**' - '.github/workflows/call-docker-build-worker.yaml' +permissions: + id-token: write + contents: read + jobs: build-scan-push: runs-on: ubuntu-latest @@ -30,13 +34,15 @@ jobs: mkdir -p security-reports chmod 777 security-reports - - name: Configure AWS credentials + - name: Configure AWS credentials using OIDC uses: aws-actions/configure-aws-credentials@v4 with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} aws-region: ap-southeast-1 + - name: Verify AWS identity + run: aws sts get-caller-identity + - name: Run SAST scan with Semgrep run: | python -m pip install --upgrade pip From bbba7b9dfe6bbf3769986896d38755ad5f7db3e6 Mon Sep 17 00:00:00 2001 From: oliviasiow Date: Tue, 30 Jun 2026 00:28:09 +0800 Subject: [PATCH 20/31] Test OIDC IAM role authentication from development branch --- .github/workflows/call-docker-build-result.yaml | 1 + .github/workflows/call-docker-build-vote.yaml | 1 + .github/workflows/call-docker-build-worker.yaml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/call-docker-build-result.yaml b/.github/workflows/call-docker-build-result.yaml index b945a4bc3f..82e01ab1f6 100644 --- a/.github/workflows/call-docker-build-result.yaml +++ b/.github/workflows/call-docker-build-result.yaml @@ -1,4 +1,5 @@ name: Build Result Service +# Trigger OIDC test commit on: workflow_dispatch: diff --git a/.github/workflows/call-docker-build-vote.yaml b/.github/workflows/call-docker-build-vote.yaml index dfb954e435..5150a3de41 100644 --- a/.github/workflows/call-docker-build-vote.yaml +++ b/.github/workflows/call-docker-build-vote.yaml @@ -1,4 +1,5 @@ name: Build Vote Service +# Trigger OIDC test commit on: workflow_dispatch: diff --git a/.github/workflows/call-docker-build-worker.yaml b/.github/workflows/call-docker-build-worker.yaml index 272bd952a6..a4e7e03be0 100644 --- a/.github/workflows/call-docker-build-worker.yaml +++ b/.github/workflows/call-docker-build-worker.yaml @@ -1,4 +1,5 @@ name: Build Worker Service +# Trigger OIDC test commit on: workflow_dispatch: From 289e8fc03995f314c5a3cf6b18e7685d2813c119 Mon Sep 17 00:00:00 2001 From: kannanmadhu Date: Tue, 30 Jun 2026 07:02:32 +0800 Subject: [PATCH 21/31] Testing OIDC config with kubectl --- .../call-docker-build-result-dev-oidc.yaml | 512 ++++++++++++++++++ 1 file changed, 512 insertions(+) create mode 100644 .github/workflows/call-docker-build-result-dev-oidc.yaml diff --git a/.github/workflows/call-docker-build-result-dev-oidc.yaml b/.github/workflows/call-docker-build-result-dev-oidc.yaml new file mode 100644 index 0000000000..e9d2a75c1c --- /dev/null +++ b/.github/workflows/call-docker-build-result-dev-oidc.yaml @@ -0,0 +1,512 @@ +name: Build Result Service +# Trigger OIDC test commit + +on: + workflow_dispatch: + push: + branches: + - development + paths: + - 'result/**' + - '.github/workflows/call-docker-build-result-dev-oidc.yaml' +# pull_request: +# branches: +# - main +# - development +# paths: +# - 'result/**' +# - '.github/workflows/call-docker-build-result-dev-oidc.yaml' + +permissions: + id-token: write + contents: read + +jobs: + build-scan-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Prepare security report directory + run: | + mkdir -p security-reports + chmod 777 security-reports + + - name: Configure AWS credentials using OIDC + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ap-southeast-1 + + - name: Verify AWS identity + run: aws sts get-caller-identity + + - name: Run SAST scan with Semgrep + run: | + python -m pip install --upgrade pip + python -m pip install semgrep + semgrep scan \ + --config p/ci \ + --severity ERROR \ + --exclude result/views/angular.min.js \ + --exclude result/views/socket.io.js \ + --sarif \ + --output security-reports/semgrep-result.sarif \ + . || true + + - name: Generate SAST HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/semgrep-result.sarif") + html_path = pathlib.Path("security-reports/semgrep-result.html") + title = "SAST (Semgrep) - Result Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish SAST Summary + if: always() + run: | + { + echo "## SAST (Semgrep) - Result Service" + echo "" + if [ -f security-reports/semgrep-result.sarif ]; then + echo "✅ SAST scan completed." + echo "" + echo "**Report artifact files:** semgrep-result.sarif, semgrep-result.html" + else + echo "❌ SAST report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Run dependency vulnerability scan with Trivy + uses: aquasecurity/trivy-action@v0.20.0 + with: + scan-type: fs + scan-ref: ./result + vuln-type: library + severity: HIGH,CRITICAL + ignore-unfixed: true + exit-code: '0' + format: sarif + output: security-reports/trivy-fs-result.sarif + + - name: Generate Dependency Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/trivy-fs-result.sarif") + html_path = pathlib.Path("security-reports/trivy-fs-result.html") + title = "SCA / Dependency Scan (Trivy FS) - Result Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Dependency Scan Summary + if: always() + run: | + { + echo "## SCA / Dependency Vulnerability Scan (Trivy FS) - Result Service" + echo "" + if [ -f security-reports/trivy-fs-result.sarif ]; then + echo "✅ Dependency vulnerability scan completed." + echo "" + echo "**Report artifact files:** trivy-fs-result.sarif, trivy-fs-result.html" + else + echo "❌ Dependency scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Run secret scan with Gitleaks + run: | + GITLEAKS_VERSION=8.24.2 + curl -sSL -o gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + tar -xzf gitleaks.tar.gz gitleaks + sudo mv gitleaks /usr/local/bin/gitleaks + gitleaks detect \ + --source . \ + --redact \ + --verbose \ + --report-format json \ + --report-path security-reports/gitleaks-result.json \ + --exit-code 0 + + - name: Generate Secret Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + json_path = pathlib.Path("security-reports/gitleaks-result.json") + html_path = pathlib.Path("security-reports/gitleaks-result.html") + title = "Secret Scan (Gitleaks) - Result Service" + findings = [] + error = None + try: + text = json_path.read_text(encoding="utf-8").strip() + data = json.loads(text) if text else [] + if isinstance(data, list): + findings = data + elif isinstance(data, dict): + findings = data.get("findings") or data.get("results") or [] + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source JSON file: {html.escape(str(json_path))}

" + ] + if error: + parts.append(f"

Could not parse Gitleaks report: {html.escape(error)}

") + elif not findings: + parts.append("

No secrets were reported.

") + else: + parts.append(f"

Total findings: {len(findings)}

") + parts.append("") + for item in findings: + rule = item.get("RuleID") or item.get("rule") or "" + desc = item.get("Description") or item.get("description") or "" + file = item.get("File") or item.get("file") or "" + line = item.get("StartLine") or item.get("line") or "" + commit = item.get("Commit") or item.get("commit") or "" + parts.append("" + "".join(f"" for x in (rule, desc, file, line, commit)) + "") + parts.append("
RuleDescriptionFileLineCommit
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Secret Scan Summary + if: always() + run: | + { + echo "## Secret Scan (Gitleaks) - Result Service" + echo "" + if [ -f security-reports/gitleaks-result.json ]; then + echo "✅ Secret scan completed." + echo "" + echo "**Report artifact files:** gitleaks-result.json, gitleaks-result.html" + else + echo "❌ Secret scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Set version tag + run: | + if [[ "${GITHUB_REF##*/}" == "main" ]]; then + echo "TAG=v2" >> $GITHUB_ENV + else + echo "TAG=v1" >> $GITHUB_ENV + fi + + - name: Build Docker image + run: | + REGISTRY=${{ steps.login-ecr.outputs.registry }}/bq-eks-repo-dev + docker build -t result-service:${TAG} -t $REGISTRY:result-${TAG} ./result + + - name: Security scan with Trivy + uses: aquasecurity/trivy-action@v0.20.0 + with: + scan-type: image + image-ref: result-service:${{ env.TAG }} + severity: HIGH,CRITICAL + ignore-unfixed: true + exit-code: '0' + format: sarif + output: security-reports/trivy-image-result.sarif + + - name: Generate Container Image Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/trivy-image-result.sarif") + html_path = pathlib.Path("security-reports/trivy-image-result.html") + title = "Container Image Scan (Trivy Image) - Result Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Container Image Scan Summary + if: always() + run: | + { + echo "## Container Image Scan (Trivy Image) - Result Service" + echo "" + if [ -f security-reports/trivy-image-result.sarif ]; then + echo "✅ Container image scan completed." + echo "" + echo "**Report artifact files:** trivy-image-result.sarif, trivy-image-result.html" + else + echo "❌ Container image scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Run DAST scan with OWASP ZAP + continue-on-error: true + run: | + docker network create result-dast-net || true + docker rm -f db-service result-dast || true + docker run -d --name db-service --network result-dast-net \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_DB=postgres \ + postgres:15-alpine + docker run -d --name result-dast --network result-dast-net -p 8081:80 result-service:${TAG} + sleep 30 + curl --fail --retry 10 --retry-delay 5 http://localhost:8081/ + + mkdir -p zap-reports + chmod 777 zap-reports + + docker run --rm \ + --user root \ + --network host \ + -v "${PWD}/zap-reports:/zap/wrk/:rw" \ + ghcr.io/zaproxy/zaproxy:stable \ + zap-baseline.py \ + -t http://localhost:8081 \ + -I \ + -r result-zap-report.html + + mkdir -p security-reports + cp zap-reports/result-zap-report.html security-reports/result-zap-report.html || true + echo "=== zap-reports ===" + ls -lah zap-reports || true + echo "=== zap-reports ===" + ls -lah zap-reports || true + echo "=== security-reports ===" + ls -lah security-reports || true || true + + - name: Verify ZAP Report + if: always() + run: | + echo "=== zap-reports ===" + ls -lah zap-reports || true + echo "=== security-reports ===" + ls -lah security-reports || true + + - name: Publish DAST Summary + if: always() + run: | + { + echo "## DAST (OWASP ZAP) - Result Service" + echo "" + if [ -f security-reports/result-zap-report.html ]; then + echo "✅ DAST scan completed and HTML report was generated." + echo "" + echo "**Report artifact file:** result-zap-report.html" + else + echo "❌ DAST report was not generated." + echo "" + echo "Please check the OWASP ZAP logs above." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Publish Consolidated Security Summary + if: always() + run: | + { + echo "# DevSecOps Security Reports - Result Service" + echo "" + echo "| Security Control | Report File |" + echo "|---|---|" + echo "| SAST (Semgrep) | semgrep-result.sarif, semgrep-result.html |" + echo "| SCA / Dependency Scan (Trivy FS) | trivy-fs-result.sarif, trivy-fs-result.html |" + echo "| Secret Scan (Gitleaks) | gitleaks-result.json, gitleaks-result.html |" + echo "| Container Image Scan (Trivy Image) | trivy-image-result.sarif, trivy-image-result.html |" + echo "| DAST (OWASP ZAP) | result-zap-report.html |" + echo "" + echo "Human-readable HTML reports and machine-readable SARIF/JSON reports are uploaded as the **security-reports-result** workflow artifact." + echo "" + echo "## Files generated" + echo "" + ls -lah security-reports || true + echo "" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload Security Reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: security-reports-result + path: security-reports/ + if-no-files-found: warn + + - name: Push image to ECR + run: | + REGISTRY=${{ steps.login-ecr.outputs.registry }}/bq-eks-repo-dev + docker push $REGISTRY:result-${TAG} + + - name: Configure kubectl + run: | + aws eks update-kubeconfig \ + --region ap-southeast-1 \ + --name bq-eks-cluster-dev + + - name: Deploy application + run: | + kubectl apply -f k8s-specifications/ + + - name: Update deployment + run: | + kubectl set image deployment/result \ + result=${{ steps.login-ecr.outputs.registry }}/bq-eks-repo-dev:result-${TAG} + + - name: Wait for rollout + run: | + kubectl rollout status deployment/result \ No newline at end of file From f74dc840f466a1b62b2a405c2fab1b447947de54 Mon Sep 17 00:00:00 2001 From: kannanmadhu Date: Wed, 1 Jul 2026 06:57:24 +0800 Subject: [PATCH 22/31] Pushing workflow files to Dev env --- .../call-docker-build-result-dev.yaml | 505 ++++++++++++++++++ .../workflows/call-docker-build-vote-dev.yaml | 502 +++++++++++++++++ .../call-docker-build-worker-dev.yaml | 460 ++++++++++++++++ 3 files changed, 1467 insertions(+) create mode 100644 .github/workflows/call-docker-build-result-dev.yaml create mode 100644 .github/workflows/call-docker-build-vote-dev.yaml create mode 100644 .github/workflows/call-docker-build-worker-dev.yaml diff --git a/.github/workflows/call-docker-build-result-dev.yaml b/.github/workflows/call-docker-build-result-dev.yaml new file mode 100644 index 0000000000..479e42534c --- /dev/null +++ b/.github/workflows/call-docker-build-result-dev.yaml @@ -0,0 +1,505 @@ +name: Build Result Service + +on: + # workflow_dispatch: + push: + branches: + - feature/madhu-sample-dev + paths: + - 'result/**' + - '.github/workflows/call-docker-build-result-dev.yaml' + # pull_request: + branches: + - feature/madhu-sample-prod + paths: + - 'result/**' + - '.github/workflows/call-docker-build-result-dev.yaml' + +jobs: + build-scan-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Prepare security report directory + run: | + mkdir -p security-reports + chmod 777 security-reports + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-southeast-1 + + - name: Run SAST scan with Semgrep + run: | + python -m pip install --upgrade pip + python -m pip install semgrep + semgrep scan \ + --config p/ci \ + --severity ERROR \ + --exclude result/views/angular.min.js \ + --exclude result/views/socket.io.js \ + --sarif \ + --output security-reports/semgrep-result.sarif \ + . || true + + - name: Generate SAST HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/semgrep-result.sarif") + html_path = pathlib.Path("security-reports/semgrep-result.html") + title = "SAST (Semgrep) - Result Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish SAST Summary + if: always() + run: | + { + echo "## SAST (Semgrep) - Result Service" + echo "" + if [ -f security-reports/semgrep-result.sarif ]; then + echo "✅ SAST scan completed." + echo "" + echo "**Report artifact files:** semgrep-result.sarif, semgrep-result.html" + else + echo "❌ SAST report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Run dependency vulnerability scan with Trivy + uses: aquasecurity/trivy-action@v0.20.0 + with: + scan-type: fs + scan-ref: ./result + vuln-type: library + severity: HIGH,CRITICAL + ignore-unfixed: true + exit-code: '0' + format: sarif + output: security-reports/trivy-fs-result.sarif + + - name: Generate Dependency Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/trivy-fs-result.sarif") + html_path = pathlib.Path("security-reports/trivy-fs-result.html") + title = "SCA / Dependency Scan (Trivy FS) - Result Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Dependency Scan Summary + if: always() + run: | + { + echo "## SCA / Dependency Vulnerability Scan (Trivy FS) - Result Service" + echo "" + if [ -f security-reports/trivy-fs-result.sarif ]; then + echo "✅ Dependency vulnerability scan completed." + echo "" + echo "**Report artifact files:** trivy-fs-result.sarif, trivy-fs-result.html" + else + echo "❌ Dependency scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Run secret scan with Gitleaks + run: | + GITLEAKS_VERSION=8.24.2 + curl -sSL -o gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + tar -xzf gitleaks.tar.gz gitleaks + sudo mv gitleaks /usr/local/bin/gitleaks + gitleaks detect \ + --source . \ + --redact \ + --verbose \ + --report-format json \ + --report-path security-reports/gitleaks-result.json \ + --exit-code 0 + + - name: Generate Secret Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + json_path = pathlib.Path("security-reports/gitleaks-result.json") + html_path = pathlib.Path("security-reports/gitleaks-result.html") + title = "Secret Scan (Gitleaks) - Result Service" + findings = [] + error = None + try: + text = json_path.read_text(encoding="utf-8").strip() + data = json.loads(text) if text else [] + if isinstance(data, list): + findings = data + elif isinstance(data, dict): + findings = data.get("findings") or data.get("results") or [] + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source JSON file: {html.escape(str(json_path))}

" + ] + if error: + parts.append(f"

Could not parse Gitleaks report: {html.escape(error)}

") + elif not findings: + parts.append("

No secrets were reported.

") + else: + parts.append(f"

Total findings: {len(findings)}

") + parts.append("") + for item in findings: + rule = item.get("RuleID") or item.get("rule") or "" + desc = item.get("Description") or item.get("description") or "" + file = item.get("File") or item.get("file") or "" + line = item.get("StartLine") or item.get("line") or "" + commit = item.get("Commit") or item.get("commit") or "" + parts.append("" + "".join(f"" for x in (rule, desc, file, line, commit)) + "") + parts.append("
RuleDescriptionFileLineCommit
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Secret Scan Summary + if: always() + run: | + { + echo "## Secret Scan (Gitleaks) - Result Service" + echo "" + if [ -f security-reports/gitleaks-result.json ]; then + echo "✅ Secret scan completed." + echo "" + echo "**Report artifact files:** gitleaks-result.json, gitleaks-result.html" + else + echo "❌ Secret scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Set version tag + run: | + if [[ "${GITHUB_REF##*/}" == "feature/madhu-sample-prod" ]]; then + echo "TAG=v2" >> $GITHUB_ENV + else + echo "TAG=v1" >> $GITHUB_ENV + fi + + - name: Build Docker image + run: | + REGISTRY=${{ steps.login-ecr.outputs.registry }}/bq-eks-repo-dev + docker build -t result-service:${TAG} -t $REGISTRY:result-${TAG} ./result + + - name: Security scan with Trivy + uses: aquasecurity/trivy-action@v0.20.0 + with: + scan-type: image + image-ref: result-service:${{ env.TAG }} + severity: HIGH,CRITICAL + ignore-unfixed: true + exit-code: '0' + format: sarif + output: security-reports/trivy-image-result.sarif + + - name: Generate Container Image Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/trivy-image-result.sarif") + html_path = pathlib.Path("security-reports/trivy-image-result.html") + title = "Container Image Scan (Trivy Image) - Result Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Container Image Scan Summary + if: always() + run: | + { + echo "## Container Image Scan (Trivy Image) - Result Service" + echo "" + if [ -f security-reports/trivy-image-result.sarif ]; then + echo "✅ Container image scan completed." + echo "" + echo "**Report artifact files:** trivy-image-result.sarif, trivy-image-result.html" + else + echo "❌ Container image scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Run DAST scan with OWASP ZAP + continue-on-error: true + run: | + docker network create result-dast-net || true + docker rm -f db-service result-dast || true + docker run -d --name db-service --network result-dast-net \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_DB=postgres \ + postgres:15-alpine + docker run -d --name result-dast --network result-dast-net -p 8081:80 result-service:${TAG} + sleep 30 + curl --fail --retry 10 --retry-delay 5 http://localhost:8081/ + + mkdir -p zap-reports + chmod 777 zap-reports + + docker run --rm \ + --user root \ + --network host \ + -v "${PWD}/zap-reports:/zap/wrk/:rw" \ + ghcr.io/zaproxy/zaproxy:stable \ + zap-baseline.py \ + -t http://localhost:8081 \ + -I \ + -r result-zap-report.html + + mkdir -p security-reports + cp zap-reports/result-zap-report.html security-reports/result-zap-report.html || true + echo "=== zap-reports ===" + ls -lah zap-reports || true + echo "=== zap-reports ===" + ls -lah zap-reports || true + echo "=== security-reports ===" + ls -lah security-reports || true || true + + - name: Verify ZAP Report + if: always() + run: | + echo "=== zap-reports ===" + ls -lah zap-reports || true + echo "=== security-reports ===" + ls -lah security-reports || true + + - name: Publish DAST Summary + if: always() + run: | + { + echo "## DAST (OWASP ZAP) - Result Service" + echo "" + if [ -f security-reports/result-zap-report.html ]; then + echo "✅ DAST scan completed and HTML report was generated." + echo "" + echo "**Report artifact file:** result-zap-report.html" + else + echo "❌ DAST report was not generated." + echo "" + echo "Please check the OWASP ZAP logs above." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Publish Consolidated Security Summary + if: always() + run: | + { + echo "# DevSecOps Security Reports - Result Service" + echo "" + echo "| Security Control | Report File |" + echo "|---|---|" + echo "| SAST (Semgrep) | semgrep-result.sarif, semgrep-result.html |" + echo "| SCA / Dependency Scan (Trivy FS) | trivy-fs-result.sarif, trivy-fs-result.html |" + echo "| Secret Scan (Gitleaks) | gitleaks-result.json, gitleaks-result.html |" + echo "| Container Image Scan (Trivy Image) | trivy-image-result.sarif, trivy-image-result.html |" + echo "| DAST (OWASP ZAP) | result-zap-report.html |" + echo "" + echo "Human-readable HTML reports and machine-readable SARIF/JSON reports are uploaded as the **security-reports-result** workflow artifact." + echo "" + echo "## Files generated" + echo "" + ls -lah security-reports || true + echo "" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload Security Reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: security-reports-result + path: security-reports/ + if-no-files-found: warn + + - name: Push image to ECR + run: | + REGISTRY=${{ steps.login-ecr.outputs.registry }}/bq-eks-repo-dev + docker push $REGISTRY:result-${TAG} + + - name: Configure kubectl + run: | + aws eks update-kubeconfig \ + --region ap-southeast-1 \ + --name bq-eks-cluster-dev + + - name: Deploy application + run: | + kubectl apply -f k8s-specifications/ + + - name: Update deployment + run: | + kubectl set image deployment/result \ + result=${{ steps.login-ecr.outputs.registry }}/bq-eks-repo-dev:result-${TAG} + + - name: Wait for rollout + run: | + kubectl rollout status deployment/result + diff --git a/.github/workflows/call-docker-build-vote-dev.yaml b/.github/workflows/call-docker-build-vote-dev.yaml new file mode 100644 index 0000000000..3dc319e7ff --- /dev/null +++ b/.github/workflows/call-docker-build-vote-dev.yaml @@ -0,0 +1,502 @@ +name: Build Vote Service + +on: + # workflow_dispatch: + push: + branches: + - feature/madhu-sample-dev + paths: + - 'vote/**' + - '.github/workflows/call-docker-build-vote-dev.yaml' + pull_request: + branches: + - main + - development + paths: + - 'vote/**' + - '.github/workflows/call-docker-build-vote.yaml' + +jobs: + build-scan-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Prepare security report directory + run: | + mkdir -p security-reports + chmod 777 security-reports + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-southeast-1 + + - name: Run SAST scan with Semgrep + run: | + python -m pip install --upgrade pip + python -m pip install semgrep + semgrep scan \ + --config p/ci \ + --severity ERROR \ + --exclude result/views/angular.min.js \ + --exclude result/views/socket.io.js \ + --sarif \ + --output security-reports/semgrep-vote.sarif \ + . || true + + - name: Generate SAST HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/semgrep-vote.sarif") + html_path = pathlib.Path("security-reports/semgrep-vote.html") + title = "SAST (Semgrep) - Vote Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish SAST Summary + if: always() + run: | + { + echo "## SAST (Semgrep) - Vote Service" + echo "" + if [ -f security-reports/semgrep-vote.sarif ]; then + echo "✅ SAST scan completed." + echo "" + echo "**Report artifact files:** semgrep-vote.sarif, semgrep-vote.html" + else + echo "❌ SAST report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Run dependency vulnerability scan with Trivy + uses: aquasecurity/trivy-action@v0.20.0 + with: + scan-type: fs + scan-ref: ./vote + vuln-type: library + severity: HIGH,CRITICAL + ignore-unfixed: true + exit-code: '0' + format: sarif + output: security-reports/trivy-fs-vote.sarif + + - name: Generate Dependency Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/trivy-fs-vote.sarif") + html_path = pathlib.Path("security-reports/trivy-fs-vote.html") + title = "SCA / Dependency Scan (Trivy FS) - Vote Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Dependency Scan Summary + if: always() + run: | + { + echo "## SCA / Dependency Vulnerability Scan (Trivy FS) - Vote Service" + echo "" + if [ -f security-reports/trivy-fs-vote.sarif ]; then + echo "✅ Dependency vulnerability scan completed." + echo "" + echo "**Report artifact files:** trivy-fs-vote.sarif, trivy-fs-vote.html" + else + echo "❌ Dependency scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Run secret scan with Gitleaks + run: | + GITLEAKS_VERSION=8.24.2 + curl -sSL -o gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + tar -xzf gitleaks.tar.gz gitleaks + sudo mv gitleaks /usr/local/bin/gitleaks + gitleaks detect \ + --source . \ + --redact \ + --verbose \ + --report-format json \ + --report-path security-reports/gitleaks-vote.json \ + --exit-code 0 + + - name: Generate Secret Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + json_path = pathlib.Path("security-reports/gitleaks-vote.json") + html_path = pathlib.Path("security-reports/gitleaks-vote.html") + title = "Secret Scan (Gitleaks) - Vote Service" + findings = [] + error = None + try: + text = json_path.read_text(encoding="utf-8").strip() + data = json.loads(text) if text else [] + if isinstance(data, list): + findings = data + elif isinstance(data, dict): + findings = data.get("findings") or data.get("results") or [] + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source JSON file: {html.escape(str(json_path))}

" + ] + if error: + parts.append(f"

Could not parse Gitleaks report: {html.escape(error)}

") + elif not findings: + parts.append("

No secrets were reported.

") + else: + parts.append(f"

Total findings: {len(findings)}

") + parts.append("") + for item in findings: + rule = item.get("RuleID") or item.get("rule") or "" + desc = item.get("Description") or item.get("description") or "" + file = item.get("File") or item.get("file") or "" + line = item.get("StartLine") or item.get("line") or "" + commit = item.get("Commit") or item.get("commit") or "" + parts.append("" + "".join(f"" for x in (rule, desc, file, line, commit)) + "") + parts.append("
RuleDescriptionFileLineCommit
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Secret Scan Summary + if: always() + run: | + { + echo "## Secret Scan (Gitleaks) - Vote Service" + echo "" + if [ -f security-reports/gitleaks-vote.json ]; then + echo "✅ Secret scan completed." + echo "" + echo "**Report artifact files:** gitleaks-vote.json, gitleaks-vote.html" + else + echo "❌ Secret scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Set version tag + run: | + if [[ "${GITHUB_REF##*/}" == "feature/madhu-sample-prod" ]]; then + echo "TAG=v2" >> $GITHUB_ENV + else + echo "TAG=v1" >> $GITHUB_ENV + fi + + - name: Build Docker image + run: | + REGISTRY=${{ steps.login-ecr.outputs.registry }}/bq-eks-repo-dev + docker build -t vote-service:${TAG} ./vote + docker tag vote-service:${TAG} $REGISTRY:vote-${TAG} + + - name: Security scan with Trivy + uses: aquasecurity/trivy-action@v0.20.0 + with: + scan-type: image + image-ref: vote-service:${{ env.TAG }} + severity: HIGH,CRITICAL + ignore-unfixed: true + exit-code: '0' + format: sarif + output: security-reports/trivy-image-vote.sarif + + - name: Generate Container Image Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/trivy-image-vote.sarif") + html_path = pathlib.Path("security-reports/trivy-image-vote.html") + title = "Container Image Scan (Trivy Image) - Vote Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Container Image Scan Summary + if: always() + run: | + { + echo "## Container Image Scan (Trivy Image) - Vote Service" + echo "" + if [ -f security-reports/trivy-image-vote.sarif ]; then + echo "✅ Container image scan completed." + echo "" + echo "**Report artifact files:** trivy-image-vote.sarif, trivy-image-vote.html" + else + echo "❌ Container image scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Run DAST scan with OWASP ZAP + continue-on-error: true + run: | + docker network create vote-dast-net || true + docker rm -f redis vote-dast || true + docker run -d --name redis --network vote-dast-net redis:7-alpine + docker run -d --name vote-dast --network vote-dast-net -p 8080:80 vote-service:${TAG} + sleep 20 + curl --fail --retry 10 --retry-delay 5 http://localhost:8080/ + + mkdir -p zap-reports + chmod 777 zap-reports + + docker run --rm \ + --user root \ + --network host \ + -v "${PWD}/zap-reports:/zap/wrk/:rw" \ + ghcr.io/zaproxy/zaproxy:stable \ + zap-baseline.py \ + -t http://localhost:8080 \ + -I \ + -r vote-zap-report.html + + mkdir -p security-reports + cp zap-reports/vote-zap-report.html security-reports/vote-zap-report.html || true + echo "=== zap-reports ===" + ls -lah zap-reports || true + echo "=== zap-reports ===" + ls -lah zap-reports || true + echo "=== security-reports ===" + ls -lah security-reports || true || true + + - name: Verify ZAP Report + if: always() + run: | + echo "=== zap-reports ===" + ls -lah zap-reports || true + echo "=== security-reports ===" + ls -lah security-reports || true + + - name: Publish DAST Summary + if: always() + run: | + { + echo "## DAST (OWASP ZAP) - Vote Service" + echo "" + if [ -f security-reports/vote-zap-report.html ]; then + echo "✅ DAST scan completed and HTML report was generated." + echo "" + echo "**Report artifact file:** vote-zap-report.html" + else + echo "❌ DAST report was not generated." + echo "" + echo "Please check the OWASP ZAP logs above." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Publish Consolidated Security Summary + if: always() + run: | + { + echo "# DevSecOps Security Reports - Vote Service" + echo "" + echo "| Security Control | Report File |" + echo "|---|---|" + echo "| SAST (Semgrep) | semgrep-vote.sarif, semgrep-vote.html |" + echo "| SCA / Dependency Scan (Trivy FS) | trivy-fs-vote.sarif, trivy-fs-vote.html |" + echo "| Secret Scan (Gitleaks) | gitleaks-vote.json, gitleaks-vote.html |" + echo "| Container Image Scan (Trivy Image) | trivy-image-vote.sarif, trivy-image-vote.html |" + echo "| DAST (OWASP ZAP) | vote-zap-report.html |" + echo "" + echo "Human-readable HTML reports and machine-readable SARIF/JSON reports are uploaded as the **security-reports-vote** workflow artifact." + echo "" + echo "## Files generated" + echo "" + ls -lah security-reports || true + echo "" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload Security Reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: security-reports-vote + path: security-reports/ + if-no-files-found: warn + + - name: Push image to ECR + run: | + REGISTRY=${{ steps.login-ecr.outputs.registry }}/bq-eks-repo-dev + docker push $REGISTRY:vote-${TAG} + + - name: Configure kubectl + run: | + aws eks update-kubeconfig \ + --region ap-southeast-1 \ + --name bq-eks-cluster-dev + + - name: Deploy application + run: | + kubectl apply -f k8s-specifications/ + + - name: Update deployment + run: | + kubectl set image deployment/vote \ + vote=${{ steps.login-ecr.outputs.registry }}/bq-eks-repo-dev:vote-${TAG} + + - name: Wait for rollout + run: | + kubectl rollout status deployment/vote \ No newline at end of file diff --git a/.github/workflows/call-docker-build-worker-dev.yaml b/.github/workflows/call-docker-build-worker-dev.yaml new file mode 100644 index 0000000000..11b2512fb4 --- /dev/null +++ b/.github/workflows/call-docker-build-worker-dev.yaml @@ -0,0 +1,460 @@ +name: Build Worker Service + +on: + # workflow_dispatch: + push: + branches: + - feature/madhu-sample-prod + paths: + - 'worker/**' + - '.github/workflows/call-docker-build-worker-dev.yaml' + pull_request: + branches: + - main + - development + paths: + - 'worker/**' + - '.github/workflows/call-docker-build-worker.yaml' + +jobs: + build-scan-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Prepare security report directory + run: | + mkdir -p security-reports + chmod 777 security-reports + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-southeast-1 + + - name: Run SAST scan with Semgrep + run: | + python -m pip install --upgrade pip + python -m pip install semgrep + semgrep scan \ + --config p/ci \ + --severity ERROR \ + --exclude result/views/angular.min.js \ + --exclude result/views/socket.io.js \ + --sarif \ + --output security-reports/semgrep-worker.sarif \ + . || true + + - name: Generate SAST HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/semgrep-worker.sarif") + html_path = pathlib.Path("security-reports/semgrep-worker.html") + title = "SAST (Semgrep) - Worker Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish SAST Summary + if: always() + run: | + { + echo "## SAST (Semgrep) - Worker Service" + echo "" + if [ -f security-reports/semgrep-worker.sarif ]; then + echo "✅ SAST scan completed." + echo "" + echo "**Report artifact files:** semgrep-worker.sarif, semgrep-worker.html" + else + echo "❌ SAST report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Run dependency vulnerability scan with Trivy + uses: aquasecurity/trivy-action@v0.20.0 + with: + scan-type: fs + scan-ref: ./worker + vuln-type: library + severity: HIGH,CRITICAL + ignore-unfixed: true + exit-code: '0' + format: sarif + output: security-reports/trivy-fs-worker.sarif + + - name: Generate Dependency Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/trivy-fs-worker.sarif") + html_path = pathlib.Path("security-reports/trivy-fs-worker.html") + title = "SCA / Dependency Scan (Trivy FS) - Worker Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Dependency Scan Summary + if: always() + run: | + { + echo "## SCA / Dependency Vulnerability Scan (Trivy FS) - Worker Service" + echo "" + if [ -f security-reports/trivy-fs-worker.sarif ]; then + echo "✅ Dependency vulnerability scan completed." + echo "" + echo "**Report artifact files:** trivy-fs-worker.sarif, trivy-fs-worker.html" + else + echo "❌ Dependency scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Run secret scan with Gitleaks + run: | + GITLEAKS_VERSION=8.24.2 + curl -sSL -o gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + tar -xzf gitleaks.tar.gz gitleaks + sudo mv gitleaks /usr/local/bin/gitleaks + gitleaks detect \ + --source . \ + --redact \ + --verbose \ + --report-format json \ + --report-path security-reports/gitleaks-worker.json \ + --exit-code 0 + + - name: Generate Secret Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + json_path = pathlib.Path("security-reports/gitleaks-worker.json") + html_path = pathlib.Path("security-reports/gitleaks-worker.html") + title = "Secret Scan (Gitleaks) - Worker Service" + findings = [] + error = None + try: + text = json_path.read_text(encoding="utf-8").strip() + data = json.loads(text) if text else [] + if isinstance(data, list): + findings = data + elif isinstance(data, dict): + findings = data.get("findings") or data.get("results") or [] + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source JSON file: {html.escape(str(json_path))}

" + ] + if error: + parts.append(f"

Could not parse Gitleaks report: {html.escape(error)}

") + elif not findings: + parts.append("

No secrets were reported.

") + else: + parts.append(f"

Total findings: {len(findings)}

") + parts.append("") + for item in findings: + rule = item.get("RuleID") or item.get("rule") or "" + desc = item.get("Description") or item.get("description") or "" + file = item.get("File") or item.get("file") or "" + line = item.get("StartLine") or item.get("line") or "" + commit = item.get("Commit") or item.get("commit") or "" + parts.append("" + "".join(f"" for x in (rule, desc, file, line, commit)) + "") + parts.append("
RuleDescriptionFileLineCommit
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Secret Scan Summary + if: always() + run: | + { + echo "## Secret Scan (Gitleaks) - Worker Service" + echo "" + if [ -f security-reports/gitleaks-worker.json ]; then + echo "✅ Secret scan completed." + echo "" + echo "**Report artifact files:** gitleaks-worker.json, gitleaks-worker.html" + else + echo "❌ Secret scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Set version tag + run: | + if [[ "${GITHUB_REF##*/}" == "feature/madhu-sample-prod" ]]; then + echo "TAG=v2" >> $GITHUB_ENV + else + echo "TAG=v1" >> $GITHUB_ENV + fi + + - name: Build Docker image + run: | + REGISTRY=${{ steps.login-ecr.outputs.registry }}/bq-eks-repo-dev + docker build -t worker-service:${TAG} -t $REGISTRY:worker-${TAG} ./worker + + - name: Security scan with Trivy + uses: aquasecurity/trivy-action@v0.20.0 + with: + scan-type: image + image-ref: worker-service:${{ env.TAG }} + severity: HIGH,CRITICAL + ignore-unfixed: true + exit-code: '0' + format: sarif + output: security-reports/trivy-image-worker.sarif + + - name: Generate Container Image Scan HTML Report + if: always() + run: | + python - <<'PY' + import json, html, pathlib, datetime + sarif_path = pathlib.Path("security-reports/trivy-image-worker.sarif") + html_path = pathlib.Path("security-reports/trivy-image-worker.html") + title = "Container Image Scan (Trivy Image) - Worker Service" + rows = [] + error = None + try: + data = json.loads(sarif_path.read_text(encoding="utf-8")) + runs = data.get("runs", []) + for run in runs: + rules = {} + for rule in run.get("tool", {}).get("driver", {}).get("rules", []): + rid = rule.get("id", "") + rules[rid] = rule + for result in run.get("results", []): + rid = result.get("ruleId", "") + rule = rules.get(rid, {}) + level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") + msg = result.get("message", {}).get("text", "") + loc = "" + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region", {}) + line = region.get("startLine", "") + loc = f"{uri}:{line}" if line else uri + rows.append((level, rid, msg, loc)) + except Exception as exc: + error = str(exc) + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + parts = [ + "", + f"{html.escape(title)}", + "", + "", + f"

{html.escape(title)}

", + f"

Generated at {html.escape(now)}

", + f"

Source SARIF file: {html.escape(str(sarif_path))}

" + ] + if error: + parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") + elif not rows: + parts.append("

No findings were reported.

") + else: + parts.append(f"

Total findings: {len(rows)}

") + parts.append("") + for level, rid, msg, loc in rows: + parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") + parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") + parts.append("") + html_path.write_text("\n".join(parts), encoding="utf-8") + print(f"Generated {html_path}") + PY + + - name: Publish Container Image Scan Summary + if: always() + run: | + { + echo "## Container Image Scan (Trivy Image) - Worker Service" + echo "" + if [ -f security-reports/trivy-image-worker.sarif ]; then + echo "✅ Container image scan completed." + echo "" + echo "**Report artifact files:** trivy-image-worker.sarif, trivy-image-worker.html" + else + echo "❌ Container image scan report was not generated." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: DAST note for worker service + run: | + echo "Worker is a background service without an HTTP endpoint, so OWASP ZAP DAST is not applicable to this service." + echo "DAST is implemented for the vote and result web services." + + - name: Publish DAST Summary + if: always() + run: | + { + echo "## DAST (OWASP ZAP) - Worker Service" + echo "" + echo "ℹ️ DAST is not applicable because the worker is a background service without an HTTP endpoint." + echo "" + echo "DAST is implemented for the vote and result web services." + } >> "$GITHUB_STEP_SUMMARY" + + - name: Publish Consolidated Security Summary + if: always() + run: | + { + echo "# DevSecOps Security Reports - Worker Service" + echo "" + echo "| Security Control | Report File |" + echo "|---|---|" + echo "| SAST (Semgrep) | semgrep-worker.sarif, semgrep-worker.html |" + echo "| SCA / Dependency Scan (Trivy FS) | trivy-fs-worker.sarif, trivy-fs-worker.html |" + echo "| Secret Scan (Gitleaks) | gitleaks-worker.json, gitleaks-worker.html |" + echo "| Container Image Scan (Trivy Image) | trivy-image-worker.sarif, trivy-image-worker.html |" + echo "| DAST (OWASP ZAP) | Not applicable - background service |" + echo "" + echo "Human-readable HTML reports and machine-readable SARIF/JSON reports are uploaded as the **security-reports-worker** workflow artifact." + echo "" + echo "## Files generated" + echo "" + ls -lah security-reports || true + echo "" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload Security Reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: security-reports-worker + path: security-reports/ + if-no-files-found: warn + + - name: Push image to ECR + run: | + REGISTRY=${{ steps.login-ecr.outputs.registry }}/bq-eks-repo-dev + docker push $REGISTRY:worker-${TAG} + + - name: Configure kubectl + run: | + aws eks update-kubeconfig \ + --region ap-southeast-1 \ + --name bq-eks-cluster-dev + + - name: Deploy application + run: | + kubectl apply -f k8s-specifications/ + + - name: Update deployment + run: | + kubectl set image deployment/worker \ + worker=${{ steps.login-ecr.outputs.registry }}/bq-eks-repo-dev:worker-${TAG} + + - name: Wait for rollout + run: | + kubectl rollout status deployment/worker \ No newline at end of file From 6be3ce24062594062108eb3f6ed95fe2270d1b48 Mon Sep 17 00:00:00 2001 From: kannanmadhu Date: Wed, 1 Jul 2026 06:59:26 +0800 Subject: [PATCH 23/31] Pushing workflow files to Dev env --- .../call-docker-build-result-dev-oidc.yaml | 512 ------------------ .../call-docker-build-result-dev.yaml | 14 +- .../workflows/call-docker-build-result.yaml | 494 ----------------- ...l-docker-build-result.yaml:Zone.Identifier | Bin 25 -> 0 bytes .../workflows/call-docker-build-vote-dev.yaml | 16 +- .github/workflows/call-docker-build-vote.yaml | 491 ----------------- ...all-docker-build-vote.yaml:Zone.Identifier | Bin 25 -> 0 bytes .../call-docker-build-worker-dev.yaml | 16 +- .../workflows/call-docker-build-worker.yaml | 449 --------------- ...l-docker-build-worker.yaml:Zone.Identifier | Bin 25 -> 0 bytes 10 files changed, 23 insertions(+), 1969 deletions(-) delete mode 100644 .github/workflows/call-docker-build-result-dev-oidc.yaml delete mode 100644 .github/workflows/call-docker-build-result.yaml delete mode 100644 .github/workflows/call-docker-build-result.yaml:Zone.Identifier delete mode 100644 .github/workflows/call-docker-build-vote.yaml delete mode 100644 .github/workflows/call-docker-build-vote.yaml:Zone.Identifier delete mode 100644 .github/workflows/call-docker-build-worker.yaml delete mode 100644 .github/workflows/call-docker-build-worker.yaml:Zone.Identifier diff --git a/.github/workflows/call-docker-build-result-dev-oidc.yaml b/.github/workflows/call-docker-build-result-dev-oidc.yaml deleted file mode 100644 index e9d2a75c1c..0000000000 --- a/.github/workflows/call-docker-build-result-dev-oidc.yaml +++ /dev/null @@ -1,512 +0,0 @@ -name: Build Result Service -# Trigger OIDC test commit - -on: - workflow_dispatch: - push: - branches: - - development - paths: - - 'result/**' - - '.github/workflows/call-docker-build-result-dev-oidc.yaml' -# pull_request: -# branches: -# - main -# - development -# paths: -# - 'result/**' -# - '.github/workflows/call-docker-build-result-dev-oidc.yaml' - -permissions: - id-token: write - contents: read - -jobs: - build-scan-push: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Prepare security report directory - run: | - mkdir -p security-reports - chmod 777 security-reports - - - name: Configure AWS credentials using OIDC - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} - aws-region: ap-southeast-1 - - - name: Verify AWS identity - run: aws sts get-caller-identity - - - name: Run SAST scan with Semgrep - run: | - python -m pip install --upgrade pip - python -m pip install semgrep - semgrep scan \ - --config p/ci \ - --severity ERROR \ - --exclude result/views/angular.min.js \ - --exclude result/views/socket.io.js \ - --sarif \ - --output security-reports/semgrep-result.sarif \ - . || true - - - name: Generate SAST HTML Report - if: always() - run: | - python - <<'PY' - import json, html, pathlib, datetime - sarif_path = pathlib.Path("security-reports/semgrep-result.sarif") - html_path = pathlib.Path("security-reports/semgrep-result.html") - title = "SAST (Semgrep) - Result Service" - rows = [] - error = None - try: - data = json.loads(sarif_path.read_text(encoding="utf-8")) - runs = data.get("runs", []) - for run in runs: - rules = {} - for rule in run.get("tool", {}).get("driver", {}).get("rules", []): - rid = rule.get("id", "") - rules[rid] = rule - for result in run.get("results", []): - rid = result.get("ruleId", "") - rule = rules.get(rid, {}) - level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") - msg = result.get("message", {}).get("text", "") - loc = "" - locations = result.get("locations", []) - if locations: - phys = locations[0].get("physicalLocation", {}) - uri = phys.get("artifactLocation", {}).get("uri", "") - region = phys.get("region", {}) - line = region.get("startLine", "") - loc = f"{uri}:{line}" if line else uri - rows.append((level, rid, msg, loc)) - except Exception as exc: - error = str(exc) - now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") - parts = [ - "", - f"{html.escape(title)}", - "", - "", - f"

{html.escape(title)}

", - f"

Generated at {html.escape(now)}

", - f"

Source SARIF file: {html.escape(str(sarif_path))}

" - ] - if error: - parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") - elif not rows: - parts.append("

No findings were reported.

") - else: - parts.append(f"

Total findings: {len(rows)}

") - parts.append("") - for level, rid, msg, loc in rows: - parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") - parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") - parts.append("") - html_path.write_text("\n".join(parts), encoding="utf-8") - print(f"Generated {html_path}") - PY - - - name: Publish SAST Summary - if: always() - run: | - { - echo "## SAST (Semgrep) - Result Service" - echo "" - if [ -f security-reports/semgrep-result.sarif ]; then - echo "✅ SAST scan completed." - echo "" - echo "**Report artifact files:** semgrep-result.sarif, semgrep-result.html" - else - echo "❌ SAST report was not generated." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Run dependency vulnerability scan with Trivy - uses: aquasecurity/trivy-action@v0.20.0 - with: - scan-type: fs - scan-ref: ./result - vuln-type: library - severity: HIGH,CRITICAL - ignore-unfixed: true - exit-code: '0' - format: sarif - output: security-reports/trivy-fs-result.sarif - - - name: Generate Dependency Scan HTML Report - if: always() - run: | - python - <<'PY' - import json, html, pathlib, datetime - sarif_path = pathlib.Path("security-reports/trivy-fs-result.sarif") - html_path = pathlib.Path("security-reports/trivy-fs-result.html") - title = "SCA / Dependency Scan (Trivy FS) - Result Service" - rows = [] - error = None - try: - data = json.loads(sarif_path.read_text(encoding="utf-8")) - runs = data.get("runs", []) - for run in runs: - rules = {} - for rule in run.get("tool", {}).get("driver", {}).get("rules", []): - rid = rule.get("id", "") - rules[rid] = rule - for result in run.get("results", []): - rid = result.get("ruleId", "") - rule = rules.get(rid, {}) - level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") - msg = result.get("message", {}).get("text", "") - loc = "" - locations = result.get("locations", []) - if locations: - phys = locations[0].get("physicalLocation", {}) - uri = phys.get("artifactLocation", {}).get("uri", "") - region = phys.get("region", {}) - line = region.get("startLine", "") - loc = f"{uri}:{line}" if line else uri - rows.append((level, rid, msg, loc)) - except Exception as exc: - error = str(exc) - now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") - parts = [ - "", - f"{html.escape(title)}", - "", - "", - f"

{html.escape(title)}

", - f"

Generated at {html.escape(now)}

", - f"

Source SARIF file: {html.escape(str(sarif_path))}

" - ] - if error: - parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") - elif not rows: - parts.append("

No findings were reported.

") - else: - parts.append(f"

Total findings: {len(rows)}

") - parts.append("") - for level, rid, msg, loc in rows: - parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") - parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") - parts.append("") - html_path.write_text("\n".join(parts), encoding="utf-8") - print(f"Generated {html_path}") - PY - - - name: Publish Dependency Scan Summary - if: always() - run: | - { - echo "## SCA / Dependency Vulnerability Scan (Trivy FS) - Result Service" - echo "" - if [ -f security-reports/trivy-fs-result.sarif ]; then - echo "✅ Dependency vulnerability scan completed." - echo "" - echo "**Report artifact files:** trivy-fs-result.sarif, trivy-fs-result.html" - else - echo "❌ Dependency scan report was not generated." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Run secret scan with Gitleaks - run: | - GITLEAKS_VERSION=8.24.2 - curl -sSL -o gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" - tar -xzf gitleaks.tar.gz gitleaks - sudo mv gitleaks /usr/local/bin/gitleaks - gitleaks detect \ - --source . \ - --redact \ - --verbose \ - --report-format json \ - --report-path security-reports/gitleaks-result.json \ - --exit-code 0 - - - name: Generate Secret Scan HTML Report - if: always() - run: | - python - <<'PY' - import json, html, pathlib, datetime - json_path = pathlib.Path("security-reports/gitleaks-result.json") - html_path = pathlib.Path("security-reports/gitleaks-result.html") - title = "Secret Scan (Gitleaks) - Result Service" - findings = [] - error = None - try: - text = json_path.read_text(encoding="utf-8").strip() - data = json.loads(text) if text else [] - if isinstance(data, list): - findings = data - elif isinstance(data, dict): - findings = data.get("findings") or data.get("results") or [] - except Exception as exc: - error = str(exc) - now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") - parts = [ - "", - f"{html.escape(title)}", - "", - "", - f"

{html.escape(title)}

", - f"

Generated at {html.escape(now)}

", - f"

Source JSON file: {html.escape(str(json_path))}

" - ] - if error: - parts.append(f"

Could not parse Gitleaks report: {html.escape(error)}

") - elif not findings: - parts.append("

No secrets were reported.

") - else: - parts.append(f"

Total findings: {len(findings)}

") - parts.append("") - for item in findings: - rule = item.get("RuleID") or item.get("rule") or "" - desc = item.get("Description") or item.get("description") or "" - file = item.get("File") or item.get("file") or "" - line = item.get("StartLine") or item.get("line") or "" - commit = item.get("Commit") or item.get("commit") or "" - parts.append("" + "".join(f"" for x in (rule, desc, file, line, commit)) + "") - parts.append("
RuleDescriptionFileLineCommit
{html.escape(str(x))}
") - parts.append("") - html_path.write_text("\n".join(parts), encoding="utf-8") - print(f"Generated {html_path}") - PY - - - name: Publish Secret Scan Summary - if: always() - run: | - { - echo "## Secret Scan (Gitleaks) - Result Service" - echo "" - if [ -f security-reports/gitleaks-result.json ]; then - echo "✅ Secret scan completed." - echo "" - echo "**Report artifact files:** gitleaks-result.json, gitleaks-result.html" - else - echo "❌ Secret scan report was not generated." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Set version tag - run: | - if [[ "${GITHUB_REF##*/}" == "main" ]]; then - echo "TAG=v2" >> $GITHUB_ENV - else - echo "TAG=v1" >> $GITHUB_ENV - fi - - - name: Build Docker image - run: | - REGISTRY=${{ steps.login-ecr.outputs.registry }}/bq-eks-repo-dev - docker build -t result-service:${TAG} -t $REGISTRY:result-${TAG} ./result - - - name: Security scan with Trivy - uses: aquasecurity/trivy-action@v0.20.0 - with: - scan-type: image - image-ref: result-service:${{ env.TAG }} - severity: HIGH,CRITICAL - ignore-unfixed: true - exit-code: '0' - format: sarif - output: security-reports/trivy-image-result.sarif - - - name: Generate Container Image Scan HTML Report - if: always() - run: | - python - <<'PY' - import json, html, pathlib, datetime - sarif_path = pathlib.Path("security-reports/trivy-image-result.sarif") - html_path = pathlib.Path("security-reports/trivy-image-result.html") - title = "Container Image Scan (Trivy Image) - Result Service" - rows = [] - error = None - try: - data = json.loads(sarif_path.read_text(encoding="utf-8")) - runs = data.get("runs", []) - for run in runs: - rules = {} - for rule in run.get("tool", {}).get("driver", {}).get("rules", []): - rid = rule.get("id", "") - rules[rid] = rule - for result in run.get("results", []): - rid = result.get("ruleId", "") - rule = rules.get(rid, {}) - level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") - msg = result.get("message", {}).get("text", "") - loc = "" - locations = result.get("locations", []) - if locations: - phys = locations[0].get("physicalLocation", {}) - uri = phys.get("artifactLocation", {}).get("uri", "") - region = phys.get("region", {}) - line = region.get("startLine", "") - loc = f"{uri}:{line}" if line else uri - rows.append((level, rid, msg, loc)) - except Exception as exc: - error = str(exc) - now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") - parts = [ - "", - f"{html.escape(title)}", - "", - "", - f"

{html.escape(title)}

", - f"

Generated at {html.escape(now)}

", - f"

Source SARIF file: {html.escape(str(sarif_path))}

" - ] - if error: - parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") - elif not rows: - parts.append("

No findings were reported.

") - else: - parts.append(f"

Total findings: {len(rows)}

") - parts.append("") - for level, rid, msg, loc in rows: - parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") - parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") - parts.append("") - html_path.write_text("\n".join(parts), encoding="utf-8") - print(f"Generated {html_path}") - PY - - - name: Publish Container Image Scan Summary - if: always() - run: | - { - echo "## Container Image Scan (Trivy Image) - Result Service" - echo "" - if [ -f security-reports/trivy-image-result.sarif ]; then - echo "✅ Container image scan completed." - echo "" - echo "**Report artifact files:** trivy-image-result.sarif, trivy-image-result.html" - else - echo "❌ Container image scan report was not generated." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Run DAST scan with OWASP ZAP - continue-on-error: true - run: | - docker network create result-dast-net || true - docker rm -f db-service result-dast || true - docker run -d --name db-service --network result-dast-net \ - -e POSTGRES_USER=postgres \ - -e POSTGRES_PASSWORD=postgres \ - -e POSTGRES_DB=postgres \ - postgres:15-alpine - docker run -d --name result-dast --network result-dast-net -p 8081:80 result-service:${TAG} - sleep 30 - curl --fail --retry 10 --retry-delay 5 http://localhost:8081/ - - mkdir -p zap-reports - chmod 777 zap-reports - - docker run --rm \ - --user root \ - --network host \ - -v "${PWD}/zap-reports:/zap/wrk/:rw" \ - ghcr.io/zaproxy/zaproxy:stable \ - zap-baseline.py \ - -t http://localhost:8081 \ - -I \ - -r result-zap-report.html - - mkdir -p security-reports - cp zap-reports/result-zap-report.html security-reports/result-zap-report.html || true - echo "=== zap-reports ===" - ls -lah zap-reports || true - echo "=== zap-reports ===" - ls -lah zap-reports || true - echo "=== security-reports ===" - ls -lah security-reports || true || true - - - name: Verify ZAP Report - if: always() - run: | - echo "=== zap-reports ===" - ls -lah zap-reports || true - echo "=== security-reports ===" - ls -lah security-reports || true - - - name: Publish DAST Summary - if: always() - run: | - { - echo "## DAST (OWASP ZAP) - Result Service" - echo "" - if [ -f security-reports/result-zap-report.html ]; then - echo "✅ DAST scan completed and HTML report was generated." - echo "" - echo "**Report artifact file:** result-zap-report.html" - else - echo "❌ DAST report was not generated." - echo "" - echo "Please check the OWASP ZAP logs above." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Publish Consolidated Security Summary - if: always() - run: | - { - echo "# DevSecOps Security Reports - Result Service" - echo "" - echo "| Security Control | Report File |" - echo "|---|---|" - echo "| SAST (Semgrep) | semgrep-result.sarif, semgrep-result.html |" - echo "| SCA / Dependency Scan (Trivy FS) | trivy-fs-result.sarif, trivy-fs-result.html |" - echo "| Secret Scan (Gitleaks) | gitleaks-result.json, gitleaks-result.html |" - echo "| Container Image Scan (Trivy Image) | trivy-image-result.sarif, trivy-image-result.html |" - echo "| DAST (OWASP ZAP) | result-zap-report.html |" - echo "" - echo "Human-readable HTML reports and machine-readable SARIF/JSON reports are uploaded as the **security-reports-result** workflow artifact." - echo "" - echo "## Files generated" - echo "" - ls -lah security-reports || true - echo "" - } >> "$GITHUB_STEP_SUMMARY" - - - name: Upload Security Reports - uses: actions/upload-artifact@v4 - if: always() - with: - name: security-reports-result - path: security-reports/ - if-no-files-found: warn - - - name: Push image to ECR - run: | - REGISTRY=${{ steps.login-ecr.outputs.registry }}/bq-eks-repo-dev - docker push $REGISTRY:result-${TAG} - - - name: Configure kubectl - run: | - aws eks update-kubeconfig \ - --region ap-southeast-1 \ - --name bq-eks-cluster-dev - - - name: Deploy application - run: | - kubectl apply -f k8s-specifications/ - - - name: Update deployment - run: | - kubectl set image deployment/result \ - result=${{ steps.login-ecr.outputs.registry }}/bq-eks-repo-dev:result-${TAG} - - - name: Wait for rollout - run: | - kubectl rollout status deployment/result \ No newline at end of file diff --git a/.github/workflows/call-docker-build-result-dev.yaml b/.github/workflows/call-docker-build-result-dev.yaml index 479e42534c..a4e46b4a69 100644 --- a/.github/workflows/call-docker-build-result-dev.yaml +++ b/.github/workflows/call-docker-build-result-dev.yaml @@ -4,16 +4,16 @@ on: # workflow_dispatch: push: branches: - - feature/madhu-sample-dev - paths: - - 'result/**' - - '.github/workflows/call-docker-build-result-dev.yaml' - # pull_request: - branches: - - feature/madhu-sample-prod + - development paths: - 'result/**' - '.github/workflows/call-docker-build-result-dev.yaml' + # # pull_request: + # branches: + # - development + # paths: + # - 'result/**' + # - '.github/workflows/call-docker-build-result-dev.yaml' jobs: build-scan-push: diff --git a/.github/workflows/call-docker-build-result.yaml b/.github/workflows/call-docker-build-result.yaml deleted file mode 100644 index 82e01ab1f6..0000000000 --- a/.github/workflows/call-docker-build-result.yaml +++ /dev/null @@ -1,494 +0,0 @@ -name: Build Result Service -# Trigger OIDC test commit - -on: - workflow_dispatch: - push: - branches: - - main - - development - paths: - - 'result/**' - - '.github/workflows/call-docker-build-result.yaml' - pull_request: - branches: - - main - - development - paths: - - 'result/**' - - '.github/workflows/call-docker-build-result.yaml' - -permissions: - id-token: write - contents: read - -jobs: - build-scan-push: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Prepare security report directory - run: | - mkdir -p security-reports - chmod 777 security-reports - - - name: Configure AWS credentials using OIDC - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} - aws-region: ap-southeast-1 - - - name: Verify AWS identity - run: aws sts get-caller-identity - - - name: Run SAST scan with Semgrep - run: | - python -m pip install --upgrade pip - python -m pip install semgrep - semgrep scan \ - --config p/ci \ - --severity ERROR \ - --exclude result/views/angular.min.js \ - --exclude result/views/socket.io.js \ - --sarif \ - --output security-reports/semgrep-result.sarif \ - . || true - - - name: Generate SAST HTML Report - if: always() - run: | - python - <<'PY' - import json, html, pathlib, datetime - sarif_path = pathlib.Path("security-reports/semgrep-result.sarif") - html_path = pathlib.Path("security-reports/semgrep-result.html") - title = "SAST (Semgrep) - Result Service" - rows = [] - error = None - try: - data = json.loads(sarif_path.read_text(encoding="utf-8")) - runs = data.get("runs", []) - for run in runs: - rules = {} - for rule in run.get("tool", {}).get("driver", {}).get("rules", []): - rid = rule.get("id", "") - rules[rid] = rule - for result in run.get("results", []): - rid = result.get("ruleId", "") - rule = rules.get(rid, {}) - level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") - msg = result.get("message", {}).get("text", "") - loc = "" - locations = result.get("locations", []) - if locations: - phys = locations[0].get("physicalLocation", {}) - uri = phys.get("artifactLocation", {}).get("uri", "") - region = phys.get("region", {}) - line = region.get("startLine", "") - loc = f"{uri}:{line}" if line else uri - rows.append((level, rid, msg, loc)) - except Exception as exc: - error = str(exc) - now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") - parts = [ - "", - f"{html.escape(title)}", - "", - "", - f"

{html.escape(title)}

", - f"

Generated at {html.escape(now)}

", - f"

Source SARIF file: {html.escape(str(sarif_path))}

" - ] - if error: - parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") - elif not rows: - parts.append("

No findings were reported.

") - else: - parts.append(f"

Total findings: {len(rows)}

") - parts.append("") - for level, rid, msg, loc in rows: - parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") - parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") - parts.append("") - html_path.write_text("\n".join(parts), encoding="utf-8") - print(f"Generated {html_path}") - PY - - - name: Publish SAST Summary - if: always() - run: | - { - echo "## SAST (Semgrep) - Result Service" - echo "" - if [ -f security-reports/semgrep-result.sarif ]; then - echo "✅ SAST scan completed." - echo "" - echo "**Report artifact files:** semgrep-result.sarif, semgrep-result.html" - else - echo "❌ SAST report was not generated." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Run dependency vulnerability scan with Trivy - uses: aquasecurity/trivy-action@v0.20.0 - with: - scan-type: fs - scan-ref: ./result - vuln-type: library - severity: HIGH,CRITICAL - ignore-unfixed: true - exit-code: '0' - format: sarif - output: security-reports/trivy-fs-result.sarif - - - name: Generate Dependency Scan HTML Report - if: always() - run: | - python - <<'PY' - import json, html, pathlib, datetime - sarif_path = pathlib.Path("security-reports/trivy-fs-result.sarif") - html_path = pathlib.Path("security-reports/trivy-fs-result.html") - title = "SCA / Dependency Scan (Trivy FS) - Result Service" - rows = [] - error = None - try: - data = json.loads(sarif_path.read_text(encoding="utf-8")) - runs = data.get("runs", []) - for run in runs: - rules = {} - for rule in run.get("tool", {}).get("driver", {}).get("rules", []): - rid = rule.get("id", "") - rules[rid] = rule - for result in run.get("results", []): - rid = result.get("ruleId", "") - rule = rules.get(rid, {}) - level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") - msg = result.get("message", {}).get("text", "") - loc = "" - locations = result.get("locations", []) - if locations: - phys = locations[0].get("physicalLocation", {}) - uri = phys.get("artifactLocation", {}).get("uri", "") - region = phys.get("region", {}) - line = region.get("startLine", "") - loc = f"{uri}:{line}" if line else uri - rows.append((level, rid, msg, loc)) - except Exception as exc: - error = str(exc) - now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") - parts = [ - "", - f"{html.escape(title)}", - "", - "", - f"

{html.escape(title)}

", - f"

Generated at {html.escape(now)}

", - f"

Source SARIF file: {html.escape(str(sarif_path))}

" - ] - if error: - parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") - elif not rows: - parts.append("

No findings were reported.

") - else: - parts.append(f"

Total findings: {len(rows)}

") - parts.append("") - for level, rid, msg, loc in rows: - parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") - parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") - parts.append("") - html_path.write_text("\n".join(parts), encoding="utf-8") - print(f"Generated {html_path}") - PY - - - name: Publish Dependency Scan Summary - if: always() - run: | - { - echo "## SCA / Dependency Vulnerability Scan (Trivy FS) - Result Service" - echo "" - if [ -f security-reports/trivy-fs-result.sarif ]; then - echo "✅ Dependency vulnerability scan completed." - echo "" - echo "**Report artifact files:** trivy-fs-result.sarif, trivy-fs-result.html" - else - echo "❌ Dependency scan report was not generated." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Run secret scan with Gitleaks - run: | - GITLEAKS_VERSION=8.24.2 - curl -sSL -o gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" - tar -xzf gitleaks.tar.gz gitleaks - sudo mv gitleaks /usr/local/bin/gitleaks - gitleaks detect \ - --source . \ - --redact \ - --verbose \ - --report-format json \ - --report-path security-reports/gitleaks-result.json \ - --exit-code 0 - - - name: Generate Secret Scan HTML Report - if: always() - run: | - python - <<'PY' - import json, html, pathlib, datetime - json_path = pathlib.Path("security-reports/gitleaks-result.json") - html_path = pathlib.Path("security-reports/gitleaks-result.html") - title = "Secret Scan (Gitleaks) - Result Service" - findings = [] - error = None - try: - text = json_path.read_text(encoding="utf-8").strip() - data = json.loads(text) if text else [] - if isinstance(data, list): - findings = data - elif isinstance(data, dict): - findings = data.get("findings") or data.get("results") or [] - except Exception as exc: - error = str(exc) - now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") - parts = [ - "", - f"{html.escape(title)}", - "", - "", - f"

{html.escape(title)}

", - f"

Generated at {html.escape(now)}

", - f"

Source JSON file: {html.escape(str(json_path))}

" - ] - if error: - parts.append(f"

Could not parse Gitleaks report: {html.escape(error)}

") - elif not findings: - parts.append("

No secrets were reported.

") - else: - parts.append(f"

Total findings: {len(findings)}

") - parts.append("") - for item in findings: - rule = item.get("RuleID") or item.get("rule") or "" - desc = item.get("Description") or item.get("description") or "" - file = item.get("File") or item.get("file") or "" - line = item.get("StartLine") or item.get("line") or "" - commit = item.get("Commit") or item.get("commit") or "" - parts.append("" + "".join(f"" for x in (rule, desc, file, line, commit)) + "") - parts.append("
RuleDescriptionFileLineCommit
{html.escape(str(x))}
") - parts.append("") - html_path.write_text("\n".join(parts), encoding="utf-8") - print(f"Generated {html_path}") - PY - - - name: Publish Secret Scan Summary - if: always() - run: | - { - echo "## Secret Scan (Gitleaks) - Result Service" - echo "" - if [ -f security-reports/gitleaks-result.json ]; then - echo "✅ Secret scan completed." - echo "" - echo "**Report artifact files:** gitleaks-result.json, gitleaks-result.html" - else - echo "❌ Secret scan report was not generated." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Set version tag - run: | - if [[ "${GITHUB_REF##*/}" == "main" ]]; then - echo "TAG=v2" >> $GITHUB_ENV - else - echo "TAG=v1" >> $GITHUB_ENV - fi - - - name: Build Docker image - run: | - REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting - docker build -t result-service:${TAG} -t $REGISTRY:result-${TAG} ./result - - - name: Security scan with Trivy - uses: aquasecurity/trivy-action@v0.20.0 - with: - scan-type: image - image-ref: result-service:${{ env.TAG }} - severity: HIGH,CRITICAL - ignore-unfixed: true - exit-code: '0' - format: sarif - output: security-reports/trivy-image-result.sarif - - - name: Generate Container Image Scan HTML Report - if: always() - run: | - python - <<'PY' - import json, html, pathlib, datetime - sarif_path = pathlib.Path("security-reports/trivy-image-result.sarif") - html_path = pathlib.Path("security-reports/trivy-image-result.html") - title = "Container Image Scan (Trivy Image) - Result Service" - rows = [] - error = None - try: - data = json.loads(sarif_path.read_text(encoding="utf-8")) - runs = data.get("runs", []) - for run in runs: - rules = {} - for rule in run.get("tool", {}).get("driver", {}).get("rules", []): - rid = rule.get("id", "") - rules[rid] = rule - for result in run.get("results", []): - rid = result.get("ruleId", "") - rule = rules.get(rid, {}) - level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") - msg = result.get("message", {}).get("text", "") - loc = "" - locations = result.get("locations", []) - if locations: - phys = locations[0].get("physicalLocation", {}) - uri = phys.get("artifactLocation", {}).get("uri", "") - region = phys.get("region", {}) - line = region.get("startLine", "") - loc = f"{uri}:{line}" if line else uri - rows.append((level, rid, msg, loc)) - except Exception as exc: - error = str(exc) - now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") - parts = [ - "", - f"{html.escape(title)}", - "", - "", - f"

{html.escape(title)}

", - f"

Generated at {html.escape(now)}

", - f"

Source SARIF file: {html.escape(str(sarif_path))}

" - ] - if error: - parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") - elif not rows: - parts.append("

No findings were reported.

") - else: - parts.append(f"

Total findings: {len(rows)}

") - parts.append("") - for level, rid, msg, loc in rows: - parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") - parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") - parts.append("") - html_path.write_text("\n".join(parts), encoding="utf-8") - print(f"Generated {html_path}") - PY - - - name: Publish Container Image Scan Summary - if: always() - run: | - { - echo "## Container Image Scan (Trivy Image) - Result Service" - echo "" - if [ -f security-reports/trivy-image-result.sarif ]; then - echo "✅ Container image scan completed." - echo "" - echo "**Report artifact files:** trivy-image-result.sarif, trivy-image-result.html" - else - echo "❌ Container image scan report was not generated." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Run DAST scan with OWASP ZAP - continue-on-error: true - run: | - docker network create result-dast-net || true - docker rm -f db-service result-dast || true - docker run -d --name db-service --network result-dast-net \ - -e POSTGRES_USER=postgres \ - -e POSTGRES_PASSWORD=postgres \ - -e POSTGRES_DB=postgres \ - postgres:15-alpine - docker run -d --name result-dast --network result-dast-net -p 8081:80 result-service:${TAG} - sleep 30 - curl --fail --retry 10 --retry-delay 5 http://localhost:8081/ - - mkdir -p zap-reports - chmod 777 zap-reports - - docker run --rm \ - --user root \ - --network host \ - -v "${PWD}/zap-reports:/zap/wrk/:rw" \ - ghcr.io/zaproxy/zaproxy:stable \ - zap-baseline.py \ - -t http://localhost:8081 \ - -I \ - -r result-zap-report.html - - mkdir -p security-reports - cp zap-reports/result-zap-report.html security-reports/result-zap-report.html || true - echo "=== zap-reports ===" - ls -lah zap-reports || true - echo "=== zap-reports ===" - ls -lah zap-reports || true - echo "=== security-reports ===" - ls -lah security-reports || true || true - - - name: Verify ZAP Report - if: always() - run: | - echo "=== zap-reports ===" - ls -lah zap-reports || true - echo "=== security-reports ===" - ls -lah security-reports || true - - - name: Publish DAST Summary - if: always() - run: | - { - echo "## DAST (OWASP ZAP) - Result Service" - echo "" - if [ -f security-reports/result-zap-report.html ]; then - echo "✅ DAST scan completed and HTML report was generated." - echo "" - echo "**Report artifact file:** result-zap-report.html" - else - echo "❌ DAST report was not generated." - echo "" - echo "Please check the OWASP ZAP logs above." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Publish Consolidated Security Summary - if: always() - run: | - { - echo "# DevSecOps Security Reports - Result Service" - echo "" - echo "| Security Control | Report File |" - echo "|---|---|" - echo "| SAST (Semgrep) | semgrep-result.sarif, semgrep-result.html |" - echo "| SCA / Dependency Scan (Trivy FS) | trivy-fs-result.sarif, trivy-fs-result.html |" - echo "| Secret Scan (Gitleaks) | gitleaks-result.json, gitleaks-result.html |" - echo "| Container Image Scan (Trivy Image) | trivy-image-result.sarif, trivy-image-result.html |" - echo "| DAST (OWASP ZAP) | result-zap-report.html |" - echo "" - echo "Human-readable HTML reports and machine-readable SARIF/JSON reports are uploaded as the **security-reports-result** workflow artifact." - echo "" - echo "## Files generated" - echo "" - ls -lah security-reports || true - echo "" - } >> "$GITHUB_STEP_SUMMARY" - - - name: Upload Security Reports - uses: actions/upload-artifact@v4 - if: always() - with: - name: security-reports-result - path: security-reports/ - if-no-files-found: warn - - - name: Push image to ECR - run: | - REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting - docker push $REGISTRY:result-${TAG} diff --git a/.github/workflows/call-docker-build-result.yaml:Zone.Identifier b/.github/workflows/call-docker-build-result.yaml:Zone.Identifier deleted file mode 100644 index d6c1ec682968c796b9f5e9e080cc6f674b57c766..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2x", - f"{html.escape(title)}", - "", - "", - f"

{html.escape(title)}

", - f"

Generated at {html.escape(now)}

", - f"

Source SARIF file: {html.escape(str(sarif_path))}

" - ] - if error: - parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") - elif not rows: - parts.append("

No findings were reported.

") - else: - parts.append(f"

Total findings: {len(rows)}

") - parts.append("") - for level, rid, msg, loc in rows: - parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") - parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") - parts.append("") - html_path.write_text("\n".join(parts), encoding="utf-8") - print(f"Generated {html_path}") - PY - - - name: Publish SAST Summary - if: always() - run: | - { - echo "## SAST (Semgrep) - Vote Service" - echo "" - if [ -f security-reports/semgrep-vote.sarif ]; then - echo "✅ SAST scan completed." - echo "" - echo "**Report artifact files:** semgrep-vote.sarif, semgrep-vote.html" - else - echo "❌ SAST report was not generated." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Run dependency vulnerability scan with Trivy - uses: aquasecurity/trivy-action@v0.20.0 - with: - scan-type: fs - scan-ref: ./vote - vuln-type: library - severity: HIGH,CRITICAL - ignore-unfixed: true - exit-code: '0' - format: sarif - output: security-reports/trivy-fs-vote.sarif - - - name: Generate Dependency Scan HTML Report - if: always() - run: | - python - <<'PY' - import json, html, pathlib, datetime - sarif_path = pathlib.Path("security-reports/trivy-fs-vote.sarif") - html_path = pathlib.Path("security-reports/trivy-fs-vote.html") - title = "SCA / Dependency Scan (Trivy FS) - Vote Service" - rows = [] - error = None - try: - data = json.loads(sarif_path.read_text(encoding="utf-8")) - runs = data.get("runs", []) - for run in runs: - rules = {} - for rule in run.get("tool", {}).get("driver", {}).get("rules", []): - rid = rule.get("id", "") - rules[rid] = rule - for result in run.get("results", []): - rid = result.get("ruleId", "") - rule = rules.get(rid, {}) - level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") - msg = result.get("message", {}).get("text", "") - loc = "" - locations = result.get("locations", []) - if locations: - phys = locations[0].get("physicalLocation", {}) - uri = phys.get("artifactLocation", {}).get("uri", "") - region = phys.get("region", {}) - line = region.get("startLine", "") - loc = f"{uri}:{line}" if line else uri - rows.append((level, rid, msg, loc)) - except Exception as exc: - error = str(exc) - now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") - parts = [ - "", - f"{html.escape(title)}", - "", - "", - f"

{html.escape(title)}

", - f"

Generated at {html.escape(now)}

", - f"

Source SARIF file: {html.escape(str(sarif_path))}

" - ] - if error: - parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") - elif not rows: - parts.append("

No findings were reported.

") - else: - parts.append(f"

Total findings: {len(rows)}

") - parts.append("") - for level, rid, msg, loc in rows: - parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") - parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") - parts.append("") - html_path.write_text("\n".join(parts), encoding="utf-8") - print(f"Generated {html_path}") - PY - - - name: Publish Dependency Scan Summary - if: always() - run: | - { - echo "## SCA / Dependency Vulnerability Scan (Trivy FS) - Vote Service" - echo "" - if [ -f security-reports/trivy-fs-vote.sarif ]; then - echo "✅ Dependency vulnerability scan completed." - echo "" - echo "**Report artifact files:** trivy-fs-vote.sarif, trivy-fs-vote.html" - else - echo "❌ Dependency scan report was not generated." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Run secret scan with Gitleaks - run: | - GITLEAKS_VERSION=8.24.2 - curl -sSL -o gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" - tar -xzf gitleaks.tar.gz gitleaks - sudo mv gitleaks /usr/local/bin/gitleaks - gitleaks detect \ - --source . \ - --redact \ - --verbose \ - --report-format json \ - --report-path security-reports/gitleaks-vote.json \ - --exit-code 0 - - - name: Generate Secret Scan HTML Report - if: always() - run: | - python - <<'PY' - import json, html, pathlib, datetime - json_path = pathlib.Path("security-reports/gitleaks-vote.json") - html_path = pathlib.Path("security-reports/gitleaks-vote.html") - title = "Secret Scan (Gitleaks) - Vote Service" - findings = [] - error = None - try: - text = json_path.read_text(encoding="utf-8").strip() - data = json.loads(text) if text else [] - if isinstance(data, list): - findings = data - elif isinstance(data, dict): - findings = data.get("findings") or data.get("results") or [] - except Exception as exc: - error = str(exc) - now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") - parts = [ - "", - f"{html.escape(title)}", - "", - "", - f"

{html.escape(title)}

", - f"

Generated at {html.escape(now)}

", - f"

Source JSON file: {html.escape(str(json_path))}

" - ] - if error: - parts.append(f"

Could not parse Gitleaks report: {html.escape(error)}

") - elif not findings: - parts.append("

No secrets were reported.

") - else: - parts.append(f"

Total findings: {len(findings)}

") - parts.append("") - for item in findings: - rule = item.get("RuleID") or item.get("rule") or "" - desc = item.get("Description") or item.get("description") or "" - file = item.get("File") or item.get("file") or "" - line = item.get("StartLine") or item.get("line") or "" - commit = item.get("Commit") or item.get("commit") or "" - parts.append("" + "".join(f"" for x in (rule, desc, file, line, commit)) + "") - parts.append("
RuleDescriptionFileLineCommit
{html.escape(str(x))}
") - parts.append("") - html_path.write_text("\n".join(parts), encoding="utf-8") - print(f"Generated {html_path}") - PY - - - name: Publish Secret Scan Summary - if: always() - run: | - { - echo "## Secret Scan (Gitleaks) - Vote Service" - echo "" - if [ -f security-reports/gitleaks-vote.json ]; then - echo "✅ Secret scan completed." - echo "" - echo "**Report artifact files:** gitleaks-vote.json, gitleaks-vote.html" - else - echo "❌ Secret scan report was not generated." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Set version tag - run: | - if [[ "${GITHUB_REF##*/}" == "main" ]]; then - echo "TAG=v2" >> $GITHUB_ENV - else - echo "TAG=v1" >> $GITHUB_ENV - fi - - - name: Build Docker image - run: | - REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting - docker build -t vote-service:${TAG} ./vote - docker tag vote-service:${TAG} $REGISTRY:vote-${TAG} - - - name: Security scan with Trivy - uses: aquasecurity/trivy-action@v0.20.0 - with: - scan-type: image - image-ref: vote-service:${{ env.TAG }} - severity: HIGH,CRITICAL - ignore-unfixed: true - exit-code: '0' - format: sarif - output: security-reports/trivy-image-vote.sarif - - - name: Generate Container Image Scan HTML Report - if: always() - run: | - python - <<'PY' - import json, html, pathlib, datetime - sarif_path = pathlib.Path("security-reports/trivy-image-vote.sarif") - html_path = pathlib.Path("security-reports/trivy-image-vote.html") - title = "Container Image Scan (Trivy Image) - Vote Service" - rows = [] - error = None - try: - data = json.loads(sarif_path.read_text(encoding="utf-8")) - runs = data.get("runs", []) - for run in runs: - rules = {} - for rule in run.get("tool", {}).get("driver", {}).get("rules", []): - rid = rule.get("id", "") - rules[rid] = rule - for result in run.get("results", []): - rid = result.get("ruleId", "") - rule = rules.get(rid, {}) - level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") - msg = result.get("message", {}).get("text", "") - loc = "" - locations = result.get("locations", []) - if locations: - phys = locations[0].get("physicalLocation", {}) - uri = phys.get("artifactLocation", {}).get("uri", "") - region = phys.get("region", {}) - line = region.get("startLine", "") - loc = f"{uri}:{line}" if line else uri - rows.append((level, rid, msg, loc)) - except Exception as exc: - error = str(exc) - now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") - parts = [ - "", - f"{html.escape(title)}", - "", - "", - f"

{html.escape(title)}

", - f"

Generated at {html.escape(now)}

", - f"

Source SARIF file: {html.escape(str(sarif_path))}

" - ] - if error: - parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") - elif not rows: - parts.append("

No findings were reported.

") - else: - parts.append(f"

Total findings: {len(rows)}

") - parts.append("") - for level, rid, msg, loc in rows: - parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") - parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") - parts.append("") - html_path.write_text("\n".join(parts), encoding="utf-8") - print(f"Generated {html_path}") - PY - - - name: Publish Container Image Scan Summary - if: always() - run: | - { - echo "## Container Image Scan (Trivy Image) - Vote Service" - echo "" - if [ -f security-reports/trivy-image-vote.sarif ]; then - echo "✅ Container image scan completed." - echo "" - echo "**Report artifact files:** trivy-image-vote.sarif, trivy-image-vote.html" - else - echo "❌ Container image scan report was not generated." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Run DAST scan with OWASP ZAP - continue-on-error: true - run: | - docker network create vote-dast-net || true - docker rm -f redis vote-dast || true - docker run -d --name redis --network vote-dast-net redis:7-alpine - docker run -d --name vote-dast --network vote-dast-net -p 8080:80 vote-service:${TAG} - sleep 20 - curl --fail --retry 10 --retry-delay 5 http://localhost:8080/ - - mkdir -p zap-reports - chmod 777 zap-reports - - docker run --rm \ - --user root \ - --network host \ - -v "${PWD}/zap-reports:/zap/wrk/:rw" \ - ghcr.io/zaproxy/zaproxy:stable \ - zap-baseline.py \ - -t http://localhost:8080 \ - -I \ - -r vote-zap-report.html - - mkdir -p security-reports - cp zap-reports/vote-zap-report.html security-reports/vote-zap-report.html || true - echo "=== zap-reports ===" - ls -lah zap-reports || true - echo "=== zap-reports ===" - ls -lah zap-reports || true - echo "=== security-reports ===" - ls -lah security-reports || true || true - - - name: Verify ZAP Report - if: always() - run: | - echo "=== zap-reports ===" - ls -lah zap-reports || true - echo "=== security-reports ===" - ls -lah security-reports || true - - - name: Publish DAST Summary - if: always() - run: | - { - echo "## DAST (OWASP ZAP) - Vote Service" - echo "" - if [ -f security-reports/vote-zap-report.html ]; then - echo "✅ DAST scan completed and HTML report was generated." - echo "" - echo "**Report artifact file:** vote-zap-report.html" - else - echo "❌ DAST report was not generated." - echo "" - echo "Please check the OWASP ZAP logs above." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Publish Consolidated Security Summary - if: always() - run: | - { - echo "# DevSecOps Security Reports - Vote Service" - echo "" - echo "| Security Control | Report File |" - echo "|---|---|" - echo "| SAST (Semgrep) | semgrep-vote.sarif, semgrep-vote.html |" - echo "| SCA / Dependency Scan (Trivy FS) | trivy-fs-vote.sarif, trivy-fs-vote.html |" - echo "| Secret Scan (Gitleaks) | gitleaks-vote.json, gitleaks-vote.html |" - echo "| Container Image Scan (Trivy Image) | trivy-image-vote.sarif, trivy-image-vote.html |" - echo "| DAST (OWASP ZAP) | vote-zap-report.html |" - echo "" - echo "Human-readable HTML reports and machine-readable SARIF/JSON reports are uploaded as the **security-reports-vote** workflow artifact." - echo "" - echo "## Files generated" - echo "" - ls -lah security-reports || true - echo "" - } >> "$GITHUB_STEP_SUMMARY" - - - name: Upload Security Reports - uses: actions/upload-artifact@v4 - if: always() - with: - name: security-reports-vote - path: security-reports/ - if-no-files-found: warn - - - name: Push image to ECR - run: | - REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting - docker push $REGISTRY:vote-${TAG} diff --git a/.github/workflows/call-docker-build-vote.yaml:Zone.Identifier b/.github/workflows/call-docker-build-vote.yaml:Zone.Identifier deleted file mode 100644 index d6c1ec682968c796b9f5e9e080cc6f674b57c766..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2x", - f"{html.escape(title)}", - "", - "", - f"

{html.escape(title)}

", - f"

Generated at {html.escape(now)}

", - f"

Source SARIF file: {html.escape(str(sarif_path))}

" - ] - if error: - parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") - elif not rows: - parts.append("

No findings were reported.

") - else: - parts.append(f"

Total findings: {len(rows)}

") - parts.append("") - for level, rid, msg, loc in rows: - parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") - parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") - parts.append("") - html_path.write_text("\n".join(parts), encoding="utf-8") - print(f"Generated {html_path}") - PY - - - name: Publish SAST Summary - if: always() - run: | - { - echo "## SAST (Semgrep) - Worker Service" - echo "" - if [ -f security-reports/semgrep-worker.sarif ]; then - echo "✅ SAST scan completed." - echo "" - echo "**Report artifact files:** semgrep-worker.sarif, semgrep-worker.html" - else - echo "❌ SAST report was not generated." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Run dependency vulnerability scan with Trivy - uses: aquasecurity/trivy-action@v0.20.0 - with: - scan-type: fs - scan-ref: ./worker - vuln-type: library - severity: HIGH,CRITICAL - ignore-unfixed: true - exit-code: '0' - format: sarif - output: security-reports/trivy-fs-worker.sarif - - - name: Generate Dependency Scan HTML Report - if: always() - run: | - python - <<'PY' - import json, html, pathlib, datetime - sarif_path = pathlib.Path("security-reports/trivy-fs-worker.sarif") - html_path = pathlib.Path("security-reports/trivy-fs-worker.html") - title = "SCA / Dependency Scan (Trivy FS) - Worker Service" - rows = [] - error = None - try: - data = json.loads(sarif_path.read_text(encoding="utf-8")) - runs = data.get("runs", []) - for run in runs: - rules = {} - for rule in run.get("tool", {}).get("driver", {}).get("rules", []): - rid = rule.get("id", "") - rules[rid] = rule - for result in run.get("results", []): - rid = result.get("ruleId", "") - rule = rules.get(rid, {}) - level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") - msg = result.get("message", {}).get("text", "") - loc = "" - locations = result.get("locations", []) - if locations: - phys = locations[0].get("physicalLocation", {}) - uri = phys.get("artifactLocation", {}).get("uri", "") - region = phys.get("region", {}) - line = region.get("startLine", "") - loc = f"{uri}:{line}" if line else uri - rows.append((level, rid, msg, loc)) - except Exception as exc: - error = str(exc) - now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") - parts = [ - "", - f"{html.escape(title)}", - "", - "", - f"

{html.escape(title)}

", - f"

Generated at {html.escape(now)}

", - f"

Source SARIF file: {html.escape(str(sarif_path))}

" - ] - if error: - parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") - elif not rows: - parts.append("

No findings were reported.

") - else: - parts.append(f"

Total findings: {len(rows)}

") - parts.append("") - for level, rid, msg, loc in rows: - parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") - parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") - parts.append("") - html_path.write_text("\n".join(parts), encoding="utf-8") - print(f"Generated {html_path}") - PY - - - name: Publish Dependency Scan Summary - if: always() - run: | - { - echo "## SCA / Dependency Vulnerability Scan (Trivy FS) - Worker Service" - echo "" - if [ -f security-reports/trivy-fs-worker.sarif ]; then - echo "✅ Dependency vulnerability scan completed." - echo "" - echo "**Report artifact files:** trivy-fs-worker.sarif, trivy-fs-worker.html" - else - echo "❌ Dependency scan report was not generated." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Run secret scan with Gitleaks - run: | - GITLEAKS_VERSION=8.24.2 - curl -sSL -o gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" - tar -xzf gitleaks.tar.gz gitleaks - sudo mv gitleaks /usr/local/bin/gitleaks - gitleaks detect \ - --source . \ - --redact \ - --verbose \ - --report-format json \ - --report-path security-reports/gitleaks-worker.json \ - --exit-code 0 - - - name: Generate Secret Scan HTML Report - if: always() - run: | - python - <<'PY' - import json, html, pathlib, datetime - json_path = pathlib.Path("security-reports/gitleaks-worker.json") - html_path = pathlib.Path("security-reports/gitleaks-worker.html") - title = "Secret Scan (Gitleaks) - Worker Service" - findings = [] - error = None - try: - text = json_path.read_text(encoding="utf-8").strip() - data = json.loads(text) if text else [] - if isinstance(data, list): - findings = data - elif isinstance(data, dict): - findings = data.get("findings") or data.get("results") or [] - except Exception as exc: - error = str(exc) - now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") - parts = [ - "", - f"{html.escape(title)}", - "", - "", - f"

{html.escape(title)}

", - f"

Generated at {html.escape(now)}

", - f"

Source JSON file: {html.escape(str(json_path))}

" - ] - if error: - parts.append(f"

Could not parse Gitleaks report: {html.escape(error)}

") - elif not findings: - parts.append("

No secrets were reported.

") - else: - parts.append(f"

Total findings: {len(findings)}

") - parts.append("") - for item in findings: - rule = item.get("RuleID") or item.get("rule") or "" - desc = item.get("Description") or item.get("description") or "" - file = item.get("File") or item.get("file") or "" - line = item.get("StartLine") or item.get("line") or "" - commit = item.get("Commit") or item.get("commit") or "" - parts.append("" + "".join(f"" for x in (rule, desc, file, line, commit)) + "") - parts.append("
RuleDescriptionFileLineCommit
{html.escape(str(x))}
") - parts.append("") - html_path.write_text("\n".join(parts), encoding="utf-8") - print(f"Generated {html_path}") - PY - - - name: Publish Secret Scan Summary - if: always() - run: | - { - echo "## Secret Scan (Gitleaks) - Worker Service" - echo "" - if [ -f security-reports/gitleaks-worker.json ]; then - echo "✅ Secret scan completed." - echo "" - echo "**Report artifact files:** gitleaks-worker.json, gitleaks-worker.html" - else - echo "❌ Secret scan report was not generated." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Set version tag - run: | - if [[ "${GITHUB_REF##*/}" == "main" ]]; then - echo "TAG=v2" >> $GITHUB_ENV - else - echo "TAG=v1" >> $GITHUB_ENV - fi - - - name: Build Docker image - run: | - REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting - docker build -t worker-service:${TAG} -t $REGISTRY:worker-${TAG} ./worker - - - name: Security scan with Trivy - uses: aquasecurity/trivy-action@v0.20.0 - with: - scan-type: image - image-ref: worker-service:${{ env.TAG }} - severity: HIGH,CRITICAL - ignore-unfixed: true - exit-code: '0' - format: sarif - output: security-reports/trivy-image-worker.sarif - - - name: Generate Container Image Scan HTML Report - if: always() - run: | - python - <<'PY' - import json, html, pathlib, datetime - sarif_path = pathlib.Path("security-reports/trivy-image-worker.sarif") - html_path = pathlib.Path("security-reports/trivy-image-worker.html") - title = "Container Image Scan (Trivy Image) - Worker Service" - rows = [] - error = None - try: - data = json.loads(sarif_path.read_text(encoding="utf-8")) - runs = data.get("runs", []) - for run in runs: - rules = {} - for rule in run.get("tool", {}).get("driver", {}).get("rules", []): - rid = rule.get("id", "") - rules[rid] = rule - for result in run.get("results", []): - rid = result.get("ruleId", "") - rule = rules.get(rid, {}) - level = result.get("level") or rule.get("defaultConfiguration", {}).get("level", "") - msg = result.get("message", {}).get("text", "") - loc = "" - locations = result.get("locations", []) - if locations: - phys = locations[0].get("physicalLocation", {}) - uri = phys.get("artifactLocation", {}).get("uri", "") - region = phys.get("region", {}) - line = region.get("startLine", "") - loc = f"{uri}:{line}" if line else uri - rows.append((level, rid, msg, loc)) - except Exception as exc: - error = str(exc) - now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") - parts = [ - "", - f"{html.escape(title)}", - "", - "", - f"

{html.escape(title)}

", - f"

Generated at {html.escape(now)}

", - f"

Source SARIF file: {html.escape(str(sarif_path))}

" - ] - if error: - parts.append(f"

Could not parse SARIF report: {html.escape(error)}

") - elif not rows: - parts.append("

No findings were reported.

") - else: - parts.append(f"

Total findings: {len(rows)}

") - parts.append("") - for level, rid, msg, loc in rows: - parts.append("" + "".join(f"" for x in (level, rid, msg, loc)) + "") - parts.append("
LevelRule IDMessageLocation
{html.escape(str(x))}
") - parts.append("") - html_path.write_text("\n".join(parts), encoding="utf-8") - print(f"Generated {html_path}") - PY - - - name: Publish Container Image Scan Summary - if: always() - run: | - { - echo "## Container Image Scan (Trivy Image) - Worker Service" - echo "" - if [ -f security-reports/trivy-image-worker.sarif ]; then - echo "✅ Container image scan completed." - echo "" - echo "**Report artifact files:** trivy-image-worker.sarif, trivy-image-worker.html" - else - echo "❌ Container image scan report was not generated." - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: DAST note for worker service - run: | - echo "Worker is a background service without an HTTP endpoint, so OWASP ZAP DAST is not applicable to this service." - echo "DAST is implemented for the vote and result web services." - - - name: Publish DAST Summary - if: always() - run: | - { - echo "## DAST (OWASP ZAP) - Worker Service" - echo "" - echo "ℹ️ DAST is not applicable because the worker is a background service without an HTTP endpoint." - echo "" - echo "DAST is implemented for the vote and result web services." - } >> "$GITHUB_STEP_SUMMARY" - - - name: Publish Consolidated Security Summary - if: always() - run: | - { - echo "# DevSecOps Security Reports - Worker Service" - echo "" - echo "| Security Control | Report File |" - echo "|---|---|" - echo "| SAST (Semgrep) | semgrep-worker.sarif, semgrep-worker.html |" - echo "| SCA / Dependency Scan (Trivy FS) | trivy-fs-worker.sarif, trivy-fs-worker.html |" - echo "| Secret Scan (Gitleaks) | gitleaks-worker.json, gitleaks-worker.html |" - echo "| Container Image Scan (Trivy Image) | trivy-image-worker.sarif, trivy-image-worker.html |" - echo "| DAST (OWASP ZAP) | Not applicable - background service |" - echo "" - echo "Human-readable HTML reports and machine-readable SARIF/JSON reports are uploaded as the **security-reports-worker** workflow artifact." - echo "" - echo "## Files generated" - echo "" - ls -lah security-reports || true - echo "" - } >> "$GITHUB_STEP_SUMMARY" - - - name: Upload Security Reports - uses: actions/upload-artifact@v4 - if: always() - with: - name: security-reports-worker - path: security-reports/ - if-no-files-found: warn - - - name: Push image to ECR - run: | - REGISTRY=${{ steps.login-ecr.outputs.registry }}/mightycapstonevoting - docker push $REGISTRY:worker-${TAG} diff --git a/.github/workflows/call-docker-build-worker.yaml:Zone.Identifier b/.github/workflows/call-docker-build-worker.yaml:Zone.Identifier deleted file mode 100644 index d6c1ec682968c796b9f5e9e080cc6f674b57c766..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2x Date: Wed, 1 Jul 2026 07:05:25 +0800 Subject: [PATCH 24/31] Editing notes --- README.md => Notes/App Notes/README.md | 0 .../App Notes/architecture.excalidraw.png | Bin Notes/ArchitectureDiagram.md | 3 +++ Notes/image.png | Bin 0 -> 118826 bytes 4 files changed, 3 insertions(+) rename README.md => Notes/App Notes/README.md (100%) rename architecture.excalidraw.png => Notes/App Notes/architecture.excalidraw.png (100%) create mode 100644 Notes/ArchitectureDiagram.md create mode 100644 Notes/image.png diff --git a/README.md b/Notes/App Notes/README.md similarity index 100% rename from README.md rename to Notes/App Notes/README.md diff --git a/architecture.excalidraw.png b/Notes/App Notes/architecture.excalidraw.png similarity index 100% rename from architecture.excalidraw.png rename to Notes/App Notes/architecture.excalidraw.png diff --git a/Notes/ArchitectureDiagram.md b/Notes/ArchitectureDiagram.md new file mode 100644 index 0000000000..6caaafc068 --- /dev/null +++ b/Notes/ArchitectureDiagram.md @@ -0,0 +1,3 @@ +The following picture shows the complete CI/CD Pipleine for this implemented project + +![alt text](image.png) \ No newline at end of file diff --git a/Notes/image.png b/Notes/image.png new file mode 100644 index 0000000000000000000000000000000000000000..3aedecb616f94e37f0fe7f759efaf39351380141 GIT binary patch literal 118826 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 Date: Wed, 1 Jul 2026 07:10:23 +0800 Subject: [PATCH 25/31] editing docs --- {Notes => docs}/App Notes/README.md | 0 .../App Notes/architecture.excalidraw.png | Bin {Notes => docs}/ArchitectureDiagram.md | 0 docs/ard/0001-mightcapstone.md | 3 +++ docs/ard/image.png | Bin 0 -> 84332 bytes {Notes => docs}/image.png | Bin 6 files changed, 3 insertions(+) rename {Notes => docs}/App Notes/README.md (100%) rename {Notes => docs}/App Notes/architecture.excalidraw.png (100%) rename {Notes => docs}/ArchitectureDiagram.md (100%) create mode 100644 docs/ard/0001-mightcapstone.md create mode 100644 docs/ard/image.png rename {Notes => docs}/image.png (100%) diff --git a/Notes/App Notes/README.md b/docs/App Notes/README.md similarity index 100% rename from Notes/App Notes/README.md rename to docs/App Notes/README.md diff --git a/Notes/App Notes/architecture.excalidraw.png b/docs/App Notes/architecture.excalidraw.png similarity index 100% rename from Notes/App Notes/architecture.excalidraw.png rename to docs/App Notes/architecture.excalidraw.png diff --git a/Notes/ArchitectureDiagram.md b/docs/ArchitectureDiagram.md similarity index 100% rename from Notes/ArchitectureDiagram.md rename to docs/ArchitectureDiagram.md diff --git a/docs/ard/0001-mightcapstone.md b/docs/ard/0001-mightcapstone.md new file mode 100644 index 0000000000..8efbecbd15 --- /dev/null +++ b/docs/ard/0001-mightcapstone.md @@ -0,0 +1,3 @@ +Assignment 15 + +![alt text](image.png) \ No newline at end of file diff --git a/docs/ard/image.png b/docs/ard/image.png new file mode 100644 index 0000000000000000000000000000000000000000..15551ef87cf8f81416e135a3a1d413e6493a3b9d GIT binary patch literal 84332 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 literal 0 HcmV?d00001 diff --git a/Notes/image.png b/docs/image.png similarity index 100% rename from Notes/image.png rename to docs/image.png From 1f211e7213fde611a01f3c9836be5080a8038ddd Mon Sep 17 00:00:00 2001 From: kannanmadhu Date: Mon, 6 Jul 2026 21:40:39 +0800 Subject: [PATCH 26/31] 07062026 --- .github/workflows/call-docker-build-result-dev.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/call-docker-build-result-dev.yaml b/.github/workflows/call-docker-build-result-dev.yaml index a4e46b4a69..695cb598bd 100644 --- a/.github/workflows/call-docker-build-result-dev.yaml +++ b/.github/workflows/call-docker-build-result-dev.yaml @@ -1,3 +1,4 @@ +#### this is the dev workflow for result service, it will be triggered on push to development branch and will perform the following tasks: name: Build Result Service on: @@ -503,3 +504,4 @@ jobs: run: | kubectl rollout status deployment/result + From 24c077fe9f6f71df76fca5e450a1fe6bf13cd9c0 Mon Sep 17 00:00:00 2001 From: kannanmadhu Date: Mon, 6 Jul 2026 22:12:01 +0800 Subject: [PATCH 27/31] 07062026 trial 2 --- .github/workflows/call-docker-build-result-dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/call-docker-build-result-dev.yaml b/.github/workflows/call-docker-build-result-dev.yaml index 695cb598bd..87c10655e5 100644 --- a/.github/workflows/call-docker-build-result-dev.yaml +++ b/.github/workflows/call-docker-build-result-dev.yaml @@ -293,7 +293,7 @@ jobs: - name: Set version tag run: | - if [[ "${GITHUB_REF##*/}" == "feature/madhu-sample-prod" ]]; then + if [[ "${GITHUB_REF##*/}" == "main" ]]; then echo "TAG=v2" >> $GITHUB_ENV else echo "TAG=v1" >> $GITHUB_ENV From c961897d70060abfbec2dddf0c857821f48c0eee Mon Sep 17 00:00:00 2001 From: kannanmadhu Date: Mon, 6 Jul 2026 22:33:25 +0800 Subject: [PATCH 28/31] 07062026 trial 3 --- k8s-specifications/db-deployment.yaml | 2 +- k8s-specifications/db-service.yaml | 2 +- k8s-specifications/redis-deployment.yaml | 2 +- k8s-specifications/redis-service.yaml | 2 +- k8s-specifications/result-v1-deployment.yaml | 80 ++++++++++---------- k8s-specifications/result-v1-service.yaml | 30 ++++---- k8s-specifications/result-v2-deployment.yaml | 80 ++++++++++---------- k8s-specifications/vote-deployment.yaml | 2 +- k8s-specifications/vote-service.yaml | 2 +- 9 files changed, 101 insertions(+), 101 deletions(-) diff --git a/k8s-specifications/db-deployment.yaml b/k8s-specifications/db-deployment.yaml index cf2eadd0d1..8aeb8dd8e4 100644 --- a/k8s-specifications/db-deployment.yaml +++ b/k8s-specifications/db-deployment.yaml @@ -2,7 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: db-deployment - namespace: mightycapstone + # namespace: mightycapstone labels: app: db spec: diff --git a/k8s-specifications/db-service.yaml b/k8s-specifications/db-service.yaml index ce895c1ba7..8d8b3e566e 100644 --- a/k8s-specifications/db-service.yaml +++ b/k8s-specifications/db-service.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Service metadata: name: db-service - namespace: mightycapstone # 👈 ensure it matches your namespace + # namespace: mightycapstone # 👈 ensure it matches your namespace labels: app: db spec: diff --git a/k8s-specifications/redis-deployment.yaml b/k8s-specifications/redis-deployment.yaml index c58fb0204c..9feb2a23c7 100644 --- a/k8s-specifications/redis-deployment.yaml +++ b/k8s-specifications/redis-deployment.yaml @@ -2,7 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: redis-deployment - namespace: mightycapstone + # namespace: mightycapstone labels: app: redis spec: diff --git a/k8s-specifications/redis-service.yaml b/k8s-specifications/redis-service.yaml index 0c0831b36f..f3528cca2a 100644 --- a/k8s-specifications/redis-service.yaml +++ b/k8s-specifications/redis-service.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Service metadata: name: redis-service - namespace: mightycapstone + # namespace: mightycapstone labels: app: redis spec: diff --git a/k8s-specifications/result-v1-deployment.yaml b/k8s-specifications/result-v1-deployment.yaml index 05a574966d..0ee42af74b 100644 --- a/k8s-specifications/result-v1-deployment.yaml +++ b/k8s-specifications/result-v1-deployment.yaml @@ -1,40 +1,40 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: result-v1-deployment - namespace: mightycapstone - labels: - app: result-v1 -spec: - replicas: 1 - selector: - matchLabels: - app: result-v1 - template: - metadata: - labels: - app: result-v1 - spec: - containers: - - name: result - image: 255945442255.dkr.ecr.ap-southeast-1.amazonaws.com/mightycapstonevoting:result-v1 - ports: - - containerPort: 80 - name: http - env: - - name: REDIS_HOST - value: redis-service - - name: DB_HOST - value: db-service - livenessProbe: - httpGet: - path: / - port: 80 - initialDelaySeconds: 20 - periodSeconds: 10 - readinessProbe: - httpGet: - path: / - port: 80 - initialDelaySeconds: 30 - periodSeconds: 10 +# apiVersion: apps/v1 +# kind: Deployment +# metadata: +# name: result-v1-deployment +# namespace: mightycapstone +# # labels: +# app: result-v1 +# spec: +# replicas: 1 +# selector: +# matchLabels: +# app: result-v1 +# template: +# metadata: +# labels: +# app: result-v1 +# spec: +# containers: +# - name: result +# image: 255945442255.dkr.ecr.ap-southeast-1.amazonaws.com/mightycapstonevoting:result-v1 +# ports: +# - containerPort: 80 +# name: http +# env: +# - name: REDIS_HOST +# value: redis-service +# - name: DB_HOST +# value: db-service +# livenessProbe: +# httpGet: +# path: / +# port: 80 +# initialDelaySeconds: 20 +# periodSeconds: 10 +# readinessProbe: +# httpGet: +# path: / +# port: 80 +# initialDelaySeconds: 30 +# periodSeconds: 10 diff --git a/k8s-specifications/result-v1-service.yaml b/k8s-specifications/result-v1-service.yaml index ee13b2516b..c035463a3d 100644 --- a/k8s-specifications/result-v1-service.yaml +++ b/k8s-specifications/result-v1-service.yaml @@ -1,16 +1,16 @@ -apiVersion: v1 -kind: Service -metadata: - name: result-v1-service - namespace: mightycapstone - labels: - app: result-v1 -spec: - type: LoadBalancer # 👈 AWS ELB will be provisioned - selector: - app: result-v1 - ports: - - name: http - port: 8081 # external port exposed by ELB - targetPort: 80 # container port inside result pod +# apiVersion: v1 +# kind: Service +# metadata: +# name: result-v1-service +# namespace: mightycapstone +# labels: +# app: result-v1 +# spec: +# type: LoadBalancer # 👈 AWS ELB will be provisioned +# selector: +# app: result-v1 +# ports: +# - name: http +# port: 8081 # external port exposed by ELB +# targetPort: 80 # container port inside result pod diff --git a/k8s-specifications/result-v2-deployment.yaml b/k8s-specifications/result-v2-deployment.yaml index e8783907f0..7700e7f8a5 100644 --- a/k8s-specifications/result-v2-deployment.yaml +++ b/k8s-specifications/result-v2-deployment.yaml @@ -1,40 +1,40 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: result-v2-deployment - namespace: mightycapstone - labels: - app: result-v2 -spec: - replicas: 1 - selector: - matchLabels: - app: result-v2 - template: - metadata: - labels: - app: result-v2 - spec: - containers: - - name: result - image: 255945442255.dkr.ecr.ap-southeast-1.amazonaws.com/mightycapstonevoting:result-v2 - ports: - - containerPort: 80 - name: http - env: - - name: REDIS_HOST - value: redis-service - - name: DB_HOST - value: db-service - livenessProbe: - httpGet: - path: / - port: 80 - initialDelaySeconds: 20 - periodSeconds: 10 - readinessProbe: - httpGet: - path: / - port: 80 - initialDelaySeconds: 30 - periodSeconds: 10 +# apiVersion: apps/v1 +# kind: Deployment +# metadata: +# name: result-v2-deployment +# namespace: mightycapstone +# labels: +# app: result-v2 +# spec: +# replicas: 1 +# selector: +# matchLabels: +# app: result-v2 +# template: +# metadata: +# labels: +# app: result-v2 +# spec: +# containers: +# - name: result +# image: 255945442255.dkr.ecr.ap-southeast-1.amazonaws.com/mightycapstonevoting:result-v2 +# ports: +# - containerPort: 80 +# name: http +# env: +# - name: REDIS_HOST +# value: redis-service +# - name: DB_HOST +# value: db-service +# livenessProbe: +# httpGet: +# path: / +# port: 80 +# initialDelaySeconds: 20 +# periodSeconds: 10 +# readinessProbe: +# httpGet: +# path: / +# port: 80 +# initialDelaySeconds: 30 +# periodSeconds: 10 diff --git a/k8s-specifications/vote-deployment.yaml b/k8s-specifications/vote-deployment.yaml index e9973e3572..4023633789 100644 --- a/k8s-specifications/vote-deployment.yaml +++ b/k8s-specifications/vote-deployment.yaml @@ -2,7 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: vote-deployment - namespace: mightycapstone + # namespace: mightycapstone labels: app: vote spec: diff --git a/k8s-specifications/vote-service.yaml b/k8s-specifications/vote-service.yaml index 176b2a2157..5138736a4e 100644 --- a/k8s-specifications/vote-service.yaml +++ b/k8s-specifications/vote-service.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Service metadata: name: vote-service - namespace: mightycapstone + # namespace: mightycapstone labels: app: vote spec: From 50d152fcd27a886c37f57e9d8831bf0b9d2f9c22 Mon Sep 17 00:00:00 2001 From: kannanmadhu Date: Mon, 6 Jul 2026 22:36:09 +0800 Subject: [PATCH 29/31] 06072026 trial 3 --- .github/workflows/call-docker-build-vote-dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/call-docker-build-vote-dev.yaml b/.github/workflows/call-docker-build-vote-dev.yaml index 54db33bf17..054c89a5b6 100644 --- a/.github/workflows/call-docker-build-vote-dev.yaml +++ b/.github/workflows/call-docker-build-vote-dev.yaml @@ -293,7 +293,7 @@ jobs: - name: Set version tag run: | - if [[ "${GITHUB_REF##*/}" == "feature/madhu-sample-prod" ]]; then + if [[ "${GITHUB_REF##*/}" == "main" ]]; then echo "TAG=v2" >> $GITHUB_ENV else echo "TAG=v1" >> $GITHUB_ENV From c87d815ffaebbe408b8be51d4b85e0a3bf6802f6 Mon Sep 17 00:00:00 2001 From: kannanmadhu Date: Mon, 6 Jul 2026 22:41:48 +0800 Subject: [PATCH 30/31] 06072026 trial 4 --- .github/workflows/call-docker-build-worker-dev.yaml | 2 +- k8s-specifications/worker-deployment.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/call-docker-build-worker-dev.yaml b/.github/workflows/call-docker-build-worker-dev.yaml index a01a93ab39..af5206300d 100644 --- a/.github/workflows/call-docker-build-worker-dev.yaml +++ b/.github/workflows/call-docker-build-worker-dev.yaml @@ -293,7 +293,7 @@ jobs: - name: Set version tag run: | - if [[ "${GITHUB_REF##*/}" == "feature/madhu-sample-prod" ]]; then + if [[ "${GITHUB_REF##*/}" == "main" ]]; then echo "TAG=v2" >> $GITHUB_ENV else echo "TAG=v1" >> $GITHUB_ENV diff --git a/k8s-specifications/worker-deployment.yaml b/k8s-specifications/worker-deployment.yaml index 17c4b9fc84..c1a47f5891 100644 --- a/k8s-specifications/worker-deployment.yaml +++ b/k8s-specifications/worker-deployment.yaml @@ -2,7 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: worker-deployment - namespace: mightycapstone + # namespace: mightycapstone labels: app: worker spec: From 8b50656a90e905068b14aa2319f10b6062292c94 Mon Sep 17 00:00:00 2001 From: kannanmadhu Date: Mon, 6 Jul 2026 22:49:40 +0800 Subject: [PATCH 31/31] 06072026 trial 5 --- .github/workflows/call-docker-build-result-dev.yaml | 2 +- .github/workflows/call-docker-build-worker-dev.yaml | 3 ++- k8s-specifications/worker-deployment.yaml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/call-docker-build-result-dev.yaml b/.github/workflows/call-docker-build-result-dev.yaml index 87c10655e5..dc28e5d679 100644 --- a/.github/workflows/call-docker-build-result-dev.yaml +++ b/.github/workflows/call-docker-build-result-dev.yaml @@ -1,4 +1,4 @@ -#### this is the dev workflow for result service, it will be triggered on push to development branch and will perform the following tasks: + name: Build Result Service on: diff --git a/.github/workflows/call-docker-build-worker-dev.yaml b/.github/workflows/call-docker-build-worker-dev.yaml index af5206300d..e0a66b0fd7 100644 --- a/.github/workflows/call-docker-build-worker-dev.yaml +++ b/.github/workflows/call-docker-build-worker-dev.yaml @@ -457,4 +457,5 @@ jobs: - name: Wait for rollout run: | - kubectl rollout status deployment/worker \ No newline at end of file + kubectl rollout status deployment/worker + diff --git a/k8s-specifications/worker-deployment.yaml b/k8s-specifications/worker-deployment.yaml index c1a47f5891..90ba0059cb 100644 --- a/k8s-specifications/worker-deployment.yaml +++ b/k8s-specifications/worker-deployment.yaml @@ -1,7 +1,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: worker-deployment + name: worker # namespace: mightycapstone labels: app: worker