Skip to content

Commit 744286c

Browse files
authored
Merge pull request #30 from packagist/zizmor-fixes
Configure zizmor to check the action.yml/workflow files and address any issues
2 parents 30351ad + 4406dc1 commit 744286c

6 files changed

Lines changed: 231 additions & 48 deletions

File tree

.github/workflows/zizmor.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: GitHub Actions Security Analysis with zizmor 🌈
2+
3+
on: # zizmor: ignore[concurrency-limits]
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- '.github/workflows/*.yml'
9+
- 'action.yml'
10+
pull_request:
11+
paths:
12+
- '.github/workflows/*.yml'
13+
- 'action.yml'
14+
15+
permissions: {}
16+
17+
jobs:
18+
zizmor:
19+
name: Run zizmor 🌈
20+
runs-on: ubuntu-latest
21+
permissions:
22+
contents: read
23+
steps:
24+
- name: Checkout repository
25+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
26+
with:
27+
persist-credentials: false
28+
29+
- name: Run zizmor 🌈
30+
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
31+
with:
32+
advanced-security: false
33+
annotations: true
34+
online-audits: false
35+
persona: 'pedantic'

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,21 @@ For example:
8181
skip_git_hooks: "true"
8282
```
8383

84+
#### packagist_url
85+
86+
The `packagist_url` input parameter sets the base URL of the Private Packagist instance that dispatches this action.
87+
Webhook callbacks in the dispatched payload must point at a URL under this prefix; any other host is refused before
88+
the action makes the HTTP request, preventing a hostile payload from redirecting the webhook (and its credentials)
89+
to an attacker-controlled server.
90+
91+
The default is `https://packagist.com`. Override it only when running a Private Packagist Self-Hosted installation:
92+
93+
```yaml
94+
- uses: packagist/conductor-github-action
95+
with:
96+
packagist_url: "https://packagist.example.com"
97+
```
98+
8499
## Copyright and License
85100

86101
The GitHub Action is licensed under the MIT License.

action.yml

Lines changed: 95 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,84 +12,121 @@ inputs:
1212
description: Skip any git hooks that get installed as part of the GitHub Action e.g. during composer install or update.
1313
default: 'false'
1414
required: false
15+
packagist_url:
16+
description: Base URL of the Private Packagist instance that dispatches this action. Webhook URLs in the payload must be under this prefix. Requests to any other host are refused. Override this for Self-Hosted installations.
17+
default: 'https://packagist.com'
18+
required: false
1519

1620
runs:
1721
using: "composite"
1822
steps:
19-
# Temporary workaround to make sure you can set up Conductor for
20-
# the first time. The CI verification job runs "composer update nothing"
21-
# which fails if your composer.lock contains any versions with
22-
# known security issues in Composer >=2.9.0
23-
- name: Set security blocking environment variable
24-
shell: "bash"
25-
run: echo "COMPOSER_NO_SECURITY_BLOCKING=${{ github.event.client_payload.branch == 'conductor-nothing' && 1 || 0 }}" >> $GITHUB_ENV
26-
- name: Set Conductor version
27-
shell: "bash"
28-
run: echo "CONDUCTOR_ACTION_VERSION=1.5.3" >> $GITHUB_ENV
29-
- run: |
23+
# Set local environment variables using jq instead of passing the values via env: to not leak secrets before masking them
24+
- name: Mask Composer authentication token
25+
shell: bash
26+
run: |
3027
CONDUCTOR_TOKEN=$(jq -r '.client_payload.composerAuthentication.token' $GITHUB_EVENT_PATH)
3128
echo "::add-mask::$CONDUCTOR_TOKEN"
3229
if: ${{ github.event.client_payload.composerAuthentication.type != 'none' }}
33-
shell: "bash"
30+
31+
- name: Mask webhook authentication token
32+
shell: bash
33+
run: |
34+
WEBHOOK_AUTHENTICATION_PASSWORD=$(jq -r '.client_payload.webhook.authentication.password' $GITHUB_EVENT_PATH)
35+
echo "::add-mask::$WEBHOOK_AUTHENTICATION_PASSWORD"
36+
37+
# This is the version that needs to be increased for each release of the GitHub Action
38+
- name: Set Conductor version
39+
shell: bash
40+
run: echo "CONDUCTOR_ACTION_VERSION=1.5.3" >> $GITHUB_ENV
41+
42+
- name: Validate Conductor branch name
43+
shell: bash
44+
run: '"${GITHUB_ACTION_PATH}/bin/branch_name_check.sh" "${BRANCH}"'
45+
env:
46+
BRANCH: ${{ github.event.client_payload.branch }}
3447

