Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/artifactory-oidc/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: "Artifactory OIDC Auth"
description: "Exchange GitHub OIDC token for Artifactory access and configure pip indexes"

inputs:
artifactory-url:
description: "Base Artifactory platform URL. Falls back to ARTIFACTORY_URL env var if not provided."
required: false
default: ""
oidc-provider-name:
description: "OIDC provider name configured in Artifactory"
required: false
default: "github-actions"
pypi-repo:
description: "Artifactory virtual PyPI repository name"
required: false
default: "virtual-pypi-thirdparty"

runs:
using: "composite"
steps:
- name: Exchange GitHub OIDC token for Artifactory token
shell: bash
env:
INPUT_ARTIFACTORY_URL: ${{ inputs.artifactory-url }}
OIDC_PROVIDER_NAME: ${{ inputs.oidc-provider-name }}
PYPI_REPO: ${{ inputs.pypi-repo }}
run: |
set -euo pipefail
ARTIFACTORY_URL="${INPUT_ARTIFACTORY_URL:-${ARTIFACTORY_URL:-}}"
if [ -z "${ARTIFACTORY_URL}" ]; then
echo "::error::ARTIFACTORY_URL is not set (pass as input or set as env var)"; exit 1
fi

OIDC_JWT=$(curl -sS \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${ARTIFACTORY_URL}" \
-H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" | jq -r '.value')

if [ -z "$OIDC_JWT" ] || [ "$OIDC_JWT" = "null" ]; then
echo "::error::Failed to obtain GitHub OIDC token"; exit 1
fi

