Skip to content
Merged
4 changes: 2 additions & 2 deletions .github/workflows/changelog-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/docker-build-push-ecr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
11 changes: 6 additions & 5 deletions .github/workflows/ecr-security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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=""
Expand All @@ -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 \
Expand Down Expand Up @@ -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'
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/pr-checks-actions.yml
Original file line number Diff line number Diff line change
@@ -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'
3 changes: 2 additions & 1 deletion .github/workflows/pre-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

8 changes: 5 additions & 3 deletions .github/workflows/readme-ai-v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 != '' }}
Expand All @@ -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 \
Expand All @@ -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 }}" \
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/shellcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 4 additions & 4 deletions .github/workflows/tfsec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
davidf-null marked this conversation as resolved.
if: steps.modules.outputs.has_tf_files == 'true'
Expand All @@ -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
Expand Down
80 changes: 80 additions & 0 deletions .github/workflows/trivy-tofu-scan.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion .github/workflows/update-readme-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
91 changes: 91 additions & 0 deletions docs/superpowers/specs/2026-05-20-trivy-tofu-scan-design.md
Original file line number Diff line number Diff line change
@@ -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` |