Skip to content

chore(gha): harden GitHub Actions workflows#652

Open
oboehmer wants to merge 8 commits intomainfrom
chore/gh-action-hardening
Open

chore(gha): harden GitHub Actions workflows#652
oboehmer wants to merge 8 commits intomainfrom
chore/gh-action-hardening

Conversation

@oboehmer
Copy link
Collaborator

@oboehmer oboehmer commented Mar 16, 2026

Description

Harden GitHub Actions workflows based on security audit findings. This PR implements supply chain attack prevention and least-privilege permissions

It also adds release gating to ensure tests pass before publishing (not related to security findings).

Closes

Related Issue(s)

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactoring / Technical debt (internal improvements with no user-facing changes)
  • Documentation update
  • Chore (build process, CI, tooling, dependencies)
  • Other (please describe):

Test Framework Affected

  • PyATS
  • Robot Framework
  • Both
  • N/A (not test-framework specific)

Network as Code (NaC) Architecture Affected

  • ACI (APIC)
  • NDO (Nexus Dashboard Orchestrator)
  • NDFC / VXLAN-EVPN (Nexus Dashboard Fabric Controller)
  • Catalyst SD-WAN (SDWAN Manager / vManage)
  • Catalyst Center (DNA Center)
  • ISE (Identity Services Engine)
  • FMC (Firepower Management Center)
  • Meraki (Cloud-managed)
  • NX-OS (Nexus Direct-to-Device)
  • IOS-XE (Direct-to-Device)
  • IOS-XR (Direct-to-Device)
  • Hyperfabric
  • All architectures
  • N/A (architecture-agnostic)

Platform Tested

  • macOS (version tested: )
  • Linux (distro/version tested: )

Key Changes

  • SHA pinning: All GitHub Actions pinned to commit SHAs with version comments for Dependabot compatibility
  • Least-privilege permissions: Explicit permissions blocks added to all jobs (contents: read or {})
  • Webex notification: Replaced unmaintained qsnyder/action-wxt with direct curl to Webex API, including input sanitization
  • Release gating: release.yml now calls test.yml as a reusable workflow before building/publishing
  • .gitignore sync: Aligned with release/pyats-integration-v1.1-beta branch

Testing Done

  • Unit tests added/updated
  • Integration tests performed
  • Manual testing performed:
    • PyATS tests executed successfully
    • Robot Framework tests executed successfully
    • D2D/SSH tests executed successfully (if applicable)
    • HTML reports generated correctly
  • All existing tests pass (pytest / pre-commit run -a)

Test Commands Used

pushed a tag along with a failing test (380019a). build step was skipped

image

Checklist

  • Code follows project style guidelines (pre-commit run -a passes)
  • Self-review of code completed
  • Code is commented where necessary (especially complex logic)
  • Documentation updated (if applicable)
  • No new warnings introduced
  • Changes work on both macOS and Linux
  • CHANGELOG.md updated (if applicable)

Screenshots (if applicable)

Additional Notes

Security improvements:

  • SHA pins prevent supply chain attacks where a malicious actor compromises an actions tag
  • Version comments (# v6) enable Dependabot to still track and propose updates
  • permissions: {} on notification job grants zero permissions (most restrictive)
  • Lint job retains contents: write for legitimate dependabot lock file updates

Webex curl migration:

  • qsnyder/action-wxt was unmaintained (last commit 2022, no releases)
  • Direct curl gives full control and eliminates third-party dependency
  • Input sanitization via jq -Rs prevents JSON injection from commit messages

- Pin all GitHub Actions to commit SHAs to prevent supply chain attacks
- Add version comments (e.g., "# v6") for Dependabot compatibility
- Add explicit permissions blocks to all jobs (least-privilege)
- Replace unmaintained qsnyder/action-wxt with direct curl to Webex API
- Add input sanitization for notification message content
- Add workflow_call trigger to test.yml for reusability
- Add test job in release.yml that calls test.yml before build
- Skip notifications when invoked via workflow_call
Syncs .gitignore with release/pyats-integration-v1.1-beta branch
@oboehmer oboehmer requested a review from danischm March 16, 2026 14:42
@oboehmer oboehmer marked this pull request as draft March 16, 2026 16:03
This reverts commit 9057edf.
@oboehmer oboehmer marked this pull request as ready for review March 16, 2026 16:10
@oboehmer oboehmer self-assigned this Mar 16, 2026
@aitestino
Copy link
Collaborator

Hey @oboehmer, thank you for the PR — I had an agent do a quick review and it flagged the following before merge.

Things that need adjustment:

  1. Shell injection in the sanitize step — The ${{ github.event.head_commit.message }} and ${{ github.event.pull_request.title }} expressions get expanded server-side before the shell script is assembled, so a crafted commit message can break out of the single quotes and execute arbitrary commands. The fix is to pass them through an env: block instead:

    - name: Sanitize notification variables
      env:
        RAW_COMMIT_MSG: ${{ github.event.head_commit.message }}
        RAW_PR_TITLE: ${{ github.event.pull_request.title }}
      run: |
        COMMIT_MSG=$(echo "$RAW_COMMIT_MSG" | head -c 500)
        PR_TITLE=$(echo "$RAW_PR_TITLE" | head -c 200)
        echo "SAFE_COMMIT_MSG=${COMMIT_MSG}" >> "$GITHUB_ENV"
        echo "SAFE_PR_TITLE=${PR_TITLE}" >> "$GITHUB_ENV"

    GitHub documents this pattern here: https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections

  2. JSON double-encoding in the notification payloadjq -Rs . outputs a JSON string literal including surrounding double quotes. When that value gets placed inside the curl --data JSON, you end up with double-quoted strings that produce invalid JSON. Switching to jq -n --arg to build the entire payload handles all escaping correctly:

    PAYLOAD=$(jq -n \
      --arg msg "$SAFE_COMMIT_MSG" \
      --arg title "$SAFE_PR_TITLE" \
      --arg url "$WORKFLOW_URL" \
      '{text: "Build failed for \($title)\nCommit: \($msg)\nDetails: \($url)"}')
  3. Missing --fail-with-body on curl — Without --fail or --fail-with-body, curl returns exit code 0 on HTTP 4xx/5xx (it only fails on network errors). So if the Webex API rejects the payload, the || fallback never fires and the step shows green. Adding --fail-with-body makes the || warning pattern actually work as intended. One-word fix.

  4. Magic numbers — The 500, 200, and 30 values in the sanitize/timeout steps would benefit from brief inline comments explaining why those limits were chosen (API payload limits? readability? timeout budget?).

What do you think?

P.S. — This comment was drafted using voice-to-text via Claude Code. If the tone comes across as overly direct or terse, please know that's just how it tends to phrase things. No offense or criticism is intended — this is purely an objective technical review of the PR. Thanks for understanding! 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants