Skip to content

Commit bcf2e24

Browse files
authored
Merge branch 'main' into CM-59977-sca-cli-maintainability-improvements
2 parents d094f97 + 8e4450c commit bcf2e24

File tree

2 files changed

+90
-11
lines changed

2 files changed

+90
-11
lines changed

.github/workflows/build_executable.yml

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -125,20 +125,34 @@ jobs:
125125
echo "PATH_TO_CYCODE_CLI_EXECUTABLE=dist/cycode-cli/cycode-cli" >> $GITHUB_ENV
126126
127127
- name: Test executable
128-
run: time $PATH_TO_CYCODE_CLI_EXECUTABLE version
128+
run: time $PATH_TO_CYCODE_CLI_EXECUTABLE status
129129

130130
- name: Codesign onedir binaries
131131
if: runner.os == 'macOS' && matrix.mode == 'onedir'
132132
env:
133133
APPLE_CERT_NAME: ${{ secrets.APPLE_CERT_NAME }}
134134
run: |
135-
# Sign all Mach-O binaries in the onedir output (excluding the main executable)
136-
# Main executable must be signed last after all its dependencies
137-
find dist/cycode-cli -type f ! -name "cycode-cli" | while read -r file; do
135+
# The standalone _internal/Python fails codesign --verify --strict because it was
136+
# extracted from Python.framework without Info.plist context.
137+
# Fix: remove the bare copy and replace with the framework version's binary,
138+
# then delete the framework directory (it's redundant).
139+
if [ -d dist/cycode-cli/_internal/Python.framework ]; then
140+
FRAMEWORK_PYTHON=$(find dist/cycode-cli/_internal/Python.framework/Versions -name "Python" -type f | head -1)
141+
if [ -n "$FRAMEWORK_PYTHON" ]; then
142+
echo "Replacing _internal/Python with framework binary"
143+
rm dist/cycode-cli/_internal/Python
144+
cp "$FRAMEWORK_PYTHON" dist/cycode-cli/_internal/Python
145+
fi
146+
rm -rf dist/cycode-cli/_internal/Python.framework
147+
fi
148+
149+
# Sign all Mach-O binaries (excluding the main executable)
150+
while IFS= read -r file; do
138151
if file -b "$file" | grep -q "Mach-O"; then
152+
echo "Signing: $file"
139153
codesign --force --sign "$APPLE_CERT_NAME" --timestamp --options runtime "$file"
140154
fi
141-
done
155+
done < <(find dist/cycode-cli -type f ! -name "cycode-cli")
142156
143157
# Re-sign the main executable with entitlements (must be last)
144158
codesign --force --sign "$APPLE_CERT_NAME" --timestamp --options runtime --entitlements entitlements.plist dist/cycode-cli/cycode-cli
@@ -176,15 +190,35 @@ jobs:
176190
177191
# we can't staple the app because it's executable
178192
179-
- name: Test macOS signed executable
193+
- name: Verify macOS code signatures
180194
if: runner.os == 'macOS'
181195
run: |
182-
file -b $PATH_TO_CYCODE_CLI_EXECUTABLE
183-
time $PATH_TO_CYCODE_CLI_EXECUTABLE version
196+
FAILED=false
197+
while IFS= read -r file; do
198+
if file -b "$file" | grep -q "Mach-O"; then
199+
if ! codesign --verify "$file" 2>&1; then
200+
echo "INVALID: $file"
201+
codesign -dv "$file" 2>&1 || true
202+
FAILED=true
203+
else
204+
echo "OK: $file"
205+
fi
206+
fi
207+
done < <(find dist/cycode-cli -type f)
208+
209+
if [ "$FAILED" = true ]; then
210+
echo "Found binaries with invalid signatures!"
211+
exit 1
212+
fi
184213
185-
# verify signature
186214
codesign -dv --verbose=4 $PATH_TO_CYCODE_CLI_EXECUTABLE
187215
216+
- name: Test macOS signed executable
217+
if: runner.os == 'macOS'
218+
run: |
219+
file -b $PATH_TO_CYCODE_CLI_EXECUTABLE
220+
time $PATH_TO_CYCODE_CLI_EXECUTABLE status
221+
188222
- name: Import cert for Windows and setup envs
189223
if: runner.os == 'Windows'
190224
env:
@@ -222,7 +256,7 @@ jobs:
222256
shell: cmd
223257
run: |
224258
:: call executable and expect correct output
225-
.\dist\cycode-cli.exe version
259+
.\dist\cycode-cli.exe status
226260
227261
:: verify signature
228262
signtool.exe verify /v /pa ".\dist\cycode-cli.exe"
@@ -236,6 +270,47 @@ jobs:
236270
name: ${{ env.ARTIFACT_NAME }}
237271
path: dist
238272

