Workflow File
Create.github/workflows/binarly-scan.yml:
Copy
Ask AI
name: Binarly Firmware Scan
on:
push:
paths:
- 'firmware/**'
workflow_dispatch:
env:
# Replace {slug} with your organization's tenant identifier
BINARLY_AUTH_URL: https://auth-{slug}.binarly.cloud/realms/BinarlyRealm/protocol/openid-connect/token
BINARLY_API_URL: https://dashboard-{slug}.binarly.cloud
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Firmware
run: make firmware # Your build command
- name: Authenticate with Binarly
id: auth
run: |
TOKEN=$(curl -s --fail-with-body \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id=${{ secrets.BINARLY_CLIENT_ID }}" \
--data-urlencode "client_secret=${{ secrets.BINARLY_CLIENT_SECRET }}" \
"$BINARLY_AUTH_URL" | jq -r '.access_token')
echo "token=$TOKEN" >> $GITHUB_OUTPUT
- name: Upload Firmware
id: upload
run: |
# Generate pre-signed URL
UPLOAD_RES=$(curl -s -H "Authorization: Bearer ${{ steps.auth.outputs.token }}" \
"$BINARLY_API_URL/api/v4/products/${{ secrets.BINARLY_PRODUCT_ID }}/tempFiles:generateUploadUrl")
UPLOAD_URL=$(echo $UPLOAD_RES | jq -r '.uploadUrl')
TEMP_ID=$(echo $UPLOAD_RES | jq -r '.id')
# Upload binary
curl -s -X PUT --data-binary @./build/firmware.bin "$UPLOAD_URL"
# Finalize
IMAGE_RES=$(curl -s -H "Authorization: Bearer ${{ steps.auth.outputs.token }}" \
-F "tempFileId=$TEMP_ID" \
-F "imageName=GitHub Build ${{ github.run_number }}" \
-F "version=${{ github.sha }}" \
-F "file=@./build/firmware.bin" \
"$BINARLY_API_URL/api/v4/products/${{ secrets.BINARLY_PRODUCT_ID }}/images:upload")
IMAGE_ID=$(echo $IMAGE_RES | jq -r '.id')
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
- name: Wait for Scan
run: |
STATUS="running"
while [ "$STATUS" != "completed" ]; do
sleep 30
SCAN_RES=$(curl -s -H "Authorization: Bearer ${{ steps.auth.outputs.token }}" \
"$BINARLY_API_URL/api/v4/products/${{ secrets.BINARLY_PRODUCT_ID }}/images/${{ steps.upload.outputs.image_id }}/scans?status=true")
STATUS=$(echo $SCAN_RES | jq -r '.scans[0].latestScanState.type')
echo "Scan status: $STATUS"
[ "$STATUS" == "failed" ] && exit 1
done
echo "Scan completed successfully"
- name: Verify Results
id: verify
env:
FAIL_ON_STATUS: "new,inProgress" # Configurable: new,inProgress,rejected,remediated
run: |
echo "Binarly Security Verification"
echo "Failing on statuses: $FAIL_ON_STATUS"
TOKEN="${{ steps.auth.outputs.token }}"
BINARLY_PRODUCT_ID="${{ secrets.BINARLY_PRODUCT_ID }}"
IMAGE_ID="${{ steps.upload.outputs.image_id }}"
# Get all images for the product
IMAGES_RES=$(curl -s -H "Authorization: Bearer $TOKEN" \
"$BINARLY_API_URL/api/v4/products/$BINARLY_PRODUCT_ID/images")
IMAGE_COUNT=$(echo "$IMAGES_RES" | jq '.images | length')
echo "Product has ${IMAGE_COUNT} image(s)"
# If multiple images exist, compare latest with previous
if [ "$IMAGE_COUNT" -gt 1 ]; then
# Sort images by createTime descending
LATEST_ID=$(echo "$IMAGES_RES" | jq -r '[.images | sort_by(.createTime) | reverse][0][0].id')
PREVIOUS_ID=$(echo "$IMAGES_RES" | jq -r '[.images | sort_by(.createTime) | reverse][0][1].id')
echo "Comparing: $PREVIOUS_ID → $LATEST_ID"
# Count RESOLVED findings
RESOLVED=$(curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"filters":[
{"field":"compareLeftImageId","value":"'"$PREVIOUS_ID"'","comparator":"equals"},
{"field":"compareRightImageId","value":"'"$LATEST_ID"'","comparator":"equals"},
{"field":"compareSide","value":"left","comparator":"equals"}
]}' \
"$BINARLY_API_URL/api/v4/grids/findings:gridList" | jq '.total // 0')
# Count UNCHANGED findings
UNCHANGED=$(curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"filters":[
{"field":"compareLeftImageId","value":"'"$PREVIOUS_ID"'","comparator":"equals"},
{"field":"compareRightImageId","value":"'"$LATEST_ID"'","comparator":"equals"},
{"field":"compareSide","value":"both","comparator":"equals"}
]}' \
"$BINARLY_API_URL/api/v4/grids/findings:gridList" | jq '.total // 0')
# Count NEW findings
NEW_ISSUES=$(curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"filters":[
{"field":"compareLeftImageId","value":"'"$PREVIOUS_ID"'","comparator":"equals"},
{"field":"compareRightImageId","value":"'"$LATEST_ID"'","comparator":"equals"},
{"field":"compareSide","value":"right","comparator":"equals"}
]}' \
"$BINARLY_API_URL/api/v4/grids/findings:gridList" | jq '.total // 0')
echo "Resolved: $RESOLVED | Unchanged: $UNCHANGED | New: $NEW_ISSUES"
fi
# Check for findings matching configured statuses (using 'in' comparator with array)
STATUSES_JSON=$(echo "$FAIL_ON_STATUS" | tr ',' '\n' | jq -R . | jq -s .)
# Use LATEST_ID if available (from comparison), otherwise use IMAGE_ID from upload step
CHECK_IMAGE_ID="${LATEST_ID:-$IMAGE_ID}"
TOTAL_ACTIONABLE=$(curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"filters":[
{"field":"imageId","value":"'"$CHECK_IMAGE_ID"'","comparator":"equals"},
{"field":"issueStatus","value":'"$STATUSES_JSON"',"comparator":"in"}
]}' \
"$BINARLY_API_URL/api/v4/grids/findings:gridList" | jq '.total // 0')
echo " Statuses checked: $FAIL_ON_STATUS"
echo "Total actionable findings: $TOTAL_ACTIONABLE"
if [ "$TOTAL_ACTIONABLE" -gt 0 ]; then
echo "BUILD FAILED: $TOTAL_ACTIONABLE actionable finding(s)"
exit 1
fi
echo "BUILD PASSED: No actionable findings"
Required Secrets
Configure these in Settings → Secrets and variables → Actions:| Secret | Description |
|---|---|
BINARLY_CLIENT_ID | Keycloak API Client ID |
BINARLY_CLIENT_SECRET | Keycloak API Client Secret |
BINARLY_PRODUCT_ID | Target product ID |
Triggering the Workflow
The workflow runs automatically when:- Files in
firmware/are pushed - Manually triggered via
workflow_dispatch
on: section to match your project structure.
For detailed explanation of the verification logic, see Verify Results.