ci: integrate SafeDep PMG on all workflow jobs - #275
Conversation
Route package installs through SafeDep PMG's persistent proxy in server-mode to auto-block malicious packages in CI. Adds a 'Start PMG' step (safedep/pmg@v1) before package setup and an 'Enforce PMG policy' step (pmg proxy stop --fail-on-violation, if: always()) to every job across both workflows: - ci.yml: build, test, publish - ruby.yml: test In ruby.yml the PMG step is placed before ruby/setup-ruby so it intercepts the bundle install run by bundler-cache: true.
| steps: | ||
| - uses: actions/checkout@v3 | ||
| - name: Start PMG | ||
| uses: safedep/pmg@v1 |
There was a problem hiding this comment.
Semgrep identified an issue in your code:
The safedep/pmg@v1 step can silently change when the upstream tag is repointed. A compromised action could run in CI and expose the provided SafeDep API credentials.
More details about this
The Start PMG step runs safedep/pmg@v1, a mutable version tag rather than an immutable commit. The action owner—or an attacker who compromises that repository—can repoint v1 to a malicious commit; the next push or pull request would execute that code on ubuntu-latest before the Ruby tests run. Because the step receives secrets.SAFEDEP_API_KEY and secrets.SAFEDEP_TENANT_ID, the malicious action could read or exfiltrate those credentials and use them to access the SafeDep tenant. It could also alter the workspace or workflow outputs before bundle exec rake and the PMG policy check execute.
A plausible attack sequence:
- An attacker gains control of the
safedep/pmgrepository or its release process and moves thev1tag to a malicious revision. - A developer opens a pull request or pushes to
master, triggering this workflow. - GitHub resolves
uses: safedep/pmg@v1to the attacker-controlled revision and runs its code in theStart PMGstep. - That code accesses the step's
api-keyandtenant-idinputs, which contain${{ secrets.SAFEDEP_API_KEY }}and${{ secrets.SAFEDEP_TENANT_ID }}, then sends them to an attacker-controlled endpoint or uses them against the SafeDep service.
The risk is specific to the mutable v1 reference; the other-party action can change without any change to this workflow.
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
-
Identify the full 40-character commit SHA that corresponds to the trusted
v1release ofsafedep/pmg. -
Replace the mutable tag with that commit SHA and retain the version comment for maintainability:
uses: safedep/pmg@<40-character-commit-sha> # v1 -
Confirm that the pinned commit is the intended
safedep/pmgrelease before allowing the action to accessSAFEDEP_API_KEYandSAFEDEP_TENANT_ID. A commit SHA cannot be silently repointed like a tag or branch.
💬 Ignore this finding
Leave a nosemgrep comment directly above or at the end of line 30 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 | ||
| uses: safedep/pmg@v1 |
There was a problem hiding this comment.
Semgrep identified an issue in your code:
The workflow runs safedep/pmg from the mutable v1 tag, allowing a repointed action to execute in CI. A compromised action could steal PMG credentials and the publish job’s GEM_API_KEY, then publish malicious Ruby gems.
More details about this
uses: safedep/pmg@v1 follows the mutable v1 tag rather than an immutable commit. The owner of safedep/pmg (or an attacker who compromises that repository) can move v1 to a different commit without changing this workflow, causing future runs to execute attacker-controlled code.
A plausible attack is:
- An attacker gains control of the
safedep/pmgrepository and repoints thev1tag to a malicious action. - A subsequent
buildorpublishrun starts theStart PMGstep and executes that malicious action before the Ruby tests or gem publication. - The action reads the job's
SAFEDEP_API_KEY,SAFEDEP_TENANT_ID, and—especially inpublish—GEM_API_KEYfrom the runner environment or process context. - Using
GEM_API_KEY, the attacker can publish unauthorized Ruby gems or tampered versions; they can also alter files or artifacts used by later steps. The same mutablesafedep/pmg@v1reference appears in both jobs, so both execution paths are exposed.
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
-
Identify the trusted 40-character commit SHA for the
safedep/pmgrelease corresponding tov1from the action’s official repository or release metadata. -
Replace both occurrences of
safedep/pmg@v1with the full commit SHA, retaining the version as a comment. For example:uses: safedep/pmg@<40-character-commit-sha> # v1 -
Confirm that the
uses:values in both the build/test job and the publish job use the same immutable SHA and do not use a branch or version tag.
💬 Ignore this finding
Leave a nosemgrep comment directly above or at the end of line 80 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.
| - name: Checkout | ||
| uses: actions/checkout@v2 | ||
| - name: Start PMG | ||
| uses: safedep/pmg@v1 |
There was a problem hiding this comment.
Semgrep identified an issue in your code:
The workflow executes safedep/pmg@v1, a mutable action reference that can silently be repointed to attacker-controlled code. A compromised action could access PMG secrets and tamper with or exfiltrate Ruby build and release artifacts.
More details about this
The Start PMG step runs safedep/pmg@v1, where v1 is a mutable branch or tag rather than an immutable commit. The action owner—or someone who compromises that repository—can move v1 to a malicious commit without changing this workflow, causing future build, test, or publish jobs to execute attacker-controlled code.
A plausible attack is:
- An attacker gains control of the
safedep/pmgrepository and repointsv1to a malicious release. - The next workflow run downloads that release when it reaches
uses: safedep/pmg@v1. - The malicious action executes with the job's permissions and can read
${{ secrets.SAFEDEP_API_KEY }}and${{ secrets.SAFEDEP_TENANT_ID }}supplied to the PMG step. - It can exfiltrate those credentials, inspect the checked-out Ruby source and built gems, or tamper with artifacts before
actions/upload-artifact@v3uploads them. Inpublish, the same action runs in the tag-triggered release path, so a compromisedv1can affect released packages without a workflow-file change.
To resolve this comment:
✨ Commit fix suggestion
| uses: safedep/pmg@v1 | |
| uses: safedep/pmg@<VERIFIED_VALUE_REQUIRED> # v1; replace with the verified 40-character commit SHA for the trusted v1 release |
View step-by-step instructions
- Replace the mutable tag in the PMG step with the full 40-character commit SHA for the intended
safedep/pmgv1release:uses: safedep/pmg@<40-character-commit-sha>. - Confirm that the SHA belongs to the trusted
safedep/pmgrepository and corresponds to thev1release, then optionally retain the version as a comment:uses: safedep/pmg@<40-character-commit-sha> # v1. - Apply the same pin to each
safedep/pmg@v1reference in this workflow. A commit SHA prevents the action owner from silently repointing the tag to different code.
💬 Ignore this finding
Leave a nosemgrep comment directly above or at the end of line 51 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 | ||
| uses: safedep/pmg@v1 |
There was a problem hiding this comment.
Semgrep identified an issue in your code:
The workflow runs safedep/pmg@v1, so the action can change silently when its tag is repointed. A compromised release could execute with SafeDep secrets and exfiltrate credentials or alter the build.
More details about this
The Start PMG step runs safedep/pmg@v1, where v1 is a mutable tag rather than an immutable commit. The action owner—or an attacker who compromises that repository—can repoint v1 to a malicious commit without changing this workflow.
A plausible attack is:
- An attacker gains control of the
safedep/pmgrepository and moves thev1tag to a malicious release. - A push to
masteror a matching version tag starts thisCIworkflow and downloads the newly tagged code forsafedep/pmg. - The malicious action executes during
Start PMGwithserver-mode: trueand access tosecrets.SAFEDEP_API_KEYandsecrets.SAFEDEP_TENANT_ID. - It can send those credentials, repository contents, or build data to an attacker-controlled server before the later
bundle installandgem buildsteps run, potentially allowing unauthorized access to the SafeDep tenant or tampering with the build.
Because the workflow text remains unchanged, ordinary code review may not reveal when the action's behavior changes.
To resolve this comment:
✨ Commit fix suggestion
| uses: safedep/pmg@v1 | |
| uses: safedep/pmg@<VERIFIED_VALUE_REQUIRED> # v1 | |
| # Human intervention required: replace the placeholder with the verified 40-character commit SHA for the trusted v1 release. |
View step-by-step instructions
- Resolve the trusted commit SHA for the
safedep/pmgv1release from the action’s official repository or release metadata. - Replace the mutable tag with the full 40-character commit SHA, keeping the version comment for readability:
uses: safedep/pmg@<40-character-commit-sha> # v1 - Verify that the selected commit belongs to the intended
v1release and is not a branch or abbreviated SHA.
💬 Ignore this finding
Leave a nosemgrep comment directly above or at the end of line 19 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 67bea21 🥳
| 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 0c6e9ea ⭐
| api-key: ${{ secrets.PMG_PUBLIC_REPOS_TOKEN }} | ||
| tenant-id: ${{ secrets.PMG_TENANT_ID }} | ||
|
|
||
| - uses: ruby/setup-ruby@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.
🧼 Fixed in commit 67bea21 🧼
| 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 0c6e9ea 🎈
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: 60 lines added, none removed or modified. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
| 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 0c6e9ea 🧼
| 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 0c6e9ea ⭐
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 [email protected] 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) <[email protected]>
Summary
Integrates SafeDep PMG (Package Manager Guard) into every job across both GitHub Actions workflows. PMG runs as a persistent proxy in CI, intercepts installs from supported package managers via standard proxy env vars, and auto-blocks any package flagged as malicious.
Reference: PMG in GitHub Actions
What changed
Each job received two steps, per the SafeDep docs:
safedep/pmg@v1inserver-mode, placed after checkout and before any package setup.pmg proxy stop --fail-on-violationwithif: always(), which fails the job on a block and flushes final events to the cloud (runs even if an earlier step fails).Coverage
ci.ymlbuildbundle installruby/setup-rubyci.ymltestbundle installruby/setup-rubyci.ymlpublishapt-get/gem pushruby.ymltestbundle install(viabundler-cache: true)ruby/setup-rubyso PMG intercepts the bundler install run inside the actionWhy placement matters (ruby.yml)
ruby.yml's test job usesbundler-cache: true, which runsbundle installinside theruby/setup-rubyaction. The PMG start step is therefore placed before setup-ruby so the proxy env vars are active when that install runs.Secrets
The steps reference
secrets.SAFEDEP_API_KEYandsecrets.SAFEDEP_TENANT_ID. These are optional — PMG blocks malicious packages using SafeDep's free community intelligence with or without credentials. If the secrets are unset, they resolve empty and PMG still enforces; only cloud event sync to Endpoint Hub is skipped. No job breaks either way.Verification
Diff stat
Discussion thread: Slack