diff --git a/.github/workflows/build-and-push-image.yml b/.github/workflows/build-and-push-image.yml new file mode 100644 index 0000000..9842a00 --- /dev/null +++ b/.github/workflows/build-and-push-image.yml @@ -0,0 +1,166 @@ +name: Deploy to environment + +on: + push: + branches: + - main + workflow_dispatch: + inputs: + environment: + type: environment + description: "Choose an environment to deploy to" + required: true + +concurrency: + group: ${{ github.workflow }}-${{ github.event.inputs.environment }} + +env: + DOCKER_IMAGE: identifiersapi-app + NODE_VERSION: 18 + +jobs: + set-env: + name: Determine environment + runs-on: ubuntu-22.04 + outputs: + environment: ${{ steps.var.outputs.environment }} + branch: ${{ steps.var.outputs.branch }} + release: ${{ steps.var.outputs.release }} + checked-out-sha: ${{ steps.var.outputs.checked-out-sha }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + + - id: var + run: | + GIT_REF=${{ github.ref }} + GIT_BRANCH=${GIT_REF##*/} + INPUT=${{ github.event.inputs.environment }} + ENVIRONMENT=${INPUT:-"development"} + RELEASE=${ENVIRONMENT,,}-`date +%Y-%m-%d`.${{ github.run_number }} + CHECKED_OUT_SHA="$(git log -1 '--format=format:%H')" + echo "environment=${ENVIRONMENT,,}" >> $GITHUB_OUTPUT + echo "branch=$GIT_BRANCH" >> $GITHUB_OUTPUT + echo "release=${RELEASE}" >> $GITHUB_OUTPUT + echo "checked-out-sha=${CHECKED_OUT_SHA}" >> $GITHUB_OUTPUT + + build-and-push-image: + name: Build and push to ACR + needs: set-env + runs-on: ubuntu-22.04 + environment: ${{ needs.set-env.outputs.environment }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + + - name: Azure Container Registry login + uses: docker/login-action@v3 + with: + username: ${{ secrets.AZURE_ACR_CLIENTID }} + password: ${{ secrets.AZURE_ACR_SECRET }} + registry: ${{ secrets.AZURE_ACR_URL }} + + - name: Build and push docker image + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile + build-args: COMMIT_SHA=${{ needs.set-env.outputs.checked-out-sha }} + tags: | + ${{ secrets.AZURE_ACR_URL }}/${{ env.DOCKER_IMAGE }}:${{ needs.set-env.outputs.branch }} + ${{ secrets.AZURE_ACR_URL }}/${{ env.DOCKER_IMAGE }}:${{ needs.set-env.outputs.release }} + ${{ secrets.AZURE_ACR_URL }}/${{ env.DOCKER_IMAGE }}:sha-${{ needs.set-env.outputs.checked-out-sha }} + ${{ secrets.AZURE_ACR_URL }}/${{ env.DOCKER_IMAGE }}:latest + push: true + + create-tag: + name: Tag and release + needs: set-env + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + + - name: Create tag + run: | + git tag ${{ needs.set-env.outputs.release }} + git push origin ${{ needs.set-env.outputs.release }} + + - name: Create release + uses: "actions/github-script@v7" + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + script: | + try { + await github.rest.repos.createRelease({ + draft: ${{ needs.set-env.outputs.environment == 'test' }}, + generate_release_notes: true, + name: "${{ needs.set-env.outputs.release }}", + owner: context.repo.owner, + prerelease: ${{ needs.set-env.outputs.environment == 'test' }}, + repo: context.repo.repo, + tag_name: "${{ needs.set-env.outputs.release }}", + }); + } catch (error) { + core.setFailed(error.message); + } + + deploy-image: + name: Deploy to ${{ needs.set-env.outputs.environment }} + needs: [ build-and-push-image, set-env ] + runs-on: ubuntu-22.04 + environment: ${{ needs.set-env.outputs.environment }} + steps: + - name: Azure login with ACA credentials + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_ACA_CREDENTIALS }} + + - name: Update Azure Container Apps Revision + uses: azure/CLI@v1 + id: azure + with: + azcliversion: 2.45.0 + inlineScript: | + az config set extension.use_dynamic_install=yes_without_prompt + az containerapp update \ + --name ${{ secrets.AZURE_ACA_NAME }} \ + --resource-group ${{ secrets.AZURE_ACA_RESOURCE_GROUP }} \ + --image ${{ secrets.AZURE_ACR_URL }}/${{ env.DOCKER_IMAGE }}:${{ needs.set-env.outputs.release }} \ + --output none + + cypress-tests: + name: Run Cypress Tests + if: needs.set-env.outputs.environment == 'test' || needs.set-env.outputs.environment == 'development' + needs: [ deploy-image, set-env ] + runs-on: ubuntu-22.04 + environment: ${{ needs.set-env.outputs.environment }} + defaults: + run: + working-directory: CypressTests + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + + - name: Setup node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Npm install + run: npm install + + - name: Run cypress + run: npm run cy:run -- --env apiKey="${{ secrets.IDENTIFIERS_API_KEY }}",url="${{ secrets.IDENTIFIERS_API_BASE_URL }}" + + - name: Upload screenshots + if: ${{ failure() }} + uses: actions/upload-artifact@v3 + with: + name: screenshots-${{ needs.set-env.outputs.environment }} + path: screenshots diff --git a/.github/workflows/continuous-integration-javascript.yml b/.github/workflows/continuous-integration-javascript.yml new file mode 100644 index 0000000..e1e4a92 --- /dev/null +++ b/.github/workflows/continuous-integration-javascript.yml @@ -0,0 +1,21 @@ +name: CI / JavaScript + +on: + pull_request: + paths: + - CypressTests + types: [opened, synchronize] + +jobs: + lint: + runs-on: ubuntu-latest + defaults: + working-directory: CypressTests + steps: + - name: checkout code + uses: actions/checkout@v4 + + - name: lint cypress tests + run: | + npm ci + npm run lint diff --git a/.github/workflows/continuous-integration-terraform.yml b/.github/workflows/continuous-integration-terraform.yml new file mode 100644 index 0000000..cb4ca13 --- /dev/null +++ b/.github/workflows/continuous-integration-terraform.yml @@ -0,0 +1,72 @@ +name: CI / Terraform + +on: + push: + branches: main + paths: + - 'terraform/**.tf' + pull_request: + paths: + - 'terraform/**.tf' + +jobs: + terraform-validate: + name: Validate + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Check for terraform version mismatch + run: | + DOTFILE_VERSION=$(cat terraform/.terraform-version) + TERRAFORM_IMAGE_REFERENCES=$(grep "uses: docker://hashicorp/terraform" .github/workflows/continuous-integration-terraform.yml | grep -v TERRAFORM_IMAGE_REFERENCES | wc -l | tr -d ' ') + if [ "$(grep "docker://hashicorp/terraform:${DOTFILE_VERSION}" .github/workflows/continuous-integration-terraform.yml | wc -l | tr -d ' ')" != "$TERRAFORM_IMAGE_REFERENCES" ] + then + echo -e "\033[1;31mError: terraform version in .terraform-version file does not match docker://hashicorp/terraform versions in .github/workflows/continuous-integration-terraform.yml" + exit 1 + fi + + - name: Validate Terraform docs + uses: terraform-docs/gh-actions@v1.0.0 + with: + working-dir: terraform + config-file: .terraform-docs.yml + output-file: README.md + output-method: inject + fail-on-diff: true + + - name: Remove azure backend + run: rm ./terraform/backend.tf + + - name: Run a Terraform init + uses: docker://hashicorp/terraform:1.7.3 + with: + entrypoint: terraform + args: -chdir=terraform init + + - name: Run a Terraform validate + uses: docker://hashicorp/terraform:1.7.3 + with: + entrypoint: terraform + args: -chdir=terraform validate + + - name: Run a Terraform format check + uses: docker://hashicorp/terraform:1.7.3 + with: + entrypoint: terraform + args: -chdir=terraform fmt -check=true -diff=true + + - name: Setup TFLint + uses: terraform-linters/setup-tflint@v4 + with: + tflint_version: v0.44.1 + + - name: Run TFLint + working-directory: terraform + run: tflint -f compact + + - name: Run TFSec + uses: aquasecurity/tfsec-pr-commenter-action@v1.3.1 + with: + github_token: ${{ github.token }} diff --git a/.github/workflows/security-tests.yml b/.github/workflows/security-tests.yml new file mode 100644 index 0000000..04119cf --- /dev/null +++ b/.github/workflows/security-tests.yml @@ -0,0 +1,103 @@ +name: Security scanner tests + +on: + workflow_run: + workflows: ["Deploy to environment"] + types: + - completed + +env: + ZAP_ADDRESS: localhost + ZAP_PORT: 9876 + +jobs: + run-tests-with-zap: + name: Run Cypress tests with OWASP ZAP + environment: development + runs-on: ubuntu-latest + defaults: + run: + working-directory: CypressTests + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Create directory on runner + run: | + mkdir -m 777 ${{ github.workspace }}/zapoutput + + - name: Get latest ZAP container version + run: | + ZAP_VERSION="$(wget -q -O - "https://hub.docker.com/v2/repositories/softwaresecurityproject/zap-stable/tags?page_size=2" | grep -o '"name": *"[^"]*' | grep -o '[^"]*$' | tail -n 1)" + echo "ZAP_VERSION=${ZAP_VERSION}">> $GITHUB_ENV + + - name: Restore ZAP container from cache if exists + id: cache-docker-zap + uses: actions/cache@v4 + with: + path: ~/ci/cache/docker/softwaresecurityproject + key: cache-docker-zap-${{ env.ZAP_VERSION }} + + - name: Use cached image if hit + if: steps.cache-docker-zap.outputs.cache-hit == 'true' + run: docker image load --input ~/ci/cache/docker/softwaresecurityproject/zap-stable-${{ env.ZAP_VERSION }}.tar + + - name: Pull image if no cache hit + if: steps.cache-docker-zap.outputs.cache-hit != 'true' + run: docker pull softwaresecurityproject/zap-stable:latest && mkdir -p ~/ci/cache/docker/softwaresecurityproject && docker image save softwaresecurityproject/zap-stable:latest --output ~/ci/cache/docker/softwaresecurityproject/zap-stable-${{ env.ZAP_VERSION }}.tar + + - name: Start ZAP container + env: + ZAP_PORT: 9876 + run: docker run --name zap_container --rm -d -v ${{ github.workspace }}/zapoutput/:/zap/wrk:rw -u zap -p ${{ env.ZAP_PORT }}:${{ env.ZAP_PORT }} -i softwaresecurityproject/zap-stable zap.sh -daemon -port ${{ env.ZAP_PORT }} -host 0.0.0.0 -config api.key=${{ secrets.ZAP_API_KEY }} -config api.addrs.addr.name=.* -config api.addrs.addr.regex=true -config network.localServers.mainProxy.alpn.enabled=false -config network.localServers.mainProxy.address=0.0.0.0 + + - name: Set up NodeJS + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install dependencies + run: npm ci + + - name: Run tests with scanner + env: + API_KEY: ${{ secrets.TRAMS_API_KEY }} + HTTP_PROXY: http://${{ env.ZAP_ADDRESS }}:${{ env.ZAP_PORT }} + NO_PROXY: "*.google-analytics.com,*.googletagmanager.com,*.microsoftonline.com" + URL: ${{ secrets.TRAMS_API_BASE_URL }} + ZAP: true + ZAP_API_KEY: ${{ secrets.ZAP_API_KEY }} + ZAP_ADDRESS: ${{ env.ZAP_ADDRESS }} + ZAP_PORT: ${{ env.ZAP_PORT }} + run: npm run cy:run -- --env apiKey=$API_KEY,url=$URL + + - name: Get git sha + if: '!cancelled()' + run: | + CHECKED_OUT_SHA="$(git log -1 '--format=format:%H')" + echo "checked_out_sha=${CHECKED_OUT_SHA}" >> $GITHUB_ENV + + - name: Azure login with SPN + if: '!cancelled()' + uses: azure/login@v1 + with: + creds: ${{ secrets.OWASP_AZ_CREDENTIALS }} + + - name: Push report to blob storage + if: '!cancelled()' + uses: azure/CLI@v1 + id: azure + with: + azcliversion: 2.49.0 + inlineScript: | + az storage blob upload \ + --container-name ${{ secrets.OWASP_STORAGE_CONTAINER_NAME }} \ + --account-name ${{ secrets.OWASP_STORAGE_ACCOUNT_NAME }} \ + --file "${{ github.workspace }}/zapoutput/ZAP-Report.html" \ + --name "TramsDataApi/${{ env.checked_out_sha }}/ZAP-Report.html" \ + --auth-mode login \ + --overwrite + + - name: Stop ZAP container + if: always() + run: docker stop zap_container