decode_seg() { local s="${1}"; local m=$(( ${#s} % 4 )); [ $m -ne 0 ] && s="${s}$(printf '=%.0s' $(seq 1 $((4-m))))"; echo "$s" | tr '_-' '/+' | base64 -d 2>/dev/null; }
PAYLOAD=$(decode_seg "$(echo "$OIDC_JWT" | cut -d. -f2)")
echo "OIDC token claims:"
echo " sub = $(echo "$PAYLOAD" | jq -r '.sub')"
echo " aud = $(echo "$PAYLOAD" | jq -r '.aud')"
echo " iss = $(echo "$PAYLOAD" | jq -r '.iss')"

RESP=$(curl -sS "${ARTIFACTORY_URL}/access/api/v1/oidc/token" \
-H 'Content-Type: application/json' \
-d "{\"grant_type\":\"urn:ietf:params:oauth:grant-type:token-exchange\",
\"subject_token_type\":\"urn:ietf:params:oauth:token-type:id_token\",
\"subject_token\":\"${OIDC_JWT}\",
\"provider_name\":\"${OIDC_PROVIDER_NAME}\"}")

ART_TOKEN=$(echo "$RESP" | jq -r '.access_token // empty')

if [ -z "$ART_TOKEN" ]; then
echo "::error::OIDC token exchange failed. Raw response (token field redacted):"
echo "$RESP" | jq 'if .access_token then .access_token="<redacted>" else . end' 2>/dev/null || echo "$RESP"
exit 1
fi
echo "::add-mask::$ART_TOKEN"

HOST=$(echo "${ARTIFACTORY_URL}" | sed -E 's#^https?://##')
INDEX_URL="https://:${ART_TOKEN}@${HOST}/artifactory/api/pypi/${PYPI_REPO}/simple/"

echo "::add-mask::${INDEX_URL}"
echo "PIP_INDEX_URL=${INDEX_URL}" >> "$GITHUB_ENV"
echo "PIP_TRUSTED_HOST=${HOST}" >> "$GITHUB_ENV"
84 changes: 84 additions & 0 deletions .github/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Test and Deploy to PyPI

on:
release:
types: [published]
workflow_dispatch:

env:
ARTIFACTORY_URL: ${{ vars.ARTIFACTORY_URL }}

jobs:
test:
name: Test - Python ${{ matrix.python-version }}
runs-on: ${{ github.repository_owner == 'twilio-internal' && 'ubuntu-x64' || 'ubuntu-latest' }}
timeout-minutes: 20
permissions:
contents: read
id-token: write
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0

- name: Authenticate with Artifactory
uses: ./.github/actions/artifactory-oidc

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
pip install virtualenv --upgrade
make install test-install

- name: Run tests
run: make test-with-coverage

deploy:
name: Publish to PyPI
needs: [test]
runs-on: ${{ github.repository_owner == 'twilio-internal' && 'ubuntu-x64' || 'ubuntu-latest' }}
environment: pypi
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4

- name: Authenticate with Artifactory
uses: ./.github/actions/artifactory-oidc

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Validate tag matches package version
run: |
TAG="${GITHUB_REF#refs/tags/}"
# Strip leading 'v' if present
VERSION="${TAG#v}"
PKG_VERSION=$(python setup.py --version)
if [ "$VERSION" != "$PKG_VERSION" ]; then
echo "::error::Tag $TAG does not match setup.py version $PKG_VERSION"
exit 1
fi

- name: Build package
run: |
pip install build
python -m build

- name: Verify package
run: |
pip install twine
twine check dist/*

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1
90 changes: 90 additions & 0 deletions .github/workflows/test-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: Test Release (Dry Run)

on:
workflow_dispatch:
inputs:
python-version:
description: "Python version to test with (or 'all' for full matrix)"
required: false
default: "3.12"

env:
ARTIFACTORY_URL: ${{ vars.ARTIFACTORY_URL }}

jobs:
test:
name: Test - Python ${{ matrix.python-version }}
runs-on: ${{ github.repository_owner == 'twilio-internal' && 'ubuntu-x64' || 'ubuntu-latest' }}
timeout-minutes: 20
permissions:
contents: read
id-token: write
strategy:
fail-fast: false
matrix:
python-version: ${{ github.event.inputs.python-version == 'all' && fromJson('["3.8","3.9","3.10","3.11","3.12","3.13"]') || fromJson(format('["{0}"]', github.event.inputs.python-version)) }}
steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semgrep identified an issue in your code:

Mutable version tag on actions/checkout@v4 could be silently updated to malicious code by an attacker who gains control of the repository, compromising your workflow execution.

More details about this

The step uses actions/checkout@v4, which pins to a mutable version tag instead of a specific commit SHA. An attacker who gains control of the actions/checkout repository could update the v4 tag to point to malicious code. When your workflow runs, GitHub would automatically fetch and execute this compromised version without any notification.

For example, if an attacker pushed malicious code to actions/checkout and moved the v4 tag to that commit, the next time this workflow runs it would:

  1. Execute the attacker's code in the uses: actions/checkout@v4 step
  2. Have access to your repository's code via the fetch-depth: 0 checkout
  3. Potentially exfiltrate secrets, modify your source code, or inject backdoors

This is a supply-chain attack vector that was exploited in real incidents like the trivy-action and kics-github-action compromises.

To resolve this comment:

✨ Commit fix suggestion

Suggested change
- uses: actions/checkout@v4
name: Test Release (Dry Run)
on:
workflow_dispatch:
inputs:
python-version:
description: "Python version to test with (or 'all' for full matrix)"
required: false
default: "3.12"
env:
ARTIFACTORY_URL: ${{ vars.ARTIFACTORY_URL }}
jobs:
test:
name: Test - Python ${{ matrix.python-version }}
runs-on: ${{ github.repository_owner == 'twilio-internal' && 'ubuntu-x64' || 'ubuntu-latest' }}
timeout-minutes: 20
permissions:
contents: read
id-token: write
strategy:
fail-fast: false
matrix:
python-version: ${{ github.event.inputs.python-version == 'all' && fromJson('["3.8","3.9","3.10","3.11","3.12","3.13"]') || fromJson(format('["{0}"]', github.event.inputs.python-version)) }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
- name: Authenticate with Artifactory
uses: ./.github/actions/artifactory-oidc
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install virtualenv --upgrade
make install test-install
- name: Run tests
run: make test-with-coverage
deploy-dry-run:
name: Deploy (Dry Run - No Publish)
needs: [test]
runs-on: ${{ github.repository_owner == 'twilio-internal' && 'ubuntu-x64' || 'ubuntu-latest' }}
environment: pypi
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
View step-by-step instructions
  1. Replace the mutable GitHub Action tag with a full 40-character commit SHA in the uses value.
    Change actions/checkout@v4 to a pinned commit such as actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.

  2. Apply the same change to the other actions/checkout@v4 step in this workflow so both checkout steps use a commit SHA instead of a version tag.

  3. Keep the version comment after the SHA, for example # v4, so the pinned action version is still easy to identify. Pinning to a commit SHA prevents the action owner from silently changing what code runs under the same tag.

  4. Leave the local action reference uses: ./.github/actions/artifactory-oidc unchanged, because local actions are not pinned through remote Git references.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by github-actions-mutable-action-tag.

Need help with this issue? Consult our appsec team or ask in #help-appsec on Slack.

You can view more details about this finding in the Semgrep AppSec Platform.

with:
fetch-depth: 0

- name: Authenticate with Artifactory
uses: ./.github/actions/artifactory-oidc

- uses: actions/setup-python@v5

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semgrep identified an issue in your code:

GitHub Actions step uses mutable version tag v5 instead of a pinned commit SHA, allowing the action owner to silently inject malicious code on the next workflow run.

More details about this

The step uses: actions/setup-python@v5 references the setup-python action using a mutable version tag (v5), which can be silently repointed by the action's maintainer without your knowledge.

Attack scenario:

  1. An attacker compromises the GitHub account of the setup-python action maintainer or the repository itself
  2. They force-push a malicious commit and retag v5 to point to this compromised version
  3. The next time your workflow runs, GitHub checks out the updated v5 tag—now pointing to the attacker's code
  4. The malicious setup-python action executes with your job's credentials and environment variables (including ARTIFACTORY_URL and id-token)
  5. The attacker steals credentials, injects malware into your Python environment, or exfiltrates your code

This mirrors real attacks like the 2023 trivy-action compromise, where a retagged version caused widespread supply-chain damage.

Pin this to a specific commit SHA (e.g., actions/setup-python@8ade135a41bc03ea155e62e844d188df1ea18608) so only that exact code executes.

To resolve this comment:

✨ Commit fix suggestion

Suggested change
- uses: actions/setup-python@v5
- uses: actions/setup-python@42375524ee3024772b628e6502261f4095c0c007 # v5
with:
python-version: ${{ matrix.python-version }}
- uses: actions/setup-python@42375524ee3024772b628e6502261f4095c0c007 # v5
with:
python-version: "3.12"
View step-by-step instructions
  1. Replace the mutable GitHub Action tag with a full 40-character commit SHA in the uses line for actions/setup-python.
    Change actions/setup-python@v5 to actions/setup-python@<full-40-character-commit-sha>, for example actions/setup-python@<sha> # v5.

  2. Keep the version as a comment after the SHA so the workflow stays readable and future updates are easier.
    Use the format uses: actions/setup-python@<sha> # v5.

  3. Apply the same change to each occurrence of this action in the workflow, since both jobs currently use actions/setup-python@v5.

  4. Get the correct commit SHA from the official actions/setup-python release for v5, and pin to that exact commit instead of the tag name. Pinning to a commit SHA prevents the action reference from being silently changed later.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by github-actions-mutable-action-tag.

Need help with this issue? Consult our appsec team or ask in #help-appsec on Slack.

You can view more details about this finding in the Semgrep AppSec Platform.

with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
pip install virtualenv --upgrade
make install test-install

- name: Run tests
run: make test-with-coverage

deploy-dry-run:
name: Deploy (Dry Run - No Publish)
needs: [test]
runs-on: ${{ github.repository_owner == 'twilio-internal' && 'ubuntu-x64' || 'ubuntu-latest' }}
environment: pypi
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semgrep identified an issue in your code:

GitHub Actions step uses mutable version tag v4 instead of immutable commit SHA, allowing maintainer updates to silently change executed code and enabling supply-chain attacks.

More details about this

The actions/checkout@v4 step uses a mutable version tag instead of pinning to a specific commit. Version tags like v4 can be silently repointed by the action's maintainer without your knowledge. An attacker who compromises the actions/checkout repository could update the v4 tag to point to malicious code, which would then execute in your workflow.

Here's a concrete attack scenario:

  1. An attacker compromises GitHub's actions/checkout repository
  2. They update the v4 tag to point to a new commit containing malicious code (e.g., stealing secrets from $GITHUB_ENV or exfiltrating repository contents)
  3. The next time your workflow runs with uses: actions/checkout@v4, the malicious version executes automatically
  4. The attacker now has access to your repository's environment variables, source code, and credentials used during the workflow
  5. This matches real-world supply chain attacks like the trivy-action and kics-github-action compromises mentioned in security advisories

The same risk applies to actions/setup-python@v5 and any other action using mutable references elsewhere in your workflow.

To resolve this comment:

✨ Commit fix suggestion

Suggested change
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
View step-by-step instructions
  1. Replace the mutable GitHub Action reference uses: actions/checkout@v4 with a full 40-character commit SHA for the same action version, for example uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.
  2. Keep the existing with: settings such as fetch-depth: 0 exactly as they are, and only change the value after uses:.
  3. Apply the same pinning format to the other flagged actions/checkout step in this workflow if it also uses @v4 instead of a commit SHA. Pinning to a commit SHA prevents the action owner from silently changing what code runs for that version tag.
💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by github-actions-mutable-action-tag.

Need help with this issue? Consult our appsec team or ask in #help-appsec on Slack.

You can view more details about this finding in the Semgrep AppSec Platform.


- name: Authenticate with Artifactory
uses: ./.github/actions/artifactory-oidc

- uses: actions/setup-python@v5
Comment thread
shrutiburman marked this conversation as resolved.
with:
python-version: "3.12"

- name: Validate version in setup.py
run: |
PKG_VERSION=$(python setup.py --version)
echo "Package version: $PKG_VERSION"
echo "PKG_VERSION=$PKG_VERSION" >> "$GITHUB_ENV"

- name: Build package
run: |
pip install build
python -m build

- name: Verify package
run: |
pip install twine
twine check dist/*

- name: List built artifacts
run: |
echo "Built artifacts:"
ls -lh dist/
echo ""
echo "Package version: $PKG_VERSION"
echo ""
echo "--- DRY RUN COMPLETE ---"
echo "To publish for real, create a GitHub Release with a matching tag"
echo "and use the deploy.yml workflow."
8 changes: 8 additions & 0 deletions requirements.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pygments>=2.7.4 # not directly required, pinned by Snyk to avoid a vulnerability
requests>=2.32.2
PyJWT>=2.0.0, <3.0.0
aiohttp>=3.10.2
aiohttp-retry==2.8.3
certifi>=2023.7.22 # not directly required, pinned by Snyk to avoid a vulnerability
urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability
zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability
60 changes: 53 additions & 7 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,54 @@
pygments>=2.7.4 # not directly required, pinned by Snyk to avoid a vulnerability
requests>=2.32.2
PyJWT>=2.0.0, <3.0.0
aiohttp>=3.10.2
#
# This file is autogenerated by pip-compile with Python 3.14
# by the following command:
#
# pip-compile --output-file=requirements.txt requirements.in
#
aiohappyeyeballs==2.6.2
# via aiohttp
aiohttp==3.14.1
# via
# -r requirements.in
# aiohttp-retry
aiohttp-retry==2.8.3
certifi>=2023.7.22 # not directly required, pinned by Snyk to avoid a vulnerability
urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability
zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability
# via -r requirements.in
aiosignal==1.4.0
# via aiohttp
attrs==26.1.0
# via aiohttp
certifi==2026.5.20
# via
# -r requirements.in
# requests
charset-normalizer==3.4.7
# via requests
frozenlist==1.8.0
# via
# aiohttp
# aiosignal
idna==3.18
# via
# requests
# yarl
multidict==6.7.1
# via
# aiohttp
# yarl
propcache==0.5.2
# via
# aiohttp
# yarl
pygments==2.20.0
# via -r requirements.in
pyjwt==2.13.0
# via -r requirements.in
requests==2.34.2
# via -r requirements.in
urllib3==2.7.0
# via
# -r requirements.in
# requests
yarl==1.24.2
# via aiohttp
zipp==4.1.0
# via -r requirements.in