3548
- name: "Validate GitHub action version"
36-
shell: "bash"
37-
run: "${GITHUB_ACTION_PATH}/bin/ci_version_check.sh ${{ github.event.client_payload.requirements.minimumCiActionVersion }} $CONDUCTOR_ACTION_VERSION"
49+
shell: bash
50+
run: "${GITHUB_ACTION_PATH}/bin/ci_version_check.sh ${MINIMUM_CI_ACTION_VERSION} $CONDUCTOR_ACTION_VERSION"
51+
env:
52+
MINIMUM_CI_ACTION_VERSION: ${{ github.event.client_payload.requirements.minimumCiActionVersion }}
3853

3954
- name: "Validate PHP version"
40-
shell: "bash"
41-
run: "${GITHUB_ACTION_PATH}/bin/php_version_check.sh ${{ github.event.client_payload.requirements.minimumPhpVersion }}"
55+
shell: bash
56+
run: "${GITHUB_ACTION_PATH}/bin/php_version_check.sh ${MINIMUM_PHP_VERSION}"
57+
env:
58+
MINIMUM_PHP_VERSION: ${{ github.event.client_payload.requirements.minimumPhpVersion }}
4259

4360
- name: "Validate Composer version"
44-
shell: "bash"
45-
run: "${GITHUB_ACTION_PATH}/bin/composer_version_check.sh ${{ github.event.client_payload.requirements.minimumComposerVersion }}"
61+
shell: bash
62+
run: "${GITHUB_ACTION_PATH}/bin/composer_version_check.sh ${MINIMUM_COMPOSER_VERSION}"
63+
env:
64+
MINIMUM_COMPOSER_VERSION: ${{ github.event.client_payload.requirements.minimumComposerVersion }}
65+
66+
# Temporary workaround to make sure you can set up Conductor for
67+
# the first time. The CI verification job runs "composer update nothing"
68+
# which fails if your composer.lock contains any versions with
69+
# known security issues in Composer >=2.9.0
70+
- name: Set security blocking environment variable
71+
shell: bash
72+
run: |
73+
if [[ "${BRANCH}" == "conductor-nothing" ]]; then
74+
echo "COMPOSER_NO_SECURITY_BLOCKING=1" >> "$GITHUB_ENV"
75+
else
76+
echo "COMPOSER_NO_SECURITY_BLOCKING=0" >> "$GITHUB_ENV"
77+
fi
78+
env:
79+
BRANCH: ${{ github.event.client_payload.branch }}
4680

4781
- name: Store base commit info
82+
shell: bash
4883
id: base_commit_info
4984
run: |
5085
git log -1 --format="HASH=%H" >> $GITHUB_OUTPUT
5186
git log -1 --format="AUTHOR=%an" >> $GITHUB_OUTPUT
5287
git log -1 --format="MESSAGE=%s" >> $GITHUB_OUTPUT
53-
shell: bash
54-
55-
- name: Configure Composer authentication
56-
shell: "bash"
57-
if: ${{ github.event.client_payload.composerAuthentication.type == 'environment' }}
58-
run: echo 'COMPOSER_AUTH=${{ github.event.client_payload.composerAuthentication.environment }}' >> "$GITHUB_ENV"
5988
6089
- name: Install dependencies
6190
uses: ramsey/composer-install@a35c6ebd3d08125aaf8852dff361e686a1a67947 # 3.2.0
91+
env:
92+
COMPOSER_AUTH: ${{ github.event.client_payload.composerAuthentication.type == 'environment' && github.event.client_payload.composerAuthentication.environment || env.COMPOSER_AUTH }}
6293
with:
6394
working-directory: "${{ github.event.client_payload.workingDirectory }}"
6495
composer-options: "${{ github.event.client_payload.settings.debug == true && '-vvv' || '' }}"
6596

