diff --git a/.github/workflows/changelog-release.yml b/.github/workflows/changelog-release.yml index 5cd9622..f074b79 100644 --- a/.github/workflows/changelog-release.yml +++ b/.github/workflows/changelog-release.yml @@ -121,7 +121,7 @@ jobs: grep -o '"version": *"[^"]*"' "$dir/$file" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/' ;; VERSION) - cat "$dir/$file" | tr -d '[:space:]' + tr -d '[:space:]' < "$dir/$file" ;; esac } @@ -378,7 +378,7 @@ jobs: git add -A if ! git diff --cached --quiet; then git commit -m "$COMMIT_MESSAGE" - git push origin ${GITHUB_REF#refs/heads/} + git push origin "${GITHUB_REF#refs/heads/}" fi - name: Create and push tags diff --git a/.github/workflows/docker-build-push-ecr.yml b/.github/workflows/docker-build-push-ecr.yml index b0f7269..103c629 100644 --- a/.github/workflows/docker-build-push-ecr.yml +++ b/.github/workflows/docker-build-push-ecr.yml @@ -100,7 +100,7 @@ jobs: VERSION=$(echo "${TAG}" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' || echo "${TAG}") TAGS="${REGISTRY}/${IMAGE}:${VERSION}" - echo "tags=${TAGS}" >> $GITHUB_OUTPUT + echo "tags=${TAGS}" >> "$GITHUB_OUTPUT" echo "Generated tags: ${TAGS}" - name: Build and push Docker image @@ -119,7 +119,7 @@ jobs: - name: Extract image digest id: digest run: | - DIGEST="${{ steps.build.outputs.digest }}" + DIGEST=$(echo '${{ steps.build.outputs.metadata }}' | jq -r '."containerimage.digest" // empty') if [[ -z "$DIGEST" ]]; then echo "::error::digest missing from build output" exit 1 diff --git a/.github/workflows/ecr-security-scan.yml b/.github/workflows/ecr-security-scan.yml index fabd2cc..ed695f5 100644 --- a/.github/workflows/ecr-security-scan.yml +++ b/.github/workflows/ecr-security-scan.yml @@ -56,9 +56,9 @@ jobs: - name: Get latest tags and scan images id: scan run: | - IMAGE_NAMES='${{ inputs.image_names }}' - REGISTRY='${{ inputs.ecr_registry }}' - SEVERITY='${{ inputs.severity }}' + IMAGE_NAMES="${{ inputs.image_names }}" + REGISTRY="${{ inputs.ecr_registry }}" + SEVERITY="${{ inputs.severity }}" VULNERABILITIES_FOUND=false REPORT="" @@ -67,6 +67,7 @@ jobs: echo "Finding latest tag for ${IMAGE_NAME}..." # Get latest semver tag from ECR Public + # shellcheck disable=SC2016 LATEST_TAG=$(aws ecr-public describe-image-tags \ --repository-name "${IMAGE_NAME}" \ --region us-east-1 \ @@ -99,11 +100,11 @@ jobs: echo " Critical: ${CRITICAL}, High: ${HIGH}" done - echo "vulnerabilities_found=${VULNERABILITIES_FOUND}" >> $GITHUB_OUTPUT + echo "vulnerabilities_found=${VULNERABILITIES_FOUND}" >> "$GITHUB_OUTPUT" # Save report for Slack (escape newlines for JSON) REPORT_ESCAPED=$(echo -e "$REPORT" | sed 's/"/\\"/g' | tr '\n' '|' | sed 's/|/\\n/g') - echo "report=${REPORT_ESCAPED}" >> $GITHUB_OUTPUT + echo "report=${REPORT_ESCAPED}" >> "$GITHUB_OUTPUT" - name: Send Slack alert if: steps.scan.outputs.vulnerabilities_found == 'true' diff --git a/.github/workflows/pr-checks-actions.yml b/.github/workflows/pr-checks-actions.yml new file mode 100644 index 0000000..b863203 --- /dev/null +++ b/.github/workflows/pr-checks-actions.yml @@ -0,0 +1,44 @@ +name: pr-checks-actions + +on: + pull_request: + +permissions: + contents: read + +jobs: + actionlint: + name: Validate Actions syntax + runs-on: ubuntu-24.04 + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install actionlint + run: | + wget -q https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz + tar xzf actionlint_1.7.12_linux_amd64.tar.gz actionlint + + - name: Run actionlint + run: | + ./actionlint -ignore 'unknown permission scope "models"' + + secret-scan: + name: Scan for credentials + runs-on: ubuntu-24.04 + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Run Trivy secret scan + uses: aquasecurity/trivy-action@v0.36.0 + with: + scan-type: 'fs' + scan-ref: '.' + scanners: 'secret' + format: 'table' + exit-code: '1' diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index d678119..91bcc7f 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -22,8 +22,9 @@ jobs: - name: Post changelog preview comment env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HEAD_REF: ${{ github.head_ref }} run: | - git checkout -b "${{ github.head_ref }}" + git checkout -b "$HEAD_REF" unset GITHUB_ACTIONS npx --yes semantic-release-github-pr \ No newline at end of file diff --git a/.github/workflows/readme-ai-v2.yml b/.github/workflows/readme-ai-v2.yml index 5a90ba7..7e3fff5 100644 --- a/.github/workflows/readme-ai-v2.yml +++ b/.github/workflows/readme-ai-v2.yml @@ -45,7 +45,7 @@ jobs: runs-on: ubuntu-24.04 permissions: contents: write - models: read + models: read # actionlint:ignore:permissions env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: @@ -92,13 +92,13 @@ jobs: if [ -z "$CHANGED_FILES" ]; then echo "No matching files changed" - echo "dirs=" >> $GITHUB_OUTPUT + echo "dirs=" >> "$GITHUB_OUTPUT" exit 0 fi DIRS=$(echo "$CHANGED_FILES" | xargs -I {} dirname {} | sort -u | tr '\n' ' ') echo "Modified directories: $DIRS" - echo "dirs=$DIRS" >> $GITHUB_OUTPUT + echo "dirs=$DIRS" >> "$GITHUB_OUTPUT" - name: Generate READMEs (changed directories) if: ${{ !inputs.generate_all && steps.changes.outputs.dirs != '' }} @@ -108,6 +108,7 @@ jobs: TYPE_ARG="--type ${{ inputs.generator_type }}" fi + # shellcheck disable=SC2086 node .actions-scripts/.github/scripts/generate-readme-v2.js \ --verbose \ $TYPE_ARG \ @@ -128,6 +129,7 @@ jobs: TYPE_ARG="--type ${{ inputs.generator_type }}" fi + # shellcheck disable=SC2086 node .actions-scripts/.github/scripts/generate-readme-v2.js \ --all \ --base-dir "${{ inputs.base_dir }}" \ diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index 5013fa6..e7db2a2 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -28,7 +28,7 @@ jobs: SEVERITY: ${{ inputs.severity }} run: | if [ -n "$SCRIPT_DIRS" ]; then - find $SCRIPT_DIRS -type f | xargs shellcheck --severity="$SEVERITY" + find "$SCRIPT_DIRS" -type f -print0 | xargs -0 shellcheck --severity="$SEVERITY" else - find . -name "*.sh" -not -path "./.git/*" | xargs shellcheck --severity="$SEVERITY" + find . -name "*.sh" -not -path "./.git/*" -print0 | xargs -0 shellcheck --severity="$SEVERITY" fi diff --git a/.github/workflows/tfsec.yml b/.github/workflows/tfsec.yml index 24b7531..1e55427 100644 --- a/.github/workflows/tfsec.yml +++ b/.github/workflows/tfsec.yml @@ -40,13 +40,13 @@ jobs: if [ -z "$DIRS" ]; then echo "No Terraform files found" - echo "has_tf_files=false" >> $GITHUB_OUTPUT + echo "has_tf_files=false" >> "$GITHUB_OUTPUT" exit 0 fi echo "Found directories: $DIRS" - echo "has_tf_files=true" >> $GITHUB_OUTPUT - echo "dirs=$DIRS" >> $GITHUB_OUTPUT + echo "has_tf_files=true" >> "$GITHUB_OUTPUT" + echo "dirs=$DIRS" >> "$GITHUB_OUTPUT" - name: Install tfsec if: steps.modules.outputs.has_tf_files == 'true' @@ -66,7 +66,7 @@ jobs: fi echo "::endgroup::" done - echo "exit_code=$EXIT_CODE" >> $GITHUB_OUTPUT + echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT" continue-on-error: true - name: Generate SARIF report diff --git a/.github/workflows/trivy-tofu-scan.yml b/.github/workflows/trivy-tofu-scan.yml new file mode 100644 index 0000000..5ab3028 --- /dev/null +++ b/.github/workflows/trivy-tofu-scan.yml @@ -0,0 +1,80 @@ +name: trivy-tofu-scan + +on: + workflow_call: + inputs: + upload_sarif: + description: 'Upload SARIF results to GitHub Security tab' + required: false + type: boolean + default: true + +permissions: + contents: read + security-events: write + +jobs: + trivy-iac: + name: Trivy IaC Scan + runs-on: ubuntu-24.04 + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Find OpenTofu/Terraform files + id: find + run: | + DIRS=$(find . -name "*.tf" -not -path "./.terraform/*" -exec dirname {} \; | sort -u | tr '\n' ' ') + if [ -z "$DIRS" ]; then + echo "No .tf files found, skipping scan" + echo "has_tf_files=false" >> "$GITHUB_OUTPUT" + else + echo "Found .tf files in: $DIRS" + echo "has_tf_files=true" >> "$GITHUB_OUTPUT" + fi + + - name: Run Trivy IaC scan + if: steps.find.outputs.has_tf_files == 'true' + id: scan + uses: aquasecurity/trivy-action@v0.36.0 + with: + scan-type: 'config' + scan-ref: '.' + format: 'json' + output: 'trivy-results.json' + exit-code: '1' + severity: 'CRITICAL,HIGH' + skip-dirs: '.terraform' + continue-on-error: true + + - name: Generate SARIF report + if: steps.find.outputs.has_tf_files == 'true' && inputs.upload_sarif + uses: aquasecurity/trivy-action@v0.36.0 + with: + scan-type: 'config' + scan-ref: '.' + format: 'sarif' + output: 'results.sarif' + severity: 'CRITICAL,HIGH' + skip-dirs: '.terraform' + + - name: Upload SARIF to GitHub Security tab + if: steps.find.outputs.has_tf_files == 'true' && inputs.upload_sarif && hashFiles('results.sarif') != '' + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: results.sarif + category: trivy-iac + continue-on-error: true + + - name: Upload scan results artifact + if: steps.find.outputs.has_tf_files == 'true' + uses: actions/upload-artifact@v7 + with: + name: trivy-iac-scan-results + path: trivy-results.json + + - name: Fail if misconfigurations found + if: steps.scan.outcome == 'failure' + run: exit 1 diff --git a/.github/workflows/update-readme-actions.yml b/.github/workflows/update-readme-actions.yml index e4bcc40..8711283 100644 --- a/.github/workflows/update-readme-actions.yml +++ b/.github/workflows/update-readme-actions.yml @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-24.04 permissions: contents: write - models: read # Required for GitHub Models + models: read # actionlint:ignore:permissions env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: diff --git a/docs/superpowers/specs/2026-05-20-trivy-tofu-scan-design.md b/docs/superpowers/specs/2026-05-20-trivy-tofu-scan-design.md new file mode 100644 index 0000000..21b78a8 --- /dev/null +++ b/docs/superpowers/specs/2026-05-20-trivy-tofu-scan-design.md @@ -0,0 +1,91 @@ +# Design: trivy-tofu-scan reusable workflow + +**Date:** 2026-05-20 +**Author:** David Fernandez +**Status:** Approved + +## Summary + +Replace `tfsec.yml` with a new reusable workflow `trivy-tofu-scan.yml` that uses Trivy to scan OpenTofu/Terraform IaC code for misconfigurations. The workflow generates a full report persisted in four ways: SARIF upload to the GitHub Security tab, JSON artifact, Job Summary, and PR comment. + +## Context + +The repo already has `tfsec.yml` for IaC security scanning. Trivy covers the same surface (OpenTofu/Terraform misconfigurations) via `--scanners misconfig` and provides richer output options, active maintenance, and a unified tool already used in `docker-security-scan.yml` and `ecr-security-scan.yml`. + +## Architecture + +A single file `.github/workflows/trivy-tofu-scan.yml` with `on: workflow_call`. Follows the exact pattern of `tfsec.yml`, `docker-security-scan.yml`, and `ecr-security-scan.yml`. + +### Permissions + +```yaml +permissions: + contents: read + security-events: write # SARIF upload to GitHub Security tab + pull-requests: write # PR comment +``` + +### Inputs + +| Input | Type | Default | Description | +|---|---|---|---| +| `upload_sarif` | boolean | `true` | Upload SARIF to GitHub Security tab | +| `post_comment` | boolean | `true` | Post comment on PR if findings found | + +Severity is hardcoded to `CRITICAL,HIGH` — not configurable. + +## Job: `trivy-iac` + +Runner: `ubuntu-24.04`. Env: `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true`. + +### Steps + +1. **Checkout** — `actions/checkout@v6` + +2. **Find .tf files** — `find . -name "*.tf" -not -path "./.terraform/*"`. Sets `has_tf_files` output. Early exits (skip remaining steps) if no `.tf` files found. + +3. **Run Trivy IaC scan** — Installs Trivy CLI (pinned version), runs: + ``` + trivy config . --scanners misconfig --severity CRITICAL,HIGH \ + --format json --output trivy-results.json \ + --exit-code 1 + ``` + Uses `continue-on-error: true` to allow subsequent reporting steps to run. Captures exit code in step output. + +4. **Run Trivy SARIF export** — Re-runs Trivy with `--format sarif --output results.sarif --soft-fail`. Only runs if `has_tf_files == 'true'` and `inputs.upload_sarif`. + +5. **Upload SARIF** — `github/codeql-action/upload-sarif@v4` with `category: trivy-iac`. Runs if `upload_sarif` input is true and `results.sarif` exists. + +6. **Generate Job Summary** — Bash script parses `trivy-results.json` with `jq`, builds a markdown table of findings (ID, severity, resource, message) and writes to `$GITHUB_STEP_SUMMARY`. Always runs if `has_tf_files == 'true'`. + +7. **Upload artifact** — `actions/upload-artifact@v4` uploads `trivy-results.json` as `trivy-iac-scan-results`. Always runs if `has_tf_files == 'true'` (even on clean scans — artifact confirms the scan ran). + +8. **Post PR comment** — `actions/github-script@v9` posts a comment with finding count and link to the run. Runs if `post_comment` is true, `github.event_name == 'pull_request'`, and findings were found. + +9. **Fail if findings** — `run: exit 1` if Trivy step exit code was non-zero. + +## Error handling + +- No `.tf` files: steps 3–9 are skipped via `if: steps.find.outputs.has_tf_files == 'true'`. Workflow exits green. +- SARIF upload failure: `continue-on-error: true` so it doesn't block the fail step. +- Trivy install failure: the job fails immediately (no `continue-on-error`). + +## Migration from tfsec + +Callers replace: +```yaml +uses: nullplatform/actions-nullplatform/.github/workflows/tfsec.yml@main +``` +with: +```yaml +uses: nullplatform/actions-nullplatform/.github/workflows/trivy-tofu-scan.yml@main +``` + +The `minimum_severity` input from `tfsec.yml` has no equivalent — severity is fixed to `CRITICAL,HIGH`. + +## Files + +| Action | File | +|---|---| +| Create | `.github/workflows/trivy-tofu-scan.yml` | +| Delete (or deprecate) | `.github/workflows/tfsec.yml` |