273+
- name: Verify macOS artifact end-to-end
274+
if: runner.os == 'macOS' && matrix.mode == 'onedir'
275+
uses: actions/download-artifact@v4
276+
with:
277+
name: ${{ env.ARTIFACT_NAME }}
278+
path: /tmp/artifact-verify
279+
280+
- name: Verify macOS artifact signatures and run with quarantine
281+
if: runner.os == 'macOS' && matrix.mode == 'onedir'
282+
run: |
283+
# extract the onedir zip exactly as an end user would
284+
ARCHIVE=$(find /tmp/artifact-verify -name "*.zip" | head -1)
285+
echo "Verifying archive: $ARCHIVE"
286+
unzip "$ARCHIVE" -d /tmp/artifact-extracted
287+
288+
# verify all Mach-O code signatures
289+
FAILED=false
290+
while IFS= read -r file; do
291+
if file -b "$file" | grep -q "Mach-O"; then
292+
if ! codesign --verify "$file" 2>&1; then
293+
echo "INVALID: $file"
294+
codesign -dv "$file" 2>&1 || true
295+
FAILED=true
296+
else
297+
echo "OK: $file"
298+
fi
299+
fi
300+
done < <(find /tmp/artifact-extracted -type f)
301+
302+
if [ "$FAILED" = true ]; then
303+
echo "Artifact contains binaries with invalid signatures!"
304+
exit 1
305+
fi
306+
307+
# simulate download quarantine and test execution
308+
# this is the definitive test — it triggers the same dlopen checks end users experience
309+
find /tmp/artifact-extracted -type f -exec xattr -w com.apple.quarantine "0081;$(printf '%x' $(date +%s));CI;$(uuidgen)" {} \;
310+
EXECUTABLE=$(find /tmp/artifact-extracted -name "cycode-cli" -type f | head -1)
311+
echo "Testing quarantined executable: $EXECUTABLE"
312+
time "$EXECUTABLE" status
313+
239314
- name: Upload files to release
240315
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish }}
241316
uses: svenstaro/upload-release-action@v2

process_executable_file.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ def get_cli_archive_path(output_path: Path, is_onedir: bool) -> str:
140140
return os.path.join(output_path, get_cli_archive_filename(is_onedir))
141141

142142

143+
def archive_directory(input_path: Path, output_path: str) -> None:
144+
shutil.make_archive(output_path.removesuffix(f'.{_ARCHIVE_FORMAT}'), _ARCHIVE_FORMAT, input_path)
145+
146+
143147
def process_executable_file(input_path: Path, is_onedir: bool) -> str:
144148
output_path = input_path.parent
145149
hash_file_path = get_cli_hash_path(output_path, is_onedir)
@@ -150,7 +154,7 @@ def process_executable_file(input_path: Path, is_onedir: bool) -> str:
150154
write_hashes_db_to_file(normalized_hashes, hash_file_path)
151155

152156
archived_file_path = get_cli_archive_path(output_path, is_onedir)
153-
shutil.make_archive(archived_file_path, _ARCHIVE_FORMAT, input_path)
157+
archive_directory(input_path, f'{archived_file_path}.{_ARCHIVE_FORMAT}')
154158
shutil.rmtree(input_path)
155159
else:
156160
file_hash = get_hash_of_file(input_path)

0 commit comments

Comments
 (0)