6697
- name: Modify requirements in the composer.json
67-
run: "${{ github.event.client_payload.settings.debug == true && github.event.client_payload.requireCommand.debug || github.event.client_payload.requireCommand.plain }}"
6898
if: ${{ github.event.client_payload.requireCommand }}
6999
shell: bash
100+
run: '"${GITHUB_ACTION_PATH}/bin/run_composer_command.sh" require'
70101
working-directory: "${{ github.event.client_payload.workingDirectory }}"
102+
env:
103+
COMPOSER_COMMAND_STRING: ${{ github.event.client_payload.settings.debug == true && github.event.client_payload.requireCommand.debug || github.event.client_payload.requireCommand.plain }}
104+
COMPOSER_AUTH: ${{ github.event.client_payload.composerAuthentication.type == 'environment' && github.event.client_payload.composerAuthentication.environment || env.COMPOSER_AUTH }}
71105

72106
- name: Composer update
73-
run: "${{ github.event.client_payload.settings.debug == true && github.event.client_payload.updateCommand.debug || github.event.client_payload.updateCommand.plain }}"
74107
shell: bash
108+
run: '"${GITHUB_ACTION_PATH}/bin/run_composer_command.sh" update'
75109
working-directory: "${{ github.event.client_payload.workingDirectory }}"
110+
env:
111+
COMPOSER_COMMAND_STRING: ${{ github.event.client_payload.settings.debug == true && github.event.client_payload.updateCommand.debug || github.event.client_payload.updateCommand.plain }}
112+
COMPOSER_AUTH: ${{ github.event.client_payload.composerAuthentication.type == 'environment' && github.event.client_payload.composerAuthentication.environment || env.COMPOSER_AUTH }}
76113

77114
- name: Uninstall git hooks
115+
shell: bash
78116
if: ${{ inputs.skip_git_hooks != 'false' }}
79117
run: "rm -rf .git/hooks"
80-
shell: "bash"
81118

82119
- name: Create branch
83-
run: git checkout -b $BRANCH
84120
shell: bash
121+
run: git checkout -b $BRANCH
85122
env:
86123
BRANCH: ${{ github.event.client_payload.branch }}
87124

88125
- name: Add files
126+
shell: bash
89127
run: |
90128
read -r -a PATTERN_EXPANDED <<< "$FILE_PATTERN";
91129
git add ${FILE_PATTERN:+"${PATTERN_EXPANDED[@]}"};
92-
shell: bash
93130
env:
94131
FILE_PATTERN: ${{ inputs.file_pattern }}
95132

@@ -100,37 +137,29 @@ runs:
100137
skip-empty: true
101138

102139
- name: Store number of changed files
140+
shell: bash
103141
id: number_of_changed_files
104142
run: echo "COUNT=$(git --no-pager diff --name-only $GITHUB_SHA | wc -l | tr -d ' ')" >> $GITHUB_OUTPUT
105-
shell: bash
106143

107144
- name: Store Conductor commit info
145+
shell: bash
108146
id: conductor_commit_info
109147
run: |
110148
git log -1 --format="HASH=%H" >> $GITHUB_OUTPUT
111149
git log -1 --format="AUTHOR=%an" >> $GITHUB_OUTPUT
112150
git log -1 --format="MESSAGE=%s" >> $GITHUB_OUTPUT
113-
shell: bash
114151
115152
- name: Push branch
116-
run: git push origin $BRANCH --force
117153
shell: bash
154+
run: git push origin $BRANCH --force
118155
if: ${{ steps.number_of_changed_files.outputs.COUNT != 0 }}
119156
env:
120157
BRANCH: ${{ github.event.client_payload.branch }}
121158

