diff --git a/.editorconfig b/.editorconfig index 9ae301b..42b5ed3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,7 +5,9 @@ end_of_line = lf indent_size = 2 indent_style = space insert_final_newline = true + +[*.{js,json,tf*,yml,yaml}] trim_trailing_whitespace = true -[md] +[*.md] trim_trailing_whitespace = false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee4192d..c814d7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,3 +14,79 @@ jobs: markup-lint: name: Markup uses: ./.github/workflows/reusable-markup-lint.yml + + test-terraform-setup: + name: "Test / Terraform setup action" + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v7 + - name: Install terraform + uses: ./actions/terraform/setup + - name: Verify terraform is installed + run: | + terraform version + command -v terraform + + test-tflint-setup: + name: "Test / TFLint setup action" + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v7 + - name: Setup TFLint + uses: ./actions/tflint/setup + - name: Verify tflint is installed + run: | + tflint --version + command -v tflint + + test-syft-generate-sbom: + name: "Test / Syft generate-sbom action" + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v7 + - name: Generate SBOM for this repository + uses: ./actions/syft/generate-sbom + with: + target-path: actions + output-file: sbom-test.spdx.json + upload-artifact: "false" + - name: Verify SBOM was generated + run: | + test -s sbom-test.spdx.json + grep -q '"spdxVersion"' sbom-test.spdx.json + + test-trivy-scan: + name: "Test / Trivy scan action" + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v7 + - name: Scan a real image with Trivy + uses: ./actions/trivy/scan + with: + scan-type: image + target: devprofr/terraform-backend-mongodb:latest + severity: CRITICAL,HIGH + exit-code: "0" + - name: Scan a directory with Trivy + uses: ./actions/trivy/scan + with: + scan-type: config + target: test/fixtures/terraform + format: sarif + output-file: trivy-test.sarif + severity: HIGH,CRITICAL + - name: Verify SARIF report was generated + run: test -s trivy-test.sarif + + test-terraform-quality: + name: "Test / Terraform quality reusable workflow" + uses: ./.github/workflows/reusable-terraform-quality.yml + with: + working-directory: test/fixtures/terraform + # explicit override: for this same-repo local "./" call, github.workflow_sha's behavior is unverified, + # so pin to a ref we know is fetchable (branch on push, PR merge ref on PRs) + workflow-parts-version: ${{ github.ref }} diff --git a/.github/workflows/reusable-container-publication.yml b/.github/workflows/reusable-container-publication.yml index c82fc17..cdad238 100644 --- a/.github/workflows/reusable-container-publication.yml +++ b/.github/workflows/reusable-container-publication.yml @@ -58,7 +58,7 @@ on: required: false default: ubuntu-latest workflow-parts-version: - description: GitHub workflow parts version (branch/tag/SHA) + description: GitHub workflow parts version (branch/tag/SHA). Should match the ref/tag/SHA the caller pinned for this reusable workflow itself. type: string required: false default: main @@ -100,9 +100,9 @@ jobs: echo "$SECRET_VARS" >> "$GITHUB_ENV" fi - name: Clone repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Checkout workflow parts - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: devpro/github-workflow-parts ref: ${{ inputs.workflow-parts-version }} @@ -129,12 +129,11 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max build-args: ${{ inputs.extra-build-arguments }} - # TODO: replace with a more secure action - # - name: Generate SBOM with Syft - # uses: anchore/sbom-action@v0 - # continue-on-error: true - # with: - # image: ${{ env.IMAGE_REF }} + - name: Generate SBOM with Syft + uses: ./workflow-parts/actions/syft/generate-sbom + continue-on-error: true + with: + target-image: ${{ env.IMAGE_REF }} - name: Push latest tag if: ${{ inputs.create-latest }} run: | diff --git a/.github/workflows/reusable-container-scan.yml b/.github/workflows/reusable-container-scan.yml index 4ad6c29..33405b3 100644 --- a/.github/workflows/reusable-container-scan.yml +++ b/.github/workflows/reusable-container-scan.yml @@ -49,6 +49,11 @@ on: type: boolean required: false default: true + workflow-parts-version: + description: "GitHub workflow parts version (branch/tag/SHA). Should match the ref/tag/SHA the caller pinned for this reusable workflow itself." + type: string + required: false + default: "main" working-directory: description: Working directory type: string @@ -64,7 +69,13 @@ jobs: working-directory: ${{ inputs.working-directory }} steps: - name: Clone repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 + - name: Checkout workflow parts + uses: actions/checkout@v7 + with: + repository: devpro/github-workflow-parts + ref: ${{ inputs.workflow-parts-version }} + path: workflow-parts - name: Build container image run: docker build . --file ${{ inputs.image-definition }} --tag ${{ env.IMAGE_REF }} shell: bash @@ -80,14 +91,15 @@ jobs: # nv-scanner-image: neuvector/scanner:5 - name: Scan container image with Trivy if: ${{ inputs.trivy-enabled }} - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 + uses: ./workflow-parts/actions/trivy/scan with: - image-ref: ${{ env.IMAGE_REF }} - format: 'table' - exit-code: '1' - ignore-unfixed: true - vuln-type: 'os,library' - severity: 'CRITICAL,HIGH' + scan-type: image + target: ${{ env.IMAGE_REF }} + format: table + exit-code: "1" + ignore-unfixed: "true" + pkg-types: os,library + severity: CRITICAL,HIGH env: GITHUB_TOKEN: ${{ github.token }} IMAGE_REF: ${{ inputs.image-path }}/${{ inputs.image-name }}:${{ inputs.image-tag }} diff --git a/.github/workflows/reusable-dotnet-quality.yml b/.github/workflows/reusable-dotnet-quality.yml index 58c705b..2f31708 100644 --- a/.github/workflows/reusable-dotnet-quality.yml +++ b/.github/workflows/reusable-dotnet-quality.yml @@ -48,6 +48,11 @@ on: type: boolean required: false default: false + sonar-cpd-exclusions: + description: Files or directories that should be excluded from Sonar code duplication analysis + type: string + required: false + default: "**/*Generated*.cs" sonar-exclusions: description: Files or directories that should be excluded from Sonar analysis type: string @@ -74,7 +79,7 @@ on: required: false default: "" workflow-parts-version: - description: GitHub workflow parts version (branch/tag/SHA) + description: GitHub workflow parts version (branch/tag/SHA). Should match the ref/tag/SHA the caller pinned for this reusable workflow itself. type: string required: false default: main @@ -128,11 +133,11 @@ jobs: run: git config --global core.autocrlf false shell: bash - name: Clone repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Checkout workflow parts - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: devpro/github-workflow-parts ref: ${{ inputs.workflow-parts-version }} @@ -155,6 +160,7 @@ jobs: uses: ./workflow-parts/actions/dotnet/build-test-sonar with: dotnet-test-args: ${{ inputs.dotnet-test-args }} + sonar-cpd-exclusions: ${{ inputs.sonar-cpd-exclusions }} sonar-exclusions: ${{ inputs.sonar-exclusions }} sonar-host-url: ${{ inputs.sonar-host-url }} sonar-organization: ${{ inputs.sonar-organization }} @@ -177,17 +183,12 @@ jobs: if: ${{ inputs.fossa-enabled && steps.fossa.outcome == 'success' }} run: echo '${{ steps.fossa.outputs.report }}' > report/fossa.html continue-on-error: true - # TODO: replace with more secure action - # - name: Generate SBOM with Syft - # uses: anchore/sbom-action@v0 - # # with: - # # path: . # Or Dockerfile path - # # format: spdx-json # Or cyclonedx-json - # # output-file: sbom.json - # # upload-artifact: true # Auto-upload to workflow artifacts + - name: Generate SBOM with Syft + uses: ./workflow-parts/actions/syft/generate-sbom + continue-on-error: true - name: Archive test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: dotnet-test-results path: | diff --git a/.github/workflows/reusable-markup-lint.yml b/.github/workflows/reusable-markup-lint.yml index d0f0b60..54e8dd1 100644 --- a/.github/workflows/reusable-markup-lint.yml +++ b/.github/workflows/reusable-markup-lint.yml @@ -28,7 +28,7 @@ jobs: working-directory: ${{ inputs.working-directory }} steps: - name: Check out repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Lint Markdown files run: npx markdownlint-cli2 "**/*.md" - name: Lint YAML files diff --git a/.github/workflows/reusable-terraform-deployment.yml b/.github/workflows/reusable-terraform-deployment.yml index 0088b82..c2bc2ed 100644 --- a/.github/workflows/reusable-terraform-deployment.yml +++ b/.github/workflows/reusable-terraform-deployment.yml @@ -37,7 +37,7 @@ on: required: false default: "" workflow-parts-version: - description: "GitHub workflow parts version (branch/tag/SHA)" + description: "GitHub workflow parts version (branch/tag/SHA). Should match the ref/tag/SHA the caller pinned for this reusable workflow itself." type: string required: false default: "main" @@ -90,9 +90,9 @@ jobs: - 8080:8080 steps: - name: Clone repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Checkout workflow parts - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: devpro/github-workflow-parts ref: ${{ inputs.workflow-parts-version }} @@ -120,13 +120,13 @@ jobs: run: | ${{ inputs.custom-commands }} - name: Cache Terraform plugins - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: | ~/.terraform.d/plugin-cache key: terraform-${{ hashFiles('**/.terraform.lock.hcl') }} - name: Install terraform - uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e + uses: ./workflow-parts/actions/terraform/setup - name: Terraform init run: terraform init - name: Terraform validate diff --git a/.github/workflows/reusable-terraform-quality.yml b/.github/workflows/reusable-terraform-quality.yml index c6b2d08..4e41fbc 100644 --- a/.github/workflows/reusable-terraform-quality.yml +++ b/.github/workflows/reusable-terraform-quality.yml @@ -18,6 +18,11 @@ on: type: string required: false default: "ubuntu-latest" + workflow-parts-version: + description: "GitHub workflow parts version (branch/tag/SHA). Should match the ref/tag/SHA the caller pinned for this reusable workflow itself." + type: string + required: false + default: "main" jobs: terraform-validate: @@ -28,15 +33,21 @@ jobs: working-directory: ${{ inputs.working-directory }} steps: - name: Clone repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 + - name: Checkout workflow parts + uses: actions/checkout@v7 + with: + repository: devpro/github-workflow-parts + ref: ${{ inputs.workflow-parts-version }} + path: workflow-parts - name: Cache Terraform plugins - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: | ~/.terraform.d/plugin-cache key: terraform-${{ hashFiles('**/.terraform.lock.hcl') }} - name: Install terraform - uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e + uses: ./workflow-parts/actions/terraform/setup - name: Check Terraform format run: terraform fmt -recursive -check - name: Terraform Init @@ -59,15 +70,15 @@ jobs: # sarif_file: results.sarif # TFLint is a pluggable terraform linter (ref. https://github.com/terraform-linters/tflint) - name: Cache TFLint plugins - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ~/.tflint.d/plugins key: tflint-${{ hashFiles('**/.tflint.hcl') }} - name: Setup TFLint - uses: terraform-linters/setup-tflint@b480b8fcdaa6f2c577f8e4fa799e89e756bb7c93 + uses: ./workflow-parts/actions/tflint/setup with: - # ref. https://github.com/terraform-linters/tflint/pkgs/container/tflint - tflint_version: v0.60.0 + # ref. https://github.com/terraform-linters/tflint/releases + tflint-version: v0.60.0 - name: Initialize TFLint run: tflint --init --recursive env: @@ -76,16 +87,15 @@ jobs: - name: Run TFLint run: tflint --recursive --format compact - name: Run Trivy IaC scan - # v0.35.0 - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 + uses: ./workflow-parts/actions/trivy/scan with: - scan-type: "config" - format: "sarif" - output: "trivy-results.sarif" - ignore-unfixed: true - severity: "HIGH,CRITICAL" + scan-type: config + target: . + format: sarif + output-file: trivy-results.sarif + severity: HIGH,CRITICAL - name: Upload SARIF as artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: always() with: name: security-results diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..372dd25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.terraform/ +*.tfplan diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index 5176f75..dae97fc 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -4,5 +4,6 @@ ignores: config: # ref. https://github.com/DavidAnson/markdownlint default: true + MD009: false MD013: line_length: 240 diff --git a/README.md b/README.md index d4efd66..cf9a308 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,16 @@ [![CI](https://github.com/devpro/github-workflow-parts/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/devpro/github-workflow-parts/actions/workflows/ci.yml) -GitHub workflow components you can trust for your repositories. -Keep your pipelines DRY! (Don't Repeat Yourself) +GitHub workflow components for code repositories. + +> [!IMPORTANT] +> Keep your pipelines **safe** and **DRY**! ## Reusable workflows -> Rather than copying and pasting from one workflow to another, you can make workflows reusable ([Reusing workflow configurations](https://docs.github.com/en/actions/concepts/workflows-and-actions/reusing-workflow-configurations)) +> [!TIP] +> Rather than copying and pasting from one workflow to another, you can make workflows reusable. +> Source: [Reusing workflow configurations](https://docs.github.com/en/actions/concepts/workflows-and-actions/reusing-workflow-configurations) Containers: @@ -29,7 +33,9 @@ Terraform: ## Composite actions -> Composite actions allow you to collect a series of workflow job steps into a single action which you can then run as a single job step in multiple workflows ([Creating a composite action](https://docs.github.com/en/actions/tutorials/create-actions/create-a-composite-action)) +> [!TIP] +> Composite actions allow you to collect a series of workflow job steps into a single action which you can then run as a single job step in multiple workflows. +> Source: [Creating a composite action](https://docs.github.com/en/actions/tutorials/create-actions/create-a-composite-action) Containers: @@ -45,3 +51,16 @@ MongoDB: - [Add your runner IP address to Atlas access list](actions/mongodb-atlas/add-runner-ip/action.yml) - [Start a server in your pipeline](actions/mongodb/start/action.yml) + +SBOM: + +- [Generate a SBOM with Syft](actions/syft/generate-sbom/action.yml) + +Terraform: + +- [Setup Terraform](actions/terraform/setup/action.yml) +- [Setup TFLint](actions/tflint/setup/action.yml) + +> [!NOTE] +> These composite actions download official release binaries directly (with SHA256 checksum verification) instead of relying on third-party marketplace actions, reducing supply-chain exposure. +> Linux runners only (for example `ubuntu-latest`). diff --git a/actions/dotnet/build-test-sonar/action.yml b/actions/dotnet/build-test-sonar/action.yml index 78ac516..5cadcc9 100644 --- a/actions/dotnet/build-test-sonar/action.yml +++ b/actions/dotnet/build-test-sonar/action.yml @@ -10,6 +10,10 @@ inputs: description: Java version that will be installed (for Sonar CLI) required: false default: "21" + sonar-cpd-exclusions: + description: Files or directories that should be excluded from Sonar code duplication analysis + required: false + default: "**/*Generated*.cs" sonar-exclusions: description: Files or directories that should be excluded from Sonar analysis required: false @@ -48,14 +52,14 @@ runs: java-version: ${{ inputs.java-version }} distribution: "zulu" - name: Cache Sonar packages - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ~/sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Cache Sonar scanner id: cache-sonar-scanner - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ./.sonar/scanner key: ${{ runner.os }}-sonar-scanner @@ -74,7 +78,7 @@ runs: /n:"${{ inputs.sonar-project-name }}" \ /d:sonar.token="${{ inputs.sonar-token}}" \ /d:sonar.host.url="${{ inputs.sonar-host-url }}" \ - /d:sonar.cpd.exclusions="**/*Generated*.cs,${{ inputs.report-folder }}/**" \ + /d:sonar.cpd.exclusions="${{ inputs.report-folder }}/**,${{ inputs.sonar-cpd-exclusions }}" \ /d:sonar.exclusions="${{ inputs.report-folder }}/**,${{ inputs.sonar-exclusions }}" \ /d:sonar.coverageReportPaths="${{ inputs.report-folder }}/SonarQube.xml" \ /d:sonar.cs.vstest.reportsPaths="${{ inputs.report-folder }}/TestResults/*.trx" diff --git a/actions/dotnet/install-lint-restore/action.yml b/actions/dotnet/install-lint-restore/action.yml index 41ab806..a595889 100644 --- a/actions/dotnet/install-lint-restore/action.yml +++ b/actions/dotnet/install-lint-restore/action.yml @@ -11,7 +11,7 @@ runs: using: "composite" steps: - name: Install .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: ${{ inputs.dotnet-version }} - name: Install .NET linters diff --git a/actions/dotnet/pack-push/action.yml b/actions/dotnet/pack-push/action.yml index a1de296..324e291 100644 --- a/actions/dotnet/pack-push/action.yml +++ b/actions/dotnet/pack-push/action.yml @@ -22,7 +22,7 @@ runs: using: "composite" steps: - name: Install .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: ${{ inputs.dotnet-version }} - name: Package diff --git a/actions/syft/generate-sbom/action.yml b/actions/syft/generate-sbom/action.yml new file mode 100644 index 0000000..c4872ba --- /dev/null +++ b/actions/syft/generate-sbom/action.yml @@ -0,0 +1,89 @@ +name: Generate SBOM with Syft +description: | + Installs Syft by downloading the official binary from its GitHub release, verifying its SHA256 checksum, + and uses it to generate a Software Bill of Materials for a container image or a source directory + (replacement for anchore/sbom-action, to reduce third-party GitHub Action supply-chain risk). + Linux runners only (for example ubuntu-latest). + +inputs: + syft-version: + description: Version of Syft to install (check latest from https://github.com/anchore/syft/releases) + required: false + default: "1.49.0" + target-image: + description: Container image reference to scan (mutually exclusive with target-path) + required: false + default: "" + target-path: + description: Source directory to scan, used only when target-image is not set + required: false + default: "." + output-format: + description: SBOM format (see https://github.com/anchore/syft#supported-output-formats) + required: false + default: "spdx-json" + output-file: + description: Path of the generated SBOM file + required: false + default: "sbom.spdx.json" + upload-artifact: + description: Upload the generated SBOM as a workflow artifact + required: false + default: "true" + artifact-name: + description: Name of the uploaded workflow artifact + required: false + default: "sbom" + +runs: + using: "composite" + steps: + - name: Install Syft + shell: bash + env: + SYFT_VERSION: ${{ inputs.syft-version }} + SYFT_TARBALL: syft_${{ inputs.syft-version }}_linux_amd64.tar.gz + SYFT_CHECKSUMS: syft_${{ inputs.syft-version }}_checksums.txt + run: | + BASE_URL="https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}" + + echo "Downloading Syft binary and checksums..." + curl -sSL -O "${BASE_URL}/${SYFT_TARBALL}" + curl -sSL -O "${BASE_URL}/${SYFT_CHECKSUMS}" + + echo "Verifying checksum..." + grep " ${SYFT_TARBALL}$" "${SYFT_CHECKSUMS}" | sha256sum --check --status + + if [ $? -eq 0 ]; then + echo "Verification successful!" + tar -xzf "${SYFT_TARBALL}" syft + chmod +x syft + sudo mv syft /usr/local/bin/syft + rm -f "${SYFT_TARBALL}" "${SYFT_CHECKSUMS}" + syft version + else + echo "ERROR: Checksum verification failed!" >&2 + exit 1 + fi + - name: Generate SBOM + shell: bash + env: + TARGET_IMAGE: ${{ inputs.target-image }} + TARGET_PATH: ${{ inputs.target-path }} + OUTPUT_FORMAT: ${{ inputs.output-format }} + OUTPUT_FILE: ${{ inputs.output-file }} + run: | + if [ -n "$TARGET_IMAGE" ]; then + SOURCE="$TARGET_IMAGE" + else + SOURCE="dir:${TARGET_PATH}" + fi + + echo "Scanning ${SOURCE} with Syft..." + syft "$SOURCE" -o "${OUTPUT_FORMAT}=${OUTPUT_FILE}" + - name: Upload SBOM as artifact + if: ${{ inputs.upload-artifact == 'true' }} + uses: actions/upload-artifact@v7 + with: + name: ${{ inputs.artifact-name }} + path: ${{ inputs.output-file }} diff --git a/actions/terraform/setup/action.yml b/actions/terraform/setup/action.yml new file mode 100644 index 0000000..21d6590 --- /dev/null +++ b/actions/terraform/setup/action.yml @@ -0,0 +1,42 @@ +name: Setup Terraform +description: | + Installs the Terraform CLI by downloading the official binary from HashiCorp's release server and verifying its SHA256 checksum + (replacement for hashicorp/setup-terraform, to reduce third-party GitHub Action supply-chain risk). + Linux runners only (for example ubuntu-latest). + +inputs: + terraform-version: + description: Version of Terraform to install (check latest from https://releases.hashicorp.com/terraform/) + required: false + default: "1.15.8" + +runs: + using: "composite" + steps: + - name: Install Terraform + shell: bash + env: + TERRAFORM_VERSION: ${{ inputs.terraform-version }} + TERRAFORM_ZIP: terraform_${{ inputs.terraform-version }}_linux_amd64.zip + TERRAFORM_SHA256SUMS: terraform_${{ inputs.terraform-version }}_SHA256SUMS + run: | + BASE_URL="https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}" + + echo "Downloading Terraform binary and checksums..." + curl -sSL -O "${BASE_URL}/${TERRAFORM_ZIP}" + curl -sSL -O "${BASE_URL}/${TERRAFORM_SHA256SUMS}" + + echo "Verifying checksum..." + grep " ${TERRAFORM_ZIP}$" "${TERRAFORM_SHA256SUMS}" | sha256sum --check --status + + if [ $? -eq 0 ]; then + echo "Verification successful!" + unzip -o "${TERRAFORM_ZIP}" + chmod +x terraform + sudo mv terraform /usr/local/bin/terraform + rm -f "${TERRAFORM_ZIP}" "${TERRAFORM_SHA256SUMS}" + terraform version + else + echo "ERROR: Checksum verification failed!" >&2 + exit 1 + fi diff --git a/actions/tflint/setup/action.yml b/actions/tflint/setup/action.yml new file mode 100644 index 0000000..770a67b --- /dev/null +++ b/actions/tflint/setup/action.yml @@ -0,0 +1,41 @@ +name: Setup TFLint +description: | + Installs TFLint by downloading the official binary from its GitHub release and verifying its SHA256 checksum (replacement for terraform-linters/setup-tflint, + to reduce third-party GitHub Action supply-chain risk). + Linux runners only (for example ubuntu-latest). + +inputs: + tflint-version: + description: Version of TFLint to install (check latest from https://github.com/terraform-linters/tflint/releases) + required: false + default: "v0.60.0" + +runs: + using: "composite" + steps: + - name: Install TFLint + shell: bash + env: + TFLINT_VERSION: ${{ inputs.tflint-version }} + TFLINT_ZIP: tflint_linux_amd64.zip + run: | + BASE_URL="https://github.com/terraform-linters/tflint/releases/download/${TFLINT_VERSION}" + + echo "Downloading TFLint binary and checksums..." + curl -sSL -O "${BASE_URL}/${TFLINT_ZIP}" + curl -sSL -O "${BASE_URL}/checksums.txt" + + echo "Verifying checksum..." + grep " ${TFLINT_ZIP}$" checksums.txt | sha256sum --check --status + + if [ $? -eq 0 ]; then + echo "Verification successful!" + unzip -o "${TFLINT_ZIP}" + chmod +x tflint + sudo mv tflint /usr/local/bin/tflint + rm -f "${TFLINT_ZIP}" checksums.txt + tflint --version + else + echo "ERROR: Checksum verification failed!" >&2 + exit 1 + fi diff --git a/actions/trivy/scan/action.yml b/actions/trivy/scan/action.yml new file mode 100644 index 0000000..98d7286 --- /dev/null +++ b/actions/trivy/scan/action.yml @@ -0,0 +1,106 @@ +name: Scan with Trivy +description: | + Installs Trivy by downloading the official binary from its GitHub release and verifying its SHA256 checksum, + then scans a container image or a source directory for vulnerabilities or misconfigurations (replacement for aquasecurity/trivy-action, + to reduce third-party GitHub Action supply-chain risk). + Linux runners only (for example ubuntu-latest). + +inputs: + trivy-version: + description: Version of Trivy to install (check latest from https://github.com/aquasecurity/trivy/releases) + required: false + default: "0.72.0" + scan-type: + description: What to scan, "image" (container image reference) or "config" (misconfiguration/IaC scan of a directory) + required: false + default: "image" + target: + description: Container image reference (scan-type "image") or directory path (scan-type "config") to scan + required: true + format: + description: Trivy report format (table, json, sarif, cyclonedx, spdx-json...) + required: false + default: "table" + output-file: + description: Path of the generated report file (empty prints to stdout) + required: false + default: "" + severity: + description: Comma-separated list of severities to report + required: false + default: "CRITICAL,HIGH" + exit-code: + description: Exit code to use when findings matching the severity filter are detected (0 = never fail the step) + required: false + default: "0" + ignore-unfixed: + description: Only report vulnerabilities with a fix available (scan-type "image" only) + required: false + default: "false" + pkg-types: + description: Comma-separated package types to scan, os and/or library (scan-type "image" only) + required: false + default: "os,library" + +runs: + using: "composite" + steps: + - name: Install Trivy + shell: bash + env: + TRIVY_VERSION: ${{ inputs.trivy-version }} + TRIVY_TARBALL: trivy_${{ inputs.trivy-version }}_Linux-64bit.tar.gz + TRIVY_CHECKSUMS: trivy_${{ inputs.trivy-version }}_checksums.txt + run: | + BASE_URL="https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}" + + echo "Downloading Trivy binary and checksums..." + curl -sSL -O "${BASE_URL}/${TRIVY_TARBALL}" + curl -sSL -O "${BASE_URL}/${TRIVY_CHECKSUMS}" + + echo "Verifying checksum..." + grep " ${TRIVY_TARBALL}$" "${TRIVY_CHECKSUMS}" | sha256sum --check --status + + if [ $? -eq 0 ]; then + echo "Verification successful!" + tar -xzf "${TRIVY_TARBALL}" trivy + chmod +x trivy + sudo mv trivy /usr/local/bin/trivy + rm -f "${TRIVY_TARBALL}" "${TRIVY_CHECKSUMS}" + trivy version + else + echo "ERROR: Checksum verification failed!" >&2 + exit 1 + fi + - name: Run Trivy scan + shell: bash + env: + SCAN_TYPE: ${{ inputs.scan-type }} + TARGET: ${{ inputs.target }} + FORMAT: ${{ inputs.format }} + OUTPUT_FILE: ${{ inputs.output-file }} + SEVERITY: ${{ inputs.severity }} + EXIT_CODE: ${{ inputs.exit-code }} + IGNORE_UNFIXED: ${{ inputs.ignore-unfixed }} + PKG_TYPES: ${{ inputs.pkg-types }} + run: | + args=("$SCAN_TYPE" --format "$FORMAT" --severity "$SEVERITY" --exit-code "$EXIT_CODE") + + if [ -n "$OUTPUT_FILE" ]; then + args+=(--output "$OUTPUT_FILE") + fi + + # ignore-unfixed and pkg-types only make sense for vulnerability (image) scans + if [ "$SCAN_TYPE" = "image" ]; then + if [ "$IGNORE_UNFIXED" = "true" ]; then + args+=(--ignore-unfixed) + fi + if [ -n "$PKG_TYPES" ]; then + args+=(--pkg-types "$PKG_TYPES") + fi + fi + + args+=("$TARGET") + + echo "Running: trivy ${args[*]}" + trivy "${args[@]}" diff --git a/test/fixtures/terraform/.terraform.lock.hcl b/test/fixtures/terraform/.terraform.lock.hcl new file mode 100644 index 0000000..be7a6ca --- /dev/null +++ b/test/fixtures/terraform/.terraform.lock.hcl @@ -0,0 +1,23 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/local" { + version = "2.9.0" + constraints = "~> 2.5" + hashes = [ + "h1:9rBZCMNpxKwMlRbWH2QpwD3kqUCAejdOZQ/aiiDObXQ=", + "zh:0baa4566cf77f1ff52f4293d1c8536202dd23edc197c3196413a28343c3ac3a0", + "zh:16b5559c3c07088ddad11a9bb9e9c0799999363c2958e9a5be2bcbbf2cd9ca64", + "zh:197c79015a10d1cce904a8ea722cbc750c42aeae2da53f44a6a0751d9fd1aa90", + "zh:29d0b03e5343a80677ebfeb2e2c31cbe4b1f65e736e53417454a4277fec2544c", + "zh:4896bfa6cf1d2fd562b47ef2e87f47862ae92a04f8ad5d764380f0c6653473b8", + "zh:531f8529cbca49f681883e57761a05a8398afaef6d1ab0d205d26bf12f4428e8", + "zh:6aaf5011d83161c86d2bfb80c0923ec934e578288758da2f37acb7aec129004b", + "zh:7430275253d3d3c40aa6179e0ec0d63212874dbbc06c5a51b9d07ec590f9756c", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:be17dc611e95e26cdf6cad79dfccf1064f0e32032a2efeb939a9bbe7fb1cbfe9", + "zh:f0e3b0aa644202e1d79d2000dca91f6019425da71e9800fa23f27e51c034f195", + "zh:f62bae4519e4ead49182ddc8afe8cf61e2a4c3ba3973b0fbba967736a2696aa3", + "zh:fcafa360a5b0b96244f26f4e3a6d642b716a376557142c2442ff2fb12d11da18", + ] +} diff --git a/test/fixtures/terraform/.tflint.hcl b/test/fixtures/terraform/.tflint.hcl new file mode 100644 index 0000000..427121c --- /dev/null +++ b/test/fixtures/terraform/.tflint.hcl @@ -0,0 +1,4 @@ +plugin "terraform" { + enabled = true + preset = "recommended" +} diff --git a/test/fixtures/terraform/main.tf b/test/fixtures/terraform/main.tf new file mode 100644 index 0000000..e7a3b55 --- /dev/null +++ b/test/fixtures/terraform/main.tf @@ -0,0 +1,20 @@ +# Minimal, valid Terraform configuration used by this repository's own CI (see .github/workflows/ci.yml) +# to exercise the reusable-terraform-quality.yml workflow end-to-end: terraform fmt/init/validate, +# Checkov, TFLint and Trivy all run against it. +# It intentionally has no cloud provider dependency so it can run without any credentials. + +terraform { + required_version = ">= 1.9.0" + + required_providers { + local = { + source = "hashicorp/local" + version = "~> 2.5" + } + } +} + +resource "local_file" "example" { + filename = "${path.module}/example.txt" + content = "Hello from the github-workflow-parts CI test fixture.\n" +}