122159
- name: Call webhook from Private Packagist to create the pull request
123160
shell: bash
124-
env:
125-
RUN_ID: ${{ github.run_id }};
126-
CHANGED_FILES: ${{ steps.number_of_changed_files.outputs.COUNT }}
127-
BASE_COMMIT_HASH: ${{ steps.base_commit_info.outputs.HASH }}
128-
BASE_COMMIT_AUTHOR: ${{ steps.base_commit_info.outputs.AUTHOR }}
129-
BASE_COMMIT_MESSAGE: ${{ steps.base_commit_info.outputs.MESSAGE }}
130-
CONDUCTOR_COMMIT_HASH: ${{ steps.conductor_commit_info.outputs.HASH }}
131-
CONDUCTOR_COMMIT_AUTHOR: ${{ steps.conductor_commit_info.outputs.AUTHOR }}
132-
CONDUCTOR_COMMIT_MESSAGE: ${{ steps.conductor_commit_info.outputs.MESSAGE }}
133161
run: |
162+
"${GITHUB_ACTION_PATH}/bin/webhook_url_check.sh" "${PACKAGIST_URL}" "${WEBHOOK_EXECUTEDURL}"
134163
jq -n '{
135164
"runId": env.RUN_ID,
136165
"numberOfChangedFiles": env.CHANGED_FILES,
@@ -150,20 +179,29 @@ runs:
150179
"ciScriptVersion": env.CONDUCTOR_ACTION_VERSION
151180
}
152181
}' | curl -fsSL -X POST \
153-
-u "${{ github.event.client_payload.webhook.authentication.username }}:${{ github.event.client_payload.webhook.authentication.password }}" \
182+
-u "${WEBHOOK_AUTHENTICATION_USERNAME}:${WEBHOOK_AUTHENTICATION_PASSWORD}" \
154183
--header "Content-Type: application/json" \
155184
--data @- \
156-
"${{ github.event.client_payload.webhook.executedUrl }}"
157-
158-
- name: Call webhook from Private Packagist to notify about build failure
159-
shell: bash
185+
"${WEBHOOK_EXECUTEDURL}"
160186
env:
161187
RUN_ID: ${{ github.run_id }}
188+
CHANGED_FILES: ${{ steps.number_of_changed_files.outputs.COUNT }}
162189
BASE_COMMIT_HASH: ${{ steps.base_commit_info.outputs.HASH }}
163190
BASE_COMMIT_AUTHOR: ${{ steps.base_commit_info.outputs.AUTHOR }}
164191
BASE_COMMIT_MESSAGE: ${{ steps.base_commit_info.outputs.MESSAGE }}
192+
CONDUCTOR_COMMIT_HASH: ${{ steps.conductor_commit_info.outputs.HASH }}
193+
CONDUCTOR_COMMIT_AUTHOR: ${{ steps.conductor_commit_info.outputs.AUTHOR }}
194+
CONDUCTOR_COMMIT_MESSAGE: ${{ steps.conductor_commit_info.outputs.MESSAGE }}
195+
WEBHOOK_AUTHENTICATION_USERNAME: ${{ github.event.client_payload.webhook.authentication.username }}
196+
WEBHOOK_AUTHENTICATION_PASSWORD: ${{ github.event.client_payload.webhook.authentication.password }}
197+
WEBHOOK_EXECUTEDURL: ${{ github.event.client_payload.webhook.executedUrl }}
198+
PACKAGIST_URL: ${{ inputs.packagist_url }}
199+
200+
- name: Call webhook from Private Packagist to notify about build failure
201+
shell: bash
165202
if: ${{ failure() }}
166203
run: |
204+
"${GITHUB_ACTION_PATH}/bin/webhook_url_check.sh" "${PACKAGIST_URL}" "${WEBHOOK_ERRORURL}"
167205
jq -n '{
168206
"runId": env.RUN_ID,
169207
"gitInfo": {
@@ -177,7 +215,16 @@ runs:
177215
"ciScriptVersion": env.CONDUCTOR_ACTION_VERSION
178216
}
179217
}' | curl -fsSL -X POST \
180-
-u "${{ github.event.client_payload.webhook.authentication.username }}:${{ github.event.client_payload.webhook.authentication.password }}" \
218+
-u "${WEBHOOK_AUTHENTICATION_USERNAME}:${WEBHOOK_AUTHENTICATION_PASSWORD}" \
181219
--header "Content-Type: application/json" \
182220
--data @- \
183-
"${{ github.event.client_payload.webhook.errorUrl }}"
221+
"${WEBHOOK_ERRORURL}"
222+
env:
223+
RUN_ID: ${{ github.run_id }}
224+
BASE_COMMIT_HASH: ${{ steps.base_commit_info.outputs.HASH }}
225+
BASE_COMMIT_AUTHOR: ${{ steps.base_commit_info.outputs.AUTHOR }}
226+
BASE_COMMIT_MESSAGE: ${{ steps.base_commit_info.outputs.MESSAGE }}
227+
WEBHOOK_AUTHENTICATION_USERNAME: ${{ github.event.client_payload.webhook.authentication.username }}
228+
WEBHOOK_AUTHENTICATION_PASSWORD: ${{ github.event.client_payload.webhook.authentication.password }}
229+
WEBHOOK_ERRORURL: ${{ github.event.client_payload.webhook.errorUrl }}
230+
PACKAGIST_URL: ${{ inputs.packagist_url }}

bin/branch_name_check.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
BRANCH="${1:?branch name required}"
5+
6+
# Require every Conductor-managed branch to start with the literal prefix
7+
# "conductor" and contain only characters that are safe inside a git refspec.
8+
BRANCH_RE='^conductor[A-Za-z0-9._/-]*$'
9+
10+
if [[ ! "${BRANCH}" =~ ${BRANCH_RE} ]]; then
11+
echo "::error ::branch '${BRANCH}' is not allowed; must start with 'conductor' and contain only [A-Za-z0-9._/-]"
12+
exit 1
13+
fi

bin/run_composer_command.sh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
EXPECTED_SUBCOMMAND="${1:?expected subcommand required}"
5+
: "${COMPOSER_COMMAND_STRING:?COMPOSER_COMMAND_STRING not set}"
6+
7+
# `read -ra` splits on $IFS only; it does not expand $vars, run command
8+
# substitutions, honour quoting, or perform globbing. Every shell metacharacter
9+
# in the payload therefore stays as a literal byte inside its token.
10+
read -ra TOKENS <<< "${COMPOSER_COMMAND_STRING}"
11+
12+
if [[ "${#TOKENS[@]}" -lt 2 ]]; then
13+
echo "::error ::composer command must contain at least a binary and a subcommand"
14+
exit 1
15+
fi
16+
17+
if [[ "${TOKENS[0]}" != "composer" ]]; then
18+
echo "::error ::composer command must start with 'composer', got '${TOKENS[0]}'"
19+
exit 1
20+
fi
21+
22+
if [[ "${TOKENS[1]}" != "${EXPECTED_SUBCOMMAND}" ]]; then
23+
echo "::error ::composer subcommand must be '${EXPECTED_SUBCOMMAND}', got '${TOKENS[1]}'"
24+
exit 1
25+
fi
26+
27+
# Reject tokens containing characters that have no business
28+
# appearing in a Composer package name, version constraint, or flag.
29+
SAFE_TOKEN_RE='^[A-Za-z0-9._:/@^+|=~*,<>!-]+$'
30+
for token in "${TOKENS[@]}"; do
31+
if [[ ! "${token}" =~ ${SAFE_TOKEN_RE} ]]; then
32+
echo "::error ::composer command token '${token}' contains disallowed characters"
33+
exit 1
34+
fi
35+
done
36+
37+
set -x
38+
# Argv-form execution: bash passes each array element as one argv entry with
39+
# no further parsing, so metacharacters inside a token reach Composer as
40+
# literal string data rather than as shell syntax.
41+
exec composer "${TOKENS[@]:1}"

bin/webhook_url_check.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
TRUSTED_BASE="${1:?trusted base URL required}"
5+
URL="${2:?webhook URL required}"
6+
7+
# Strip a single trailing slash from the base so the prefix check below can
8+
# always append "/". Requiring the URL to start with "<base>/" prevents a host
9+
# like "packagist.com.evil.example" from sneaking past "packagist.com".
10+
TRUSTED_BASE="${TRUSTED_BASE%/}"
11+
12+
case "${TRUSTED_BASE}" in
13+
https://*) ;;
14+
*) echo "::error ::packagist_url must use https://, got '${TRUSTED_BASE}'"; exit 1 ;;
15+
esac
16+
17+
case "${URL}" in
18+
"${TRUSTED_BASE}/"*) ;;
19+
*) echo "::error ::webhook URL '${URL}' is not under the trusted base '${TRUSTED_BASE}/'"; exit 1 ;;
20+
esac
21+
22+
# Restrict the path portion after the trusted base to alphanumerics, dashes,
23+
# and forward slashes. This blocks query strings, fragments, percent-encoding,
24+
# and any other characters that have no business appearing in a Conductor
25+
# webhook callback path.
26+
SUFFIX="${URL#"${TRUSTED_BASE}/"}"
27+
case "${SUFFIX}" in
28+
*[!A-Za-z0-9/-]*)
29+
echo "::error ::webhook URL path '${SUFFIX}' must contain only alphanumerics, '-' and '/'"
30+
exit 1
31+
;;
32+
esac

0 commit comments

Comments